Ultimate

Routes and render modes

A route is a URL + render mode + metadata + offline strategy. Render mode is a per-route property. SEO is enforced by the build, not described in a guide.

As of 2026-08. Stable API — semver from here (Upgrading).

The canonical shape

export const config = defineRoute({
  render:     'isr',                  // static | isr | ssr | stream
  revalidate: { tags: [tag.post] },
  prerender:  () => db.posts.slugs(),
  offline:    'precache',             // precache | runtime | network-only
  hydrate:    'visible',              // idle | visible | interaction | never
  budget:     { js: '40kb' },
  meta:       ({ post }) => ({ title: post.title, description: post.excerpt,
                               og: { image: post.cover }, ld: ld.Article(post) }),
});
AspectRule
Projects torouter entry, prerender list, sw.js precache/runtime rule, sitemap + RSS row, <head> + JSON-LD, per-route budget check
Ownsrender mode, hydration timing, metadata, offline strategy
Nevertouch the DB directly, hold business logic, or omit meta.description in site/ — that is a build error

post — a page URL that also answers POST

defineRoute({ render: 'ssr', …, post: 'unsubscribeOnboarding' }) (As of 22.6.0) mounts POST at the page's own path, answered by that action — its policy, idempotency and redirect — with the URL's query merged over the posted fields (the query wins a shared name: it is the URL the server minted). The RFC 8058 case: the mail client POSTs List-Unsubscribe=One-Click to /correos/baja?t=… and the action receives { t, 'List-Unsubscribe': 'One-Click' }; the page's GET stays the confirm page, and its form can post to itself with no JavaScript. Declare optional input keys for what the client posts. Not an export name, or on render: 'static': X_ROUTE_POST_INVALID at declaration; naming no registered action: the same code at boot.

navigation — how the client router treats this page

Only on a surface listed in app.config.ts navigation.client (Client navigation); anywhere else X_ROUTE_NAVIGATION_INVALID at boot, and any value but these two at declaration.

ValueThe routerThe server, before load
absentswaps it in on a click; never fetches it earlyanswers a prefetch 204, runs nothing
'prefetch'also fetches it on hover/focusanswers a prefetch like a visit. Only for a GET that does nothing but render
'document'always a real document load, and the page carries no router (no script, no metas, no bytes charged)answers a soft visit 204 + x-ultimate-location, runs nothing; the browser's load runs it once. For a GET that records something: an open, a download, a token consumed — and for a 0b page

The descriptor

