Images & Fonts
Fast images, self-hosted fonts.
An <Image> helper that emits responsive srcset/sizes markup
with explicit dimensions (no layout shift), a build-time transform that resizes and re-encodes into
dist/, and self-hosted fonts with @font-face + preload — so Apex
matches Next / Nuxt on image and font optimization.
🟡 Experimental
What you get #
Two independent, opt-in optimizations — turn on either or both. Both are gated on a config block: declare nothing and nothing changes.
- The
<Image>helper (@apex-stack/kit) — a pure function that renders a responsive<img>:srcsetcandidates, asizesattribute, explicitwidth/heightto reserve layout (no CLS), andloading="lazy"/decoding="async"defaults. - The image transform — an
imageblock inapex.config.tswiresvite-imagetoolsinto the client build for the default (apex build) and--servertargets, turning the?w=…&format=…candidates into hashed, optimized variants underdist/. (Transforming per-page image imports under--islandsis a known follow-up — #57; fonts work in all modes.) - Self-hosted fonts — a
fontsblock copies declared font files intodist/fonts/, emits the matching@font-facerules, and injects<link rel="preload" as="font" crossorigin>into the page shell.
Nothing is forced: the <Image> helper works on its own (plain URLs still render), and the
transform / fonts steps only run when their config block is present — keeping the core dependency-light for
projects that don't need them.
Quickstart #
Render a responsive image from a template or loader, and (optionally) turn on the build transform + fonts.
// anywhere you build HTML — a loader, a template helper, the client
import { image } from '@apex-stack/kit'
image({ src: '/hero.png', width: 800, height: 600, alt: 'Hero', format: 'webp' })
// → <img src="/hero.png" width="800" height="600" alt="Hero"
// loading="lazy" decoding="async"
// srcset="/hero.png?w=800&format=webp 800w, /hero.png?w=1600&format=webp 1600w"
// sizes="100vw" />
// apex.config.ts — opt in to the build transform + self-hosted fonts
export default defineConfig({
image: { formats: ['webp'], sizes: [640, 1200], quality: 70 },
fonts: {
families: [{ family: 'Inter', src: 'fonts/Inter.woff2', weight: 400 }],
},
})
The <Image> helper #
image(opts) returns an HTML string; imageAttrs(opts) returns the same as a
structured attribute map if you'd rather spread it into your own renderer. Both are pure and browser-safe —
no DOM, no Node, no imports.
| Option | Required | Default | What it does |
|---|---|---|---|
src | Yes | — | The image URL / import path. Query candidates are appended to build srcset. |
width / height | Yes | — | Intrinsic dimensions, emitted as attributes to reserve layout and avoid CLS. |
alt | No | "" | Alt text (always emitted; empty = decorative). |
widths | No | [width, width*2] | Candidate widths for srcset (retina by default). Pass [] to omit srcset. |
sizes | No | 100vw | The sizes attribute (only when a srcset is emitted). |
format | No | — | Target format appended to each candidate (?format=webp) for the transform. |
loading | No | lazy | eager for above-the-fold hero images. |
decoding | No | async | Decoding hint. |
fetchpriority | No | — | high for the LCP image. |
An above-the-fold hero, told to load eagerly at high priority with explicit candidates:
image({
src: '/lcp.jpg', width: 1200, height: 630, alt: 'Cover',
widths: [640, 1200], sizes: '(max-width: 640px) 100vw, 1200px',
loading: 'eager', fetchpriority: 'high',
})
The image config block #
Add an image block and apex build wires vite-imagetools into the
client bundle. Imports carrying transform queries — the ones the helper's srcset
emits, or ones you write by hand — are resized and re-encoded into hashed variants in dist/.
The block's mere presence turns the transform on; every field is optional.
The transform runs in the default apex build (prerender + hydrate) and
apex build --server builds. Transforming per-page image imports under
apex build --islands (SSG) is a known follow-up (#57) — self-hosted
fonts already work in every mode.
export default defineConfig({
image: {
formats: ['avif', 'webp'], // optional — output formats (defaults to source format)
sizes: [640, 828, 1200], // optional — responsive srcset widths the helper mirrors
quality: 70, // optional — encode quality (1–100) for lossy formats
},
})
| Field | Required | Default | What it does |
|---|---|---|---|
formats | No | source format | Output formats applied as the transform's default directives. |
sizes | No | — | Responsive srcset widths; align these with the widths you pass the helper. |
quality | No | lib default | Encode quality (1–100) for lossy output formats. |
vite-imagetools is loaded lazily and optionally — like the Tailwind plugin. If it isn't
installed, the transform is skipped rather than failing the build. Add it with
pnpm add -D vite-imagetools.
Self-hosted fonts #
Declare a fonts block and apex build copies the files into dist/fonts/,
emits an @font-face rule per family, and injects a
<link rel="preload" as="font" crossorigin> per face into the shell — the browser fetches
the font early, from your origin, with no third-party request and no flash of invisible text.
export default defineConfig({
fonts: {
display: 'swap', // optional — font-display for every face (default: swap)
preload: true, // optional — emit preload links (default: true)
families: [
{ family: 'Inter', src: 'fonts/Inter.woff2', weight: 400 },
{ family: 'Inter', src: 'fonts/Inter-Italic.woff2', weight: 400, style: 'italic' },
],
},
})
The emitted head fragment, for the first family above:
<style>@font-face{font-family:"Inter";font-style:normal;font-weight:400;
font-display:swap;src:url("/fonts/Inter.woff2") format("woff2")}</style>
<link rel="preload" as="font" type="font/woff2" href="/fonts/Inter.woff2" crossorigin />
| Field | Required | Default | What it does |
|---|---|---|---|
families[].family | Yes | — | The CSS font-family name. |
families[].src | Yes | — | Project-relative path to the font file; copied into dist/fonts/. |
families[].weight | No | 400 | font-weight ("100 900" for a variable font). |
families[].style | No | normal | font-style (italic / oblique). |
families[].format | No | from extension | Explicit format() hint; inferred from the file extension otherwise. |
display | No | swap | Global font-display for every emitted face. |
preload | No | true | Set false to emit the @font-face rules without preload hints. |
Verify it #
Build and inspect the output the way a browser does.
apex build
apex start # or any static host
- View source on a page using
image()— the<img>should carrysrcset,sizes, explicitwidth/height, andloading="lazy". - With an
imageblock, checkdist/for the hashed, re-encoded variants. - With a
fontsblock, confirm the files landed indist/fonts/and DevTools → Network shows the preload firing early with no third-party font request. - Run a Lighthouse pass — "properly size images", "serve images in next-gen formats", and "ensure text remains visible during webfont load" should be green.
Scope & status #
The honest edges.
- 🟡 Experimental — the config surface (
image/fonts) may still move. - The helper is markup-only —
image()emits the candidate URLs; the actual resizing happens only when theimageblock wires the transform in at build. - Transform is opt-in + optional — no
imageblock means no plugin; a missingvite-imagetoolsis skipped, not fatal. - No subsetting yet — fonts are copied and preloaded as-is; there's no automatic glyph subsetting.
See Components for how kit renders markup, and Build & deploy for the build modes the transform runs inside.