Ultimate

Static assets

A public file lives in apps/web/site/assets/, and a page names it with asset(). One call returns a content-hashed URL, served public, max-age=31536000, immutable by x dev and the container alike, and copied into the static export under the same name. As of 2026-09-25 (22.3.0).

import { asset } from '@ultimat3/render';
import { Image } from '@ultimat3/ui';

<Image
  src={asset('assets/hero-1280.jpg')}
  alt=""
  width={1280}
  height={720}
  sizes="(max-width: 700px) 100vw, 1280px"
  priority
  sources={{
    avif: [
      { src: asset('assets/hero-640.avif'), width: 640 },
      { src: asset('assets/hero-1280.avif'), width: 1280 },
    ],
    webp: [
      { src: asset('assets/hero-640.webp'), width: 640 },
      { src: asset('assets/hero-1280.webp'), width: 1280 },
    ],
  }}
/>

asset('assets/hero-640.avif') → /assets/hero-640.3f2a1b9c.avif.

The rules

RuleEnforced by
the path starts with assets/ and ends in a served extensionthe type: AssetPath is `assets/${string}.${AssetExtension}`, so asset('hero.avif') and asset('assets/x.txt') are TS2345
the file existsX_ASSET_MISSING, thrown while the page renders, so x build --target static fails on it and x dev shows it
no .., no empty segment, no backslashassetPathProblem(), the same check in the page, the table and the route
only the hashed URL is served/assets/hero.avif is 404; so is a hash that no longer matches the bytes (a document from an earlier build)
the URL changes when the bytes changethe hash is xxHash32 of the file, re-read when its mtime or size changes, with no watcher and no restart
an earlier build's URL survives a deploya static host keeps the old hashed file because nothing deletes it. The container answers it only with assets.retain in app.config.ts, which reads it from a shared disk on a miss (Deployment); without it, the stale hash is the 404 above

Call asset() in a page or a server component, never in an island. A browser has no asset table, so there it throws X_ASSET_MISSING: pass the URL to the island as a prop instead.

From a stylesheet

Sass has the same function, over the same table: asset('assets/…') inside url() compiles to the hashed URL, in x dev, the container and x build --target static alike.

@font-face {
  font-family: Inter;
  src: url(asset('assets/fonts/inter-var.woff2')) format('woff2');
  font-display: swap;
}

.hero { background-image: url(asset('assets/hero-1280.avif')); }

→ src: url("/assets/fonts/inter-var.9c1d2e3f.woff2") format("woff2"). A missing file or a path outside assets/ is X_ASSET_MISSING at compile time, naming the path — not a Sass stack. The Sass disk cache re-checks every asset a cached sheet named, so a replaced font is never served under its old hash. Sass has no table without an app loaded, so a unit test that compiles such a sheet installs one with setAssetResolver, as below.

Served types

Extensioncontent-type
avifimage/avif
webpimage/webp
pngimage/png
jpg, jpegimage/jpeg
gifimage/gif
svgimage/svg+xml
icoimage/x-icon
woff2font/woff2
mp4video/mp4
webmvideo/webm
vtttext/vtt; charset=utf-8

Any other file under assets/ (a .psd, a README.md) is not served and not copied.

Byte ranges are answered. Range: bytes=… gets a 206 with content-range, and an unsatisfiable range gets a 416. Safari will not play a <video> without this.

Where it happens

QuestionAnswer
the tablepackages/cli/src/site-assets.ts: one table per app root, installed into asset() by loadApp
the routepackages/cli/src/site-asset-routes.ts: GET /assets/*file, mounted by assetRoutes(), which both x dev and the container compose
the exportwriteSiteAssets(root, out) copies every asset to out/assets/…/<name>.<hash>.<ext>
the helperpackages/render/src/asset.ts: asset(), setAssetResolver(), assetPathProblem()

favicon.ico stays at apps/web/site/favicon.ico and the install icon at apps/web/site/icon.png. Browsers ask for those at fixed URLs, so they are not hashed.

<Image>

PropMeaning
srcthe fallback rendition every browser decodes
altrequired. "" for a decorative image, on purpose
width + height, or aspectRatio ('16 / 9')required. A box is always reserved: neither, one dimension alone, or both forms together is X_UI_INVALID_VALUE
variantswidths (or densities) of src's own encoding, rendered as the <img>'s srcset
sources{ avif?, webp? }, each a width list. Renders a <picture> with AVIF first, then WebP, then the <img>
sizesapplies to every set
prioritythe LCP image: loading="eager" + fetchpriority="high". At most one per route. Never lazy

Everything else is loading="lazy", fetchpriority="auto" and decoding="async".

Responsive images in a static export

responsiveImage() from @ultimat3/seo mints ?w=&f= URLs, which only /media answers — a dumb file server ignores the query and hands back the full original under <source type="image/webp">. So under x build --target static every candidate is a FILE instead: /_img/<source hash>-<width>.<ext>, written through core's pipeline after the documents render, one per width in every offered format. As of 2026-11.

RuleDetail
the srca file the export holds — an asset('assets/hero.png') URL. Anything else is X_IMAGE_SOURCE_UNREADABLE at build; an absolute https:// URL keeps its query URL for a CDN that answers one
the widthscapped at the source's own (usableWidths), never an upscale
the formatswebp and the source's encoding. avif at a static build is X_IMAGE_UNSUPPORTED, a build error, never a dead <source>
what is writtenonly the candidates an EXPORTED document names: a page rendered just to be weighed writes nothing
a served pagekeeps the ?w=&f= URLs, which /assets answers itself — below. The seam is installed by the export alone

The name is the source's content hash, so a changed image is a new URL and every file can be served immutable. examples/dummy's landing page (apps/web/site/page.tsx) is the worked example.

Responsive images on a served page

x dev and the container answer the same candidates on /assets/<name>.<hash>.<ext>?w=&f=: the variant is resized and re-encoded through the pipeline /media uses (core's, or the ImageTransformDriver the boot was handed), with its own content-type and public, max-age=31536000, immutable. As of 2026-11.

RuleDetail
the widthsexactly the srcset's: usableWidths(intrinsic, DEFAULT_WIDTHS) — never an upscale
the formatsDEFAULT_FORMATS (webp), or none for the source's own encoding
anything else?w=321, ?f=avif, ?f=jpeg, any ?q=, a ?w= on an SVG: 404 X_ASSET_VARIANT_UNDECLARED, before a pixel is decoded. A public URL is never a resize service; an upload under /media answers any width
a custom widthsnot served for a site asset; drop the option, or store the image and use /media
the cacheone decode per variant per content hash, in memory, least recently used out past 64 MiB; a burst of first visitors is one decode
a stale hashthe plain route's answer, query or not

In a unit test

A test that renders a page calling asset() without loading the app installs a table itself:

import { setAssetResolver } from '@ultimat3/render';

beforeAll(() => setAssetResolver((path) => `/${path}`));
afterAll(() => setAssetResolver(undefined));

Screenshots

x shot <route> --locale en photographs /en/<route> with Accept-Language: en. With no --locale, Accept-Language is still pinned to the app's default locale. x shot --matrix photographs every site route in every locale, in light and dark, at 390 and 1440 px, into .x/shot/matrix/, with an index.html contact sheet. See CLI reference.

Edit this page on GitHubThis page as markdown