route is the one primitive whose façade is a normalized descriptor rather than a set of projection methods, because a route declares no behaviour to project. It is read by the router, the prerenderer, the sitemap and the budget check — and is designed to be read by sw.js generation, which no build calls yet (#362). The descriptor's job is to hand all of them one shape so none of them branches. There is no .describe() on a route either — describePages() is the one route list.

MemberIsRule
kind: 'route'the brandlets the registry reject a non-route export. isRouteConfig(value) is the guard
meta(data)the <head> produceralways returns a promise. A synchronous declared meta is wrapped, and a meta that throws synchronously becomes a rejection — so await config.meta(data) is the one way to fail as well as the one way to succeed, and no consumer branches on a thenable
budgetthe per-route limitsalways an object, {} when undeclared, rather than a second undefined-check at every call site
render revalidate prerender offline hydrate policy cachethe declaration, carried throughunchanged. Optional keys are omitted, never set to undefined
the whole objectObject.freezedno consumer can mutate the route another consumer is about to read

The always-present budget does not weaken the site/ JS-budget check. budget is always an object, but its fields stay optional — so budget.js === undefined still means "this route declared no JS budget", and a site/ route that opts into any hydrate other than never without one is X_ROUTE_MODE_INVALID, exactly as before the normalization. Measuring the declared numbers against the built output is the separate concern in Budgets, below.

Validation runs at module evaluation. defineRoute checks the shape and the mode-local invariants immediately, so a bad route fails at build time rather than on the first request in production.

CheckedWhereCode
offline present and a known strategydefineRouteX_ROUTE_OFFLINE_MISSING
meta is a functiondefineRouteX_ROUTE_META_MISSING
mode-local: known render and hydrate; static with a policy or a revalidate; isr with a policy or with no trigger; ssr with a prerender; cache on any mode but ssr, or a public/immutable cache on a gated routedefineRouteX_ROUTE_MODE_INVALID
surface-dependent: mode allowed on the surface; site/ hydration without budget.js; prerender on a non-prerenderable moderegisterRouteX_ROUTE_MODE_INVALID
the config came from defineRoute and not straight from the authorregisterRouteX_ROUTE_UNNORMALIZED
two files claiming one URLregisterRouteX_ROUTE_DUPLICATE

The split is about what is knowable, not about strictness: everything decidable from the config alone is decided at import; the rest needs the file's surface, which only the route table knows.

Four render modes

ModeBehaviorUse
staticbuilt once, served as a filemarketing, docs
isrstatic + background regen on tag/TTLcatalogs, profiles
ssrper-request full renderfresh SEO pages
streamstatic shell flushed instantly, holes streameddefault for app pages

A served static page, As of 2026-09: a matching If-None-Match is a bodiless 304, and the container renders each page once per locale and answers from memory after — no load, no render — for a page with no dynamic segment, asked for with no query string, that answered 200. x dev renders per request. No other mode is kept this way. What is kept is bounded twice — 1,024 entries and 32 MiB of bodies (STATIC_MEMO_MAX_BYTES) — because the request origin is in the key when the app declares none; past either bound a page is rendered per request.

SurfaceDefaultAllowed
site/staticstatic, isr, ssr
app/streamstream, ssr
api/n/ano rendering at all

A mode outside a surface's allowed set is a build error, not a runtime fallback. Surfaces and their boundaries: Project layout.

spa was the fifth mode and was deleted in 6.0.0. It served <body><div id="x-root"></div></body> for the framework's whole history — 200, correct headers, blank page — because renderSpa preloaded a chunks array no build ever produced and never read the route's component. render: 'spa' is now X_ROUTE_MODE_INVALID at defineRoute time; the migration is one line, render: 'ssr', in Upgrading.

What a mode's headers are, and who has the last word

A render mode states the mode's intent; @ultimat3/http's cache-headers stage makes the final answer. As of 2026-08-23 that stage REVIEWS a cache-control the mode already wrote instead of standing down, which is what made the rule below reachable for a page at all.

The requestWhat it gets, whatever the mode wrote
anonymousthe mode's own intent, plus the key dimensions it forgot: vary: accept-language, cookie, x-timezone
carrying an identityprivate, max-age=0 — never public, never s-maxage, and never bare freshness: max-age, must-revalidate or proxy-revalidate without private/no-store is an offer a shared cache may take (RFC 9111 §3), so it is rewritten too. meta.auth is only 'public' | 'required', so the page that greets a signed-in visitor by name is a 'public' route whose own header offered it to a CDN for 30 seconds
a content-addressed URL (immutable)left alone. immutable asserts the body is a function of the URL, which is what an island chunk is

A route's own cache, on ssr

As of 2026-09-24. An ungated ssr page is offered to a CDN for 30 seconds (public, max-age=0, s-maxage=30, stale-while-revalidate=300). A page can be public and still personal: a recipient landing addressed by a capability token in its path, or a verification page that must never answer with an old result. cache replaces that default:

export const config = defineRoute({
  render: 'ssr',
  offline: 'network-only',
  cache: 'no-store',                  // or a CacheHint: { mode: 'public', sMaxAgeSeconds: 5 }
  meta: ({ t }) => ({ title: t('verify.title'), description: t('verify.description') }),
});
'no-store'private, no-store, the answer a gated page already gets. It carries the principal scope like every private document
a CacheHint@ultimat3/http's own type, without tags, emitted by cacheControl(). Its vary joins the defaults. The router checks its ages at mount (X_CONFIG_INVALID)
which modesssr only. static and isr are hashed or TTL'd documents whose headers are the mode itself. A stream is always private, no-store. On any of them cache is X_ROUTE_MODE_INVALID
a gated routemay narrow its cache ('no-store', { mode: 'private', … }), never widen it. public or immutable is X_ROUTE_MODE_INVALID
a signed-in visitorthe table above still applies. A declared public becomes private, max-age=0 for them

A shared document is rendered as nobody. As of 2026-11-02. isr and static run load, meta and the page with the anonymous actor, services rebuilt for it, the app's default zone, ctx.url at the document's own address, and no inbound headers at all, ctx.ip and ctx.peer null (withAnonymousRequest, @ultimat3/http). Whatever a stored page holds is answered to every later visitor, with public cache headers, so it varies on what its key carries: the URL and the locale. useRequestHeader() answers null; setRedirect() still works. A page that varies per visitor is ssr.

No declared origin: an isr entry is keyed by the request's origin too. canonical and og:url are absolute against the declared origin (APP_URL, SITE_ORIGIN, site.origin) or, with none, the request's own, so without this a first request with a foreign Host wrote that host into a page later visitors were answered. The origin rides the reserved __x_origin parameter. With a declared origin (http or https) there is no such parameter: one entry, and load is handed url at the declared origin. Without one, a process stores pages for 16 request origins (ISR_MAX_REQUEST_ORIGINS); any other Host is answered uncached, private, no-store, and evicts nothing (isr.origin.over_cap, logged once: declare APP_URL).

An isr entry is keyed by the negotiated locale. The store key is isrKey(url, locale, origin?) — the path in its one spelling (below), the reserved __x_locale parameter, then the query the route declared (revalidate.query, below) with its params sorted. Without it, an app shipping two locales served visitor 2 the document rendered for visitor 1, for the whole TTL, and told the CDN to do the same. The time zone is deliberately not a dimension — a locale set is declared and bounded, a zone list is not — so a date on an isr page belongs in a zone the page itself names, or the page belongs in ssr.

An isr page is one entry however its URL is spelled. The router answers /blog//abc/, /blog/%61bc and /blog/abc with the same route and the same slug, so the store keys on the address it matched, never on the pathname as written: empty segments and the trailing slash dropped, each segment decoded and re-encoded one way (@ultimat3/http's canonicalPath, built from the functions the router itself segments and decodes with). As of 2026-10-11.

RequestStored underWhy
/blog/abc, /blog/abc/, /blog//abc, //blog/abc, /blog/%61bc, /blog/./abc/blog/abcone route, one slug
/blog/caf%c3%a9, /blog/caf%C3%A9, /blog/café/blog/caf%C3%A9hex case and raw non-ASCII are spellings
/blog/a%2fb, /blog/a%2Fb/blog/a%2Fban encoded slash is part of the segment (slug is a/b), never a separator: /blog/a/b is another address, and a 404 for /blog/[slug]
/en/blog/abc, /en//blog/%61bc//en/blog/abcthe locale prefix is kept; a locale's home is /en/
/shop/blog/abc under basePath: '/shop'/blog/abcthe route table has never heard of the mount
  • load, meta and the page are handed that same address as url (base path included), so the document's canonical is /blog/abc whichever spelling arrived first. A static page is rendered at its address too, as the export writes it; ssr and stream keep the URL as the visitor wrote it.
  • Nothing is redirected, and no status changes: a spelling the router answered with a 200 is still a 200, with the canonical page's cache-control, purge keys and canonical link. Redirect at the ingress if you want one URL in a browser's address bar.
  • A key no route owns is never stored. An entry takes its ttl and its tags from its route, so stored without one it was fresh for ever and on no tag edge. isrController().serve() renders it for the request that asked, private, no-store, and logs isr.key.unowned once. The same holds for a request the router matched as one route while its address reads back as another (//en/x is the /[lang]/x route; /en/x is the /x route in English): answered, never stored.
  • Releases through 27.8.0 keyed on the raw pathname (Known Gaps).

A locale-prefixed isr page is its route's page. The key keeps the prefix — /en/blog/a?__x_locale=en is the document rendered for that URL — and the route it belongs to is found on the path with the routed prefix removed (unlocalizedPath), as the router matches it. So /en/blog/a takes /blog/:slug's revalidate.ttl (s-maxage included), joins its revalidate.tags, and is regenerated in its own locale: a tag bust or the TTL expires every locale's copy of a page, stored 404s included. A page whose truth changes with the clock (publishedAt <= now) is written by nothing when the instant passes, so no tag is busted — it needs a ttl, and the ttl alone is enough in every locale. Releases through 27.6.1 matched no route for a prefixed page (Known Gaps): tag-only s-maxage=60, no tag edge, fresh for the life of the process.

An isr regeneration holds its page for at most 30 s (DEFAULT_ISR_REGENERATE_DEADLINE_MS; isrController({ regenerateDeadlineMs }), a whole number ≥ 1) As of 2026-10-02. A render that never settles used to pin its page for the life of the process: a missed page hung every later request, a stale one was served stale forever. Past the deadline the next request renders again; the hung render is not cancelled, and its late result is dropped if a newer one has started. A stored path takes its TTL and tags from the most specific route that matches it — segment by segment, static beats :param beats *catch-all, as the request router resolves it — so /docs/7 is /docs/:id's, never /docs/*path's. A zero-length ttl ('0s', 0) is no trigger at all: X_ROUTE_MODE_INVALID.

revalidate — what an invalidation does, how old a copy may be, what the page varies on

As of 2026-10-10. Three optional keys beside tags and ttl, each refused at defineRoute when the controller could not act on it (X_ROUTE_MODE_INVALID).

import { tag } from '@ultimat3/cache';
import { defineRoute } from '@ultimat3/render';

declare const posts: { page(n: number): Promise<{ readonly titles: readonly string[] }> };

export const config = defineRoute({
  render: 'isr',
  revalidate: {
    tags: [tag('post')],
    ttl: '10m',
    onInvalidate: 'purge', // 'stale' (default) | 'purge'
    maxStale: '1h',        // past ttl + 1h a copy is not an answer; 0 (purge's default) is never
    query: ['page'],       // the only query parameters in the key; [] for none
  },
  offline: 'runtime',
  load: ({ url }) => posts.page(Number(new URL(url).searchParams.get('page') ?? '1')),
  meta: ({ url }) => ({ title: 'Blog', description: 'Every post, newest first.', canonical: url }),
});
KeyDefaultWhat it decides
onInvalidate: 'stale'yesa tag bust marks the stored page stale; the next request is answered that copy, however old, while a fresh one renders behind it (x-ultimate-isr: stale). Right for a price list
onInvalidate: 'purge'—a tag bust deletes every stored page of the route carrying the tag — each locale, each keyed query, stored 404s included — and evicts any render of them in flight, so the next request renders and waits. For content that must come down (a withdrawn article, a corrected legal text). Needs tags and ttl (X_ROUTE_MODE_INVALID without either). The response carries no stale-while-revalidate, and its s-maxage is what is left of the ttl
maxStaleunbounded; 0 under 'purge'how long past its ttl a stored page may still be answered stale: a duration, or 0 for never. Older, it is dropped and the request waits for a render. Also the stale-while-revalidate a shared cache is told (a day when omitted, absent at 0). Needs ttl
query[], nonethe query parameters the page varies on. Only these are in the stored page's key and in the url / query that load, meta and the page receive; one value each (the first), sorted

An isr route keys on no query unless it says so. As of 2026-11-02. /blog?x=1, /blog?x=2 and /blog?utm_source=mail are one render and one stored page, so no visitor can spend the origin's renders or push real pages out of the bounded store (it holds 1,000), and utm_* / fbclid / gclid do not split a page. An isr document is shared: what it varies on is the route's to declare.

The default store is bounded twice, and keeps what is read. As of 2026-11-09. memoryIsrStore({ maxEntries, maxBytes }): 1,000 pages (DEFAULT_ISR_MAX_ENTRIES) and 256 MiB of html.length (DEFAULT_ISR_MAX_BYTES), whichever is reached first. Least recently used goes first — a hit moves a page to the back, so a home page read on every request outlives cold pages generated after it; a stale mark keeps its place. A page larger than the whole byte cap is not stored and is rendered per request.

Declare every parameter the page reads. A page that reads ?currency= and does not list it is ONE stored document, the first visitor's, answered to every other currency. Two nets, both best-effort: x verify reads each isr route module for query.<name>, query['<name>'] and searchParams.get('<name>') and warns for a name the declaration omits (a read made through a helper in another file is not seen); and outside production, the query a page component receives warns once (isr.query.unkeyed-read) when the page reads a parameter the route did not declare. load and meta read the narrowed url, where an undeclared parameter is simply absent.

Nothing is redirected. /blog?utm_source=mail is answered with the /blog page: load and meta are given the narrowed URL, so canonical: url is /blog and a parameter outside the key cannot change the document stored under it. A declared parameter's values are still the visitor's — ?page=999999 is a key — so load answers withStatus(404, …) for one that names nothing, and the bounded store evicts the rest.

How fast a purge takes a page down

"The bust" below is the moment invalidateTags reaches its ISR step — after the read tiers (redis, lru, request-memo) cleared, so the re-render reads fresh rows. An action's cache.invalidates runs after its commit and is not awaited by the action's response, so the response can precede the bust by those tiers' latency.

DeploymentA request after the bust getsBound
one replicaa fresh render — never the stored page, never a render that began before the bust (its flight is evicted; a request that had joined it renders again)the next request
several replicas, per-replica stores, bus healthythe same, on each replica once it has received the broadcastbroadcast delivery, typically milliseconds; inside it a peer still answers its copy as a hit
several replicas, per-replica stores, broadcast losta replica that never heard the bust answers its copy as a fresh hit until that copy's ttl, then renders (nothing stale past it: maxStale is 0 under 'purge')the route's ttl, at the origin and at an edge in front (s-maxage is the ttl's remainder)
several replicas over one runtime.isrStorea fresh render on every replica: the page is deleted from the one store, and a replica that began a render before the bust cannot write it back (IsrStore.tagFence)the next request, bus or no bus

Order inside one bust: read tiers (farthest first) → this origin's ISR pages → the broadcast to peers → the cdn tier last. Each replica that receives the broadcast purges the edge again after its own delete, so a peer that answered the old page inside the broadcast window is cleared from the edge too. A render that read its rows before the bust is answered to its own request private, no-store, with no purge key, and is never stored.

Finish a rollout before relying on 'purge'. A replica still on a release before this one reads onInvalidate as nothing: it marks the page stale and answers it once more, with stale-while-revalidate=86400.

Past maxStale the copy is dropped before the re-render, so a render that fails there answers its 5xx rather than the old page — that is what the bound means. Ages are measured by each replica's own clock against the entry's generatedAt; over a shared store, skew between replicas moves ttl and maxStale by that skew.

A shared store must be able to refuse a write. runtime.isrStore under 'purge' needs IsrStore.tagFence — sample(entities) before a render reads its rows, bump(entities) on a bust, and an atomic setIfCurrent(entry, entities, sampled) (a transaction, a script, a conditional put) that refuses a write whose reading moved. The generation is per tag entity (post for post:1), so an unrelated row's bust costs one extra render and never a stale page. memoryIsrStore implements it; a store without it is refused at boot when any route purges (X_ROUTE_MODE_INVALID, naming the route). A bust also asks the store which keys it holds under the tags, by the route table, as well as the invalidation graph — the graph only knows the pages this process rendered. It matches on the route's tags: a post:1 bust reaches every stored page of a route tagged post.

One store for every replica: storageIsrStore. As of 2026-11-09. memoryIsrStore is one cache per process — three web replicas render a page three times and hold three copies. runtime.isrStore = storageIsrStore({ disk: disk('isr') }) keeps pages (isr/page/…) and tag-fence tokens (isr/fence/…) as objects on a @ultimat3/storage disk every replica reads. A bust writes a fresh token per tag entity; a write checks the reading, writes, then checks again and deletes its own page if a bust landed in between — a disk has no compare-and-put, and the bust lists the store only after its token is written, so no page outlives a bust its render predates. Bounded across replicas by maxEntries and maxBytes (the defaults above), swept every sweepEvery writes (32) per process, oldest write first — a read never writes, so it is not LRU. Every store call is awaited, so a custom store may answer with a promise (IsrAnswer<T>).

What isr never stores. What it does store was rendered as nobody (above); a per-visitor answer has no place in one, so that page is ssr.

RenderStoredThe visitor gets
a 5xx (withStatus(503, …) — "the read failed")never; never over a good copythe last good page, stale, while one exists (isr.render.unstored is logged); otherwise the 5xx itself, private, no-store. For one second (DEFAULT_ISR_FAILURE_COOLDOWN_MS; isrController({ failureCooldownMs })) requests for that page are answered the same failure, or the same stale page, instead of each rendering again
noStore(data) from loadnever; a stored copy of the page is droppedthat render, private, no-store
a load or page that throwsneverthe last good page, stale (isr.regenerate.failed); with none, the error page
a render invalidated while it ranneverthe request that started it: that render, private, no-store; any other request: a render of its own
a redirect (setRedirect())neverthe redirect
a 2xx or 4xxyes — a 404 is a stored pageit, for the ttl. A 4xx is never offered to a shared cache (no-store, no purge key): the pipeline's rule for every refusal

noStore is @ultimat3/render's, a mark on the data like withStatus and composable with it (noStore(withStatus(404, data))). isr only — an ssr page's cache is its route's cache key.

A page with an island is admitted to its own CSP by hash. The hydration runtime is an inline <script type="module">, and script-src admits it as one of the seven bodies HYDRATE_RUNTIME_BODIES enumerates, hashed at boot. Hashes and not a nonce because a render: 'static' page is a file on disk. The theme boot script is the second body the boot hashes, from theme.defaultMode (As of 20.2.0; an app writes neither the tag nor the hash). The speculation rules are the third (As of 2026-09, Client navigation). An app that adds its own inline script beyond those has to admit it the same way, at module scope in a file under apps/*/: configureHttp({ security: { csp: { extend: { 'script-src': [cspHashSource(body)] } } } }). The boot's own hashes are merged per directive, never replaced, so admitting a CDN source does not evict the hydration runtime's hash and lock every island out of the page.

One declaration of the vocabulary, at tier 0

As of 2026-08 the three closed sets a route is declared in — RENDER_MODES, OFFLINE_STRATEGIES, HYDRATE_STRATEGIES — are declared once, in @ultimat3/core, with each union derived from its array so the pair cannot disagree.

Import it fromWhen
@ultimat3/coreanywhere. It is tier 0, so no package is below it
@ultimat3/renderyou are already importing defineRoute — it re-exports all three, types and arrays
@ultimat3/http · @ultimat3/seo · @ultimat3/manifest · @ultimat3/pwathat package's own signatures take one, so it re-exports what it takes

Re-export, never restate. Six packages each kept a hand-written copy until 2026-08 — 14 declarations in all — because imports only go down tiers and copying was the move available. 'spa' was then deleted from one of them and five went on admitting it under a green project-wide typecheck; @ultimat3/pwa's copy mapped it to cache-first, the one strategy that gives an app/ route a shared cache entry. bun run scripts/render-modes.ts --json refuses a second declaration, matching on the literal set rather than the name — the copy that did the damage was called PwaRenderMode.

Why stream is the app default

An authed page needs fresh data and fast first paint. ssr gives freshness and a slow TTFB — the whole page waits for the slowest query. stream gives both halves: shell now, data as it resolves. A page whose body belongs in the browser declares an island({ src }), budgeted in real bytes; there is no client-side-only mode and no client router.

x g route, x g resource and x new's dashboard write render: 'stream' on app/. A stream page with no hole registers and pays nothing: it is one flush.

import { Hole } from '@ultimat3/render';
import type { JSX } from 'solid-js';

// Yours: the components and the two reads.
type View = (props: { children?: JSX.Element; rows?: readonly unknown[] }) => JSX.Element;
declare const Layout: View, Header: View, Stats: View, Feed: View;
declare const StatsSkeleton: View, StatsUnavailable: View, FeedSkeleton: View;
declare const statsQuery: (input: object) => Promise<readonly unknown[]>;
declare const feedQuery: (input: object) => Promise<readonly unknown[]>;

// A thunk: it starts after the first flush, in the request's context. `useContext()`, `useT()` and
// the actor all work in it. A promise written straight into the JSX has started already.
const readStats = async () => <Stats rows={await statsQuery({})} />;
const readFeed = async () => <Feed rows={await feedQuery({})} />;

export function Page() {
  return (
    <Layout>
      <Header />                                {/* in the first flush */}
      <Hole fallback={<StatsSkeleton />} error={<StatsUnavailable />}>
        {readStats}                             {/* streamed when the query resolves */}
      </Hole>
      <Hole fallback={<FeedSkeleton />}>
        {readFeed}                              {/* independent hole, independent flush */}
      </Hole>
    </Layout>
  );
}

Solid's fine-grained reactivity is what makes this cheap:

PropertyConsequence
No VDOMstreamed HTML is patched into place; nothing re-renders to reconcile
Signals, not component re-executiona resolved hole updates the exact DOM nodes bound to it
No hydration pass over the shellthe static shell costs 0 hydration work — a <Hole> hydrates only its own island, only when its hydrate timing fires
Compile-time templatesthe shell's JS is markup-shaped, not a component tree to replay

In a VDOM framework a streaming shell still pays for hydrating the whole tree, which is why streaming there buys TTFB but not TBT. Here it buys both.

What a hole does

FactRule
fallbackmarkup in the first flush. Rendered at shell time; it is not weighed by budgets, so keep islands out of it
errorshown when the hole's children throw or miss the 15 s deadline (DEFAULT_HOLE_TIMEOUT_MS). Rendered at shell time. Omitted: an empty hidden element. Either way the document closes, render.stream.hole_rejected or render.stream.hole_deadline is logged and the error monitor hears it
Other modes, a test, budgetsno second flush exists, so <Hole> is its children, awaited inline, and a throw is the render's throw. renderRoute shows the filled page, and a hole's island is weighed
Nestedthe inner hole renders inline inside the outer one: the set of holes is fixed at the first flush
Status, redirects, cookiesdecided before the first flush. withStatus, setRedirect and setCookie stay load's: inside a hole the last two are X_STREAM_ALREADY_FLUSHED, shown as the hole's error markup and reported. A throw in a hole is its error markup
CancellationuseContext().signal inside a hole is the hole's own: aborted at its deadline and when the client leaves. Pass it on: fetch(url, { signal: useContext().signal })
A long waitan empty comment every second while a hole is open keeps the socket's idle timer (requestTimeoutMs + 2 s) from cutting the stream
Async componentX_ROUTE_ASYNC_PAGE applies to Page, never to what renders under a hole
A hole's islandhydrates from its chunk: the chunk carries the hydration runtime after its reveal, and the realtime boot if this is the first island that needs it
A shell island while a hole is openlive: the runtime is <script type="module" async>, so it does not wait for parsing to end. With a realtime boot in the document it stays deferred (Known gaps)
Client routera soft visit reads the whole answer before it swaps (Known gaps); every reveal still runs
CSPevery chunk's inline script is a body inlineScriptSources() already hashes

Solid's own boundary component is not this: it throws outside a Solid renderer, and the server JSX factory is inert by design.

Independent holes each resolve their own queries; the tier-1 request memo means three holes reading the same query hit Postgres once.

hydrate timings

ValueWakes whenUse
idleafter first paint, on requestIdleCallback. A click made before the island mounts is captured and replayed on the same element, found by its path, as interaction does (21.0.0; the click was lost before). A keyboard or scripted click (detail: 0) is not hit-tested at (0, 0)default in app/ — above-the-fold interactivity
visiblethe island intersects the viewportbelow-the-fold lists, comment threads, charts
interactionfirst pointer/focus/key event on the islandmenus, modals, popovers, dropdowns
nevernot at all — server HTML is finalstatic islands, rendered markdown, badges

Hydration is per-island, never per-page. A blown budget.js is a build failure, so islands cannot quietly accumulate.

One kind of island is hidden until it mounts, whatever the strategy (As of 2026-10): on a signed-in page with a sync node, an island that reads realtime's record store or outbox is held (data-x-hold, visibility:hidden, its box kept), booted at once, and revealed when it mounts or after 3 s. An island that only follows a channel's events, a roster or the connection is not, and with scripting off none is. The rule and the list of names: Realtime.

Budgets

Per route, and one key: budget.js, in bytes. budget.lcp is deleted — nothing in the build observes a paint, so it was declared and never weighed.

budget.js counts raw minified bytes on disk — uncompressed, As of 2026-09-05: what the browser parses and executes, never the gzip or brotli size it transfers. A 350 kB library is 350 kB against the budget however well it compresses, because the number bounds execution cost, not bandwidth. measureDocumentJs in packages/cli/src/budgets.ts is the one reader, and the X_BUDGET_EXCEEDED line says (minified, uncompressed) so the unit is never inferred.

CheckSource of truth
Per-route JS bytesbudget.js on the route; measured from the real bundle graph
Lighthouse SEO + a11y scoresScore thresholds are not implemented or enforced (As of 2026-10)
Precache sizetotal sw.js precache set, see PWA and offline
Regressionbudgets ratchet — the recorded baseline can tighten, never loosen silently

A blown budget is a build failure, not a warning:

x verify
  ✓ typecheck            ✓ import boundaries      ✓ migration drift
  ✓ lint                 ✓ tests (6 types)        ✓ contract diff
  ✗ budgets
      site/pricing   js 61kb > 40kb   (chart.js via shared/ui/button.tsx)

Failures name the cause — the transitive import that added the bytes — because "bundle too big" without a chain is not an instruction. x verify --json emits the same content machine-readably.

SEO enforcement

ConcernEnforcement
meta.title missing on an indexable routebuild error X_SEO_META_MISSING, cause naming title and the file
meta.description missing on an indexable routethe same code, X_SEO_META_MISSING, naming description — one code for both fields, with fix: add description to meta in <file>
Title over 60 chars, description over 160build error X_SEO_META_TOO_LONG, with the measured length. A too-short description is not checked and no minimum is exported As of 2026-08: a 10-character description passes the gate. DESCRIPTION_MIN_LENGTH used to be exported and read by no validator, which is the shape a length bound must not ship in — validateMeta enforces every bound @ultimat3/seo exports, pinned by a test
Duplicate title/description across routesbuild error — duplicate meta is a ranking bug, not a style issue
og.image missing on a shareable routebuild error; the generated fallback OG image must be opted into explicitly
Broken internal linkbuild error, resolved against the route table
Missing alt on an <Image>build error
Canonical URLemitted for every route from the route table; never hand-written
robots / noindexa route-level field, so "we shipped staging to Google" is impossible without editing the route

A convention that isn't a build error doesn't exist. SEO is the archetype — every SEO regression in history was a documented convention someone forgot.

Typed JSON-LD

meta: ({ post }) => ({ /* ... */ ld: ld.Article(post) })

Helpers, typed against schema.org shapes:

HelperFor
ld.Articleposts, news, docs pages
ld.Productcatalog and detail pages with price + availability
ld.Organizationthe site's own identity, emitted once
ld.BreadcrumbListderived from the route's path segments
ld.FAQPageQ&A blocks
ld.SoftwareApplicationapp landing pages
ld.Eventdated, located events
ld.Recipeingredient + step content

A missing required property is a type error, not a Rich Results Test failure discovered next week. Output is a single <script type="application/ld+json">, deduped per page.

Generated from the route table

ArtifactDerived fromNotes
sitemap.xmlall indexable routes + prerender() resultsone <url> per page per locale with xhtml:link alternates; lastmod and the locales a page is listed in from its prerender() entry ({ params, lastmod: row.updatedAt, locales }); an index of named sections by seo.sitemap.sections, else auto-split at 50k URLs — SEO
robots.txtseo.robots.disallow and seo.robots.groups in app.config.tssitemap reference included; anything but ULTIMATE_ENV=production is Disallow: /
rss.xml / atom.xml / feed.jsonroutes tagged as feed itemsone declaration, three formats
llms.txtsite/ route titles + descriptionsmachine-readable site summary for agents: GET /llms.txt and the static export, seo.llms: false to opt out (SEO)
404 / 500required routesmissing one is a build error

Nothing here is a plugin. Deleting a route removes it from the sitemap in the same build.

i18n routing

FeatureBehavior
Locale routing/, /en/, /de/ from the locales defineCatalogs() declared — no per-route wiring. The prefix is stripped before matching; /<default>/x 301s to /x; an unrouted prefix is a plain 404
site/ localethe URL alone: unprefixed is always the default locale, never negotiated; no Vary: accept-language
hreflangfull reciprocal set emitted per route, BCP 47 region form, including x-default; absolute against the public origin
Per-locale static outputeach site/ page is prerendered once per locale (index.html, en/index.html); no client-side locale swap on site/
Missing keyrenders ⟦key⟧ in dev, fails x verify in CI
Numbers / dates / moneyIntl.* with an explicit IANA timeZone and ISO currency; never a hand-rolled format
Localized metadatameta receives locale, localizedPath(locale) and alternates

See I18n, Timezones and dates, Money.

Image pipeline

<Image src={post.cover} alt={post.title} sizes="(max-width: 700px) 100vw, 700px" priority />

One <Image> shape, one capability contract, stated once — in docs/idea/07-rendering-seo.md → Image pipeline: variants, srcset, inlined dimensions, the blur placeholder, the @ultimat3/core runtime, and the ImageTransformDriver seam AVIF/WebP variants come from. Restating it here would let the two copies drift.

A route that answers a status

As of 2026-09-07. A loader hands its data through withStatus(status, data) and the page renders as it always did — the route's own component, inside the app's own shell — with that status on the response.

import { defineRoute, withStatus } from '@ultimat3/render';

export const config = defineRoute({
  render: 'ssr',
  offline: 'runtime',
  load: async ({ params }) => {
    const host = await hostList({ id: params.host });
    return host === undefined
      ? withStatus(404, { kind: 'missing' as const, hostId: params.host })
      : { kind: 'page' as const, host };
  },
  meta: ({ data, t }) => ({ title: data.kind === 'missing' ? t('notFound') : data.host.label }),
});
what it doesrecords the status against that object; hands the same object back, so load's return type, meta's data and the page's props.data are untouched
which statusesany 2xx, 4xx or 5xx. A 3xx is X_ROUTE_STATUS_INVALID — a redirect is a Location and no body, which is setRedirect() from load (below). Out of 200–599, or not whole, is refused where it is written, never as a RangeError at new Response
SEOa 4xx or 5xx is <meta name="robots" content="noindex"> by construction — the descriptor's meta applies it after the route's own meta ran, so a page that does not exist is never indexed however meta was written. follow and the rest stay the author's; a 200 hands meta's object back untouched
every modessr, stream, static-served and isr all answer it; an isr entry stores a 2xx or 4xx status beside the HTML and serves it on every hit, stale copies included; a 5xx is answered and never stored
the static exporta file has no status. x build --target static writes the document whatever the loader said, and the build's measurer — which renders an app/ route with params: {} to weigh it — never fails on a loader answering 404: the status is a fact about the data, not a throw
an untouched appreads 200 everywhere it did, byte for byte — nothing asks unless a loader answered

A redirect is setRedirect(), from load. As of 2026-09-24. It is the same call an action's handler uses, from @ultimat3/http. On the page path, the request answers with that Location and status (default 303, and 302/307/308 when named) and no document is rendered:

import { setRedirect } from '@ultimat3/http';

load: async ({ params }) => {
  await recordOpen(params.token);            // a tracking pixel, a download link
  setRedirect('/pixel.gif', 302);
  return { params };                          // never rendered
},

With no declared cache, the redirect is private, no-store: the loader decided it for this one request, and a cached copy would answer later requests without running the loader. A route with a declared cache gets that instead. withStatus still refuses a 3xx, because a rendered document has no Location.

A refused page is withStatus(403, data). The same call answers a denial rendered in the shell — an @ultimat3/admin mount whose guardedPage() returned AdminPageDenied asks the same decideAll in its load and returns withStatus(403, data) (the admin README shows the load).

Not a throw, deliberately. Throwing is the other 404 and it is still there: a route the table does not have, or a loader that throws X_NOT_FOUND, gets the framework's error page — outside the app's shell, no sidebar, no session list. Measured in one adopter's app on 2026-09-07, /fleet/nope rendered the right page inside the shell and answered 200, because the only way to the status was the way that lost the shell. The two answers stay distinct: the table lacks the route → throw → error page; the route exists and says "not found" → withStatus → its own page.

Error pages

A browser that hits a failure in a production process gets the framework's error page — the status, the code, the request id, and nothing off the throwable. Until As of 2026-08-23 a production browser got problem+json with the internal cause and the author-facing fix: in it.

statusx devproduction
4xx, app file presentthe app's filethe app's file
4xx, no app filethe overlay — cause, fix, stackthe framework's page
5xxthe overlay, alwaysthe app's file, else the framework's page

A 4xx in dev is the page a visitor will see, so it is the one the author sees; a 5xx is a defect, and the overlay is how dev debugs one. An agent (accept: application/json) gets problem+json in both.

a page that exists and answers 404not this page at all — withStatus(404, data) in the loader renders the route's own component with the status (above); the error page is for a throw
override, one per statusapps/web/site/errors/<status>.html — served byte for byte, exact status match only; read per request under x dev (drop a file into the running server), once per status in a container
static export404.html is written by x build --target static, the app's file if present
copythe errors.* keys of the i18n catalog — an app translates or overrides by declaring them
footerlinks to the Ultimate repository and developerz.ai; an override replaces the whole page

Rules

  • render and offline are declared on the route or defaulted by surface. Never inferred at runtime.
  • revalidate.tags are typed handles from the tag graph — an unknown tag is a compile error (Caching and invalidation).
  • Routes never touch the DB directly. Data comes from a query.
  • A route holds no business logic. That is a service, behind an action.
  • prerender() must be deterministic and bounded — it is a build input.
  • Never hand-write a canonical URL, a sitemap entry, or sw.js.
  • A URL off data inside an island goes through @ultimat3/ui's Link, or safeUrl(url, 'href') from @ultimat3/core at the call site. The island's server shell is screened like every page (a javascript: href is not emitted), but once it mounts Solid's compiled setter writes <a href={row.url}> as given; only the app's CSP stands between a looser policy and a live javascript: link. As of 2026-11-09 there is no build check for it.

Edit this page on GitHubThis page as markdown