@ultimat3/render 19.3.1 → 19.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -37,6 +37,7 @@ for the mirror-image reason, keeping `render → pwa` refused. Never `cli` (upwa
37
37
  | `load` | optional, and the ONE server-side data seam. Resolved once per render by `routeDataFor()` and handed to **both** `meta` and the page component. Two resolutions is a `<title>` describing content the body does not contain. Absent `load`, the context IS the data (`{ params, url }`), which is what `meta` received before the key existed — so no consumer branches on whether a route declared one. |
38
38
  | `load` is required when the context cannot supply the data | `LoadRequirement<TData>` in `defineRoute`'s parameter — `unknown` when `RouteContext` satisfies `TData`, a required `load` when it does not. That is what makes the no-`load` fallback true rather than asserted: it was `ctx as unknown as TData`, so a `meta` reading `data.post` off a route that loads nothing type-checked and rendered `undefined` in a `<title>`. `RouteContext` is a type ALIAS for the same reason — only an alias carries the implicit index signature that makes it a `RouteData`; as an `interface` the compiler cannot see it and the cast comes back. |
39
39
  | A loader's own error | rethrown only when `isUltimateError` says so — core's brand, never a `code` property. Every `ENOENT` is an `Error` with a string `code`, and the duck-type that preceded this let all of them out of `routeDataFor` unwrapped: no `X_ROUTE_LOAD_FAILED`, no fix line, no route named. A tier-0 error (`@ultimat3/schema` cannot import core) is branded, not a subclass — never narrow this to `instanceof UltimateError`. |
40
+ | A loader's own STATUS | `withStatus(status, data)` in `route-status.ts` — the ONE way a page answers 404 (or 410, or 503) while still rendering its own component inside the app's shell. Measured in ai-maxxing 2026-09-07: the only route to a status was a throw, which is the framework's error page OUTSIDE the shell, so `/fleet/nope` rendered the right page and answered 200. The status rides on the data by IDENTITY, in a `WeakMap` — the same object comes back, so `load`'s type, `routeDataFor`'s signature and every consumer that never asks are untouched, and a frozen or class-instance result is never written into. Never a `RouteContext` method: every builder of a context (`x dev`, the prerenderer, the SEO scan, both scaffold templates) would have to supply it, and an optional method is a second way. `routeStatusOf(data)` is the reader, TOTAL, 200 when nothing asked. A 3xx is `X_ROUTE_STATUS_INVALID` — a redirect is a `Location` and no body — and the range is `finiteStatus`'s. A 4xx/5xx is `robots.index = false` BY CONSTRUCTION in `defineRoute`'s `meta` wrapper, the one function every `<head>` renderer calls; a 200 hands the author's `meta` object back by reference. The response status itself is minted by `@ultimat3/cli`'s `dev-render.ts` (`resultFor`), which reads `routeStatusOf(data)` once and hands it to the mode — this package owns the seam, never the `Response`. |
40
41
  | Type claims | `type-pins.tsx`, never a `.test.ts` — `tsconfig.json` excludes tests, so `tsc` never reads one. `.tsx` since 1.2.0: the island-as-JSX claim is only decidable by writing the JSX an author writes, checked against the same `solid-js` `JSX.Element` a page is. |
41
42
  | Descriptor `meta` / `load` | always `(x) => Promise<…>`. Authors may declare either sync; consumers never branch. |
42
43
  | Descriptor `budget` | always an object, `{}` when undeclared. Its *fields* stay optional — `budget.js === undefined` is the site/ hydration failure. |
@@ -80,7 +81,7 @@ for the mirror-image reason, keeping `render → pwa` refused. Never `cli` (upwa
80
81
  | A bust that lands MID-render | fenced with `@ultimat3/cache`'s `sampleFence({ key, tags })`, taken before `render()` and asked before `store.set` — the same mechanism `CacheStack`'s read-through fill uses, never a second one grown here. `regenerate` rendered and then wrote `{ stale: false }` unconditionally, so a `markStale` arriving in between was ERASED by HTML built from pre-write rows; for a tag-only route `isFresh` is then true forever and the process serves it for the rest of its life. `registerPath` runs BEFORE the render for the other half: `revalidateByTags` reads the graph, so a bust could not see a cold path whose first render was still in flight. |
81
82
  | Marking a page stale | `IsrStore.markStale(path)`, in place — never `set({ ...entry, stale: true })`. `set` means "this page was just generated" and the default store orders eviction by exactly that, so the read-modify-write made the STALEST page the newest: a tag bust protected the pages that most needed regenerating and evicted the freshest one instead. **Breaking**: `markStale` is a required member of `IsrStore`. |
82
83
  | ISR store bound | `memoryIsrStore()` caps at `DEFAULT_ISR_MAX_ENTRIES` (1,000), least recently generated first — a route table supports `:params` and `*`, so `/blog/:slug` retains one full HTML string per slug ever requested, 404-shaped ones included. |
83
- | A `RenderResult.status` | `finiteStatus(subject, status)` in `finite-status.ts`, at both sites that take one from a caller (`renderSsr`'s `options.status`, `streamResult`'s third argument) — 200–599, whole. It reaches `new Response(body, { status })`, which answers a bare `RangeError` for anything else: `NaN` arrives there as `The status provided (-9223372036854775808)`, no code, no fix, and the render fails two frames above the route that set it. The screen is NARROWER than the boundary on purpose — `new Response` also takes `101`, and no rendered document is a protocol switch. The name carries `finite` because `bun run finite-bounds` recognises a repair by the shape of the CALL; spelled `renderStatus` it read as no screen at all. |
84
+ | A `RenderResult.status` | `finiteStatus(subject, status)` in `finite-status.ts`, at every site that takes one from a caller (`renderSsr`'s `options.status`, `streamResult`'s third argument, `withStatus`'s first, and an `IsrRenderFn` answering `{ html, status }` at generation — `isRenderStatus`, the same range as a predicate, is the TOTAL read of a stored `IsrEntry.status` on the request path, because a custom store may JSON-round-trip one and a throw there is a 500 for the page's whole TTL) — 200–599, whole. It reaches `new Response(body, { status })`, which answers a bare `RangeError` for anything else: `NaN` arrives there as `The status provided (-9223372036854775808)`, no code, no fix, and the render fails two frames above the route that set it. The screen is NARROWER than the boundary on purpose — `new Response` also takes `101`, and no rendered document is a protocol switch. The name carries `finite` because `bun run finite-bounds` recognises a repair by the shape of the CALL; spelled `renderStatus` it read as no screen at all. |
84
85
  | `IsrEntry.ttlMs` off a store | normalised by `entryTtlMs`, TOTAL, never a throw — a ttl that is not a POSITIVE FINITE number of ms is the tag-only `null` `parseTtlMs` would have answered. `IsrStore` is a driver seam, and one backed by Redis round-trips the entry through JSON where a `ttlMs` nobody wrote reads back as `undefined`, so `entry.ttlMs === null` is false. Two failures, neither raising: `now - generatedAt < NaN` is false so the page is NEVER fresh and every request regenerates it, and the CDN is handed `s-maxage=NaN` — a directive a conforming cache IGNORES, dropping the page to heuristic caching. Read on the request path, so a refusal would turn a bad stored entry into a 500; the `isr.entry_ttl_invalid` warning is what keeps it from being silent. |
85
86
  | Route path from file | ONE reader of the surface segment: `locateSurface()` answers which surface AND where it starts. `registry.ts` sliced at `indexOf('app/')` instead, which matched inside `myapp/`, so every route under `apps/myapp/app/` served at `/app/…`. Never re-derive the offset from the surface NAME. |
86
87
  | An undecodable path segment | not a match, never a throw — `decodeSegment` in `registry.ts` is the one reader. `decodeURIComponent('%zz')` is a bare `URIError`, so a typo in a path segment was a 500 where `@ultimat3/http`'s router already answers "this branch does not match". A literal route matching the same text still wins. `router-client.ts` was the second reader and went with `createRouter`. |
@@ -102,6 +103,7 @@ for the mirror-image reason, keeping `render → pwa` refused. Never `cli` (upwa
102
103
  | CSS order | `stylesFor` sorts **globals before modules** (`isGlobalStylesheet`), never plain insertion order — the reset styles bare elements at the lowest specificity there is, so whichever page loaded first must not decide who wins a tie. `shared/` is carried by both graphs, like a package sheet: it is where an app's own global layer lives, and filtering it out is what made every deployed app render token-less. |
103
104
  | The global layer | this package may not import `@ultimat3/ui` (tier 4, the same tier — sideways, not upward: `ui` moved 5 → 4 in 2026-08 and `render → ui` stays forbidden because a same-tier edge has to be declared in `scripts/lib/tiers.ts`, and this one deliberately is not — the static bundle graph may not reach the design system, axiom 6), so the app's source graph carries it: one `shared/global.scss` that `@use`s `@ultimat3/ui/global.scss`, side-effect-imported by `shared/global.ts`. One file, because each stylesheet is its own Sass compilation — a token file `@use`d per module duplicates its `:root` block per module. `x verify` fails with `X_STYLES_GLOBAL_MISSING` when a surface's document defines none. |
104
105
  | Colours | tokens and `data-theme` only. No hex in `head.ts` or any emitted script. |
106
+ | An island's `mount` | may return `() => void`, its disposer — `return render(…)`. The runtime resolves `el.__x` with it and never calls it; `@ultimat3/testing`'s `mountIsland` does, on dispose. |
105
107
  | `<head>` binding | `head.ts` stays injection-only (testable with no catalog); `head-seo.ts` is the ONE binding of `HeadRenderers` to `@ultimat3/seo`. A caller writing its own converter is the drift this file prevents. |
106
108
 
107
109
  Cross-package: `@ultimat3/pwa` consumes route descriptors as **data**, never by import.
package/README.md CHANGED
@@ -55,6 +55,17 @@ already carries a better code and a better fix than any wrapper could. Membershi
55
55
  brand, not a `code` property: an `ENOENT` is an `Error` with a string `code` too, and it gets
56
56
  wrapped like any other loader failure.
57
57
 
58
+ A loader that wants the page to answer a **status** — a row the URL names and the table lacks —
59
+ returns its data through `withStatus(404, data)`. The same object comes back, so nothing about
60
+ `load`'s type, `meta`'s `data` or the page's props changes; every render mode reads the status off
61
+ it (`routeStatusOf`), and a 4xx or 5xx is `robots: noindex` by construction, applied by the
62
+ descriptor's `meta` after the route's own ran. Not a throw: a throw is the framework's error page,
63
+ outside the app's shell, and `As of 2026-09-07` that was the only way to a 404 — ai-maxxing's
64
+ `/fleet/nope` rendered the right page and answered 200. A 3xx is `X_ROUTE_STATUS_INVALID`; a
65
+ redirect is `@ultimat3/http`'s `redirect()`. The static export writes the document whatever the
66
+ loader said — a file has no status — and the build's measurer, which renders with `params: {}`,
67
+ never fails on a loader answering 404.
68
+
58
69
  ## `offline` and `meta` are required by the type
59
70
 
60
71
  Not by a lint rule, not by a doc — by `RouteDefinition`. Axiom 3 lives in the type system:
@@ -429,6 +440,7 @@ side effect. Anything that loads an app's source — `x dev`, `x build`, `server
429
440
  | Export | Owns |
430
441
  |---|---|
431
442
  | `defineRoute` | the `route` primitive |
443
+ | `withStatus`, `routeStatusOf` | the status a loader answers, carried on its data; 200 when nothing asked |
432
444
  | `island`, `createIslandCollector` | one interactive component on a static page |
433
445
  | `MODE_SPECS`, `assertModeShape`, `assertModeInvariants` | the mode invariant table |
434
446
  | `registerRoute`, `describeRoutes`, `routeFor`, `routePathFromFile` | the route table |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/render",
3
- "version": "19.3.1",
3
+ "version": "19.3.2",
4
4
  "description": "The route primitive and the five render modes: static, isr, ssr, stream, spa.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -36,10 +36,10 @@
36
36
  "test": "bun test"
37
37
  },
38
38
  "dependencies": {
39
- "@ultimat3/cache": "19.3.1",
40
- "@ultimat3/core": "19.3.1",
41
- "@ultimat3/i18n": "19.3.1",
42
- "@ultimat3/seo": "19.3.1",
39
+ "@ultimat3/cache": "19.3.2",
40
+ "@ultimat3/core": "19.3.2",
41
+ "@ultimat3/i18n": "19.3.2",
42
+ "@ultimat3/seo": "19.3.2",
43
43
  "sass": "1.102.0"
44
44
  }
45
45
  }
@@ -115,15 +115,47 @@ const TOKEN_FIX =
115
115
  "@ultimat3/ui/tokens exports functions and mixins — space(4), radius(md), role('surface-raised'), " +
116
116
  'text(sm) — and no $variables';
117
117
 
118
+ /**
119
+ * A leading byte-order mark or `@charset` rule: the encoding claim Sass writes at the head of any
120
+ * output holding a non-ASCII character. Anchored to the START — a `@charset` anywhere else is
121
+ * already invalid CSS and not this function's to repair.
122
+ */
123
+ const CHARSET_HEAD = /^\uFEFF?(?:@charset\s+"[^"]*"\s*;\s*)?/u;
124
+
125
+ /**
126
+ * Drop the encoding claim from the head of one compiled sheet. Sass emits it for the FILE it
127
+ * believes it is writing, and it is right about a file: a stylesheet that begins with U+FEFF is
128
+ * decoded as UTF-8 by every browser. It is wrong about a fragment. The registry concatenates
129
+ * modules verbatim, so every module after the first that holds a `content: '·'` began with a BOM
130
+ * glued to its first selector — `\uFEFF.dashboard_256ee8e0{display:grid}` — which the browser reads
131
+ * as an unparseable selector and drops with its whole rule. Measured on ai-maxxing's home
132
+ * stylesheet, 2026-09-06: seven modules, seven first rules gone, the dashboard's grid container
133
+ * painting `display: block` with only the UA rule in Chrome's matched styles. The bundle is served
134
+ * `text/css; charset=utf-8` by the route, so no sheet needs to claim its encoding at all.
135
+ *
136
+ * Applied at BOTH seams: on the compile, where `charset: false` already asks Sass not to write it,
137
+ * and again where the sheets are joined, so a future Sass that ignores the option — or a sheet
138
+ * that reached the registry by another road — still cannot put a BOM mid-file.
139
+ */
140
+ export function stripCharset(css: string): string {
141
+ return css.replace(CHARSET_HEAD, '');
142
+ }
143
+
118
144
  export function compileStylesheet(file: string, source: string): CompiledStylesheet {
119
145
  let css: string;
120
146
  try {
121
- css = sass.compileString(source, {
122
- url: pathToFileURL(file),
123
- loadPaths: [dirname(file)],
124
- importers: [packageImporter(dirname(file))],
125
- style: 'compressed',
126
- }).css;
147
+ css = stripCharset(
148
+ sass.compileString(source, {
149
+ url: pathToFileURL(file),
150
+ loadPaths: [dirname(file)],
151
+ importers: [packageImporter(dirname(file))],
152
+ style: 'compressed',
153
+ // No `@charset`, no BOM — see `stripCharset`. Dart Sass writes one for any compressed
154
+ // output holding a non-ASCII character, and re-emits an escaped `\\00b7` as the literal
155
+ // character, so escaping in the app cannot avoid it.
156
+ charset: false,
157
+ }).css,
158
+ );
127
159
  } catch (error) {
128
160
  // `renderThrowable`, never `.message`/`String()`: an importer, a plugin or a future Sass
129
161
  // release can throw a value whose own read raises, and this frame is what turns a failed
package/src/errors.ts CHANGED
@@ -14,6 +14,7 @@ export const RENDER_ERROR_CODES = [
14
14
  'X_ROUTE_FILE_INVALID',
15
15
  'X_ROUTE_LOAD_INVALID',
16
16
  'X_ROUTE_LOAD_FAILED',
17
+ 'X_ROUTE_STATUS_INVALID',
17
18
  'X_SURFACE_BOUNDARY',
18
19
  'X_BUDGET_EXCEEDED',
19
20
  'X_PRERENDER_FAILED',
@@ -37,6 +38,7 @@ export const RENDER_ERROR_TITLES: Readonly<Record<RenderErrorCode, string>> = {
37
38
  X_ROUTE_FILE_INVALID: 'a route file is not named for its surface',
38
39
  X_ROUTE_LOAD_INVALID: 'a route declared a load that is not a function',
39
40
  X_ROUTE_LOAD_FAILED: "a route's load threw while resolving its data",
41
+ X_ROUTE_STATUS_INVALID: 'a route answered a status a rendered page cannot carry',
40
42
  X_SURFACE_BOUNDARY: 'a surface imported across the hard boundary',
41
43
  X_BUDGET_EXCEEDED: 'a route blew its JS or LCP budget',
42
44
  X_PRERENDER_FAILED: 'a prerendered path threw during build',
@@ -254,3 +256,20 @@ export class RouteLoadFailedError extends UltimateError {
254
256
  });
255
257
  }
256
258
  }
259
+
260
+ /**
261
+ * A loader answered a status through `withStatus` that no rendered document can carry: a 3xx. A
262
+ * redirect is a `Location` and no body, and this seam only ever produces a body — so the answer
263
+ * is refused where it was written, with the redirect the author meant named as the fix. The
264
+ * out-of-range half (`NaN`, `199`, `600`) is `finiteStatus`'s, the same screen every mode uses.
265
+ */
266
+ export class RouteStatusInvalidError extends UltimateError {
267
+ static readonly code = 'X_ROUTE_STATUS_INVALID' as const;
268
+ constructor(cause: string, fix: string) {
269
+ super({
270
+ code: RouteStatusInvalidError.code,
271
+ cause,
272
+ fix,
273
+ });
274
+ }
275
+ }
@@ -14,6 +14,15 @@ import { assert, finiteCount } from '@ultimat3/core';
14
14
  const MIN_RENDER_STATUS = 200;
15
15
  const MAX_RENDER_STATUS = 599;
16
16
 
17
+ /**
18
+ * The one range, as a predicate, for the request-path reader that must stay TOTAL: an `IsrEntry`
19
+ * a custom store round-tripped through JSON is read on every hit, and a throw there turns one bad
20
+ * stored number into a 500 for the page's whole TTL. `finiteStatus` below is the throwing form
21
+ * and reads the same two bounds, so the two cannot disagree about what a rendered status is.
22
+ */
23
+ export const isRenderStatus = (status: number): boolean =>
24
+ Number.isSafeInteger(status) && status >= MIN_RENDER_STATUS && status <= MAX_RENDER_STATUS;
25
+
17
26
  /**
18
27
  * `NaN` is the value that gets here: `??` guards NULLISH, so a status read from a config, a JSON
19
28
  * body or `Number(process.env.X)` walks past its default intact — and the boundary then reports it
@@ -22,7 +31,7 @@ const MAX_RENDER_STATUS = 599;
22
31
  export function finiteStatus(subject: string, status: number): number {
23
32
  finiteCount(subject, 'status', status, 0);
24
33
  assert(
25
- status >= MIN_RENDER_STATUS && status <= MAX_RENDER_STATUS,
34
+ isRenderStatus(status),
26
35
  `${subject} status is ${String(status)}, which new Response() refuses with a RangeError instead of returning a document`,
27
36
  `pass a status between ${String(MIN_RENDER_STATUS)} and ${String(MAX_RENDER_STATUS)} to ${subject}, or omit it and take 200`,
28
37
  );
package/src/hydrate.ts CHANGED
@@ -117,6 +117,10 @@ export function requiredStrategies(
117
117
  // The rejection handler rethrows: swallowing it would resolve `el.__x`, and the interaction
118
118
  // runtime below would then flush its replay queue into an island that never mounted — the bug
119
119
  // `el.__x`-as-a-promise was introduced to fix, reintroduced one layer further out.
120
+ //
121
+ // `el.__x` resolves to whatever `mount()` returned. An island's `mount` may return `() => void`,
122
+ // its disposer (Solid's `render` answers one); the runtime keeps it there and
123
+ // `@ultimat3/testing`'s `mountIsland` calls it on dispose. Nothing here ever calls it.
120
124
  const RUNTIME_PRELUDE = `
121
125
  function boot(el){var e=el.getAttribute('data-x-entry');
122
126
  if(!e)return Promise.resolve();if(el.__x)return el.__x;
package/src/index.ts CHANGED
@@ -33,6 +33,7 @@ export {
33
33
  RouteMetaMissingError,
34
34
  RouteModeInvalidError,
35
35
  RouteOfflineMissingError,
36
+ RouteStatusInvalidError,
36
37
  SurfaceBoundaryError,
37
38
  } from './errors';
38
39
  export type {
@@ -129,6 +130,7 @@ export { DEFAULT_ISLAND_HYDRATE, defineRoute, isRouteConfig, tagKeys } from './r
129
130
  export type { RouteComponent } from './route-component';
130
131
  export { pageComponentOf } from './route-component';
131
132
  export { metaContextFor, routeDataFor } from './route-data';
133
+ export { DEFAULT_ROUTE_STATUS, isErrorStatus, routeStatusOf, withStatus } from './route-status';
132
134
  export type {
133
135
  BoundaryRule,
134
136
  BoundaryViolation,
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { renderThrowable } from '@ultimat3/core';
8
- import { compileStylesheet, isGlobalStylesheet } from './css-modules';
8
+ import { compileStylesheet, isGlobalStylesheet, stripCharset } from './css-modules';
9
9
  import { PrerenderFailedError } from './errors';
10
10
  import type { Surface } from './surfaces';
11
11
  import { surfaceOf } from './surfaces';
@@ -125,8 +125,10 @@ export function stylesFor(surface: Surface | null): string {
125
125
  const carried = [...stylesheets.values()].filter(
126
126
  (sheet) => sheet.surface === null || sheet.surface === 'shared' || sheet.surface === surface,
127
127
  );
128
+ // `stripCharset` on every sheet, not only the first: a `@charset` or a BOM is legal at byte 0 of
129
+ // a FILE and nowhere else, and this join is what turns seven files into one.
128
130
  return [...carried.filter((sheet) => sheet.global), ...carried.filter((sheet) => !sheet.global)]
129
- .map((sheet) => sheet.css)
131
+ .map((sheet) => stripCharset(sheet.css))
130
132
  .join('');
131
133
  }
132
134
 
@@ -169,6 +171,9 @@ let installed = false;
169
171
  * placement that covers `x dev`, `x build`, the production `server.ts` and `bun test` without each
170
172
  * of them remembering to. A plugin only affects modules loaded AFTER it, and every route module is.
171
173
  */
174
+ /** `/a/page.tsx?x-reload=3` → `/a/page.tsx`: the file on disk, which is what the hook reads. */
175
+ const withoutQuery = (path: string): string => path.replace(/\?[^/]*$/, '');
176
+
172
177
  export function installRenderLoader(): void {
173
178
  if (installed) return;
174
179
  installed = true;
@@ -176,8 +181,13 @@ export function installRenderLoader(): void {
176
181
  Bun.plugin({
177
182
  name: 'ultimate-render',
178
183
  setup(build): void {
179
- build.onLoad({ filter: /\.tsx$/ }, async ({ path }) => ({
180
- contents: transformTsx(await Bun.file(path).text()),
184
+ // The query is admitted and then stripped. `x dev` re-imports an edited route module as
185
+ // `<path>?x-reload=<hash>` — the only cache key Bun honours — and Bun hands this hook the
186
+ // specifier QUERY INCLUDED. Anchored on `.tsx$` the filter let that import fall through to
187
+ // Bun's own loader, which compiles JSX to `React.createElement`: every reloaded page then
188
+ // died on its first render with `__xh is not defined`.
189
+ build.onLoad({ filter: /\.tsx(?:\?[^/]*)?$/ }, async ({ path }) => ({
190
+ contents: transformTsx(await Bun.file(withoutQuery(path)).text()),
181
191
  loader: 'js',
182
192
  }));
183
193
 
package/src/render-isr.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  } from '@ultimat3/cache';
18
18
  import { finiteCount, logger, renderThrowable } from '@ultimat3/core';
19
19
  import { parseTtlMs } from './duration';
20
+ import { finiteStatus, isRenderStatus } from './finite-status';
20
21
  import type { RouteDescriptor } from './registry';
21
22
  import { describeRoutes } from './registry';
22
23
  import { contentHash, staticHeaders } from './render-static';
@@ -37,6 +38,12 @@ export interface IsrEntry {
37
38
  readonly ttlMs: number | null;
38
39
  /** Set by a tag invalidation; independent of the TTL clock. */
39
40
  readonly stale: boolean;
41
+ /**
42
+ * What the page answers, 200–599. Optional because an entry can come back from an app's own
43
+ * store, written before this field existed or JSON-round-tripped without it; absent reads as
44
+ * 200, the only status an entry ever had until `withStatus`.
45
+ */
46
+ readonly status?: number;
40
47
  }
41
48
 
42
49
  export interface IsrStore {
@@ -150,7 +157,22 @@ function routePathOf(key: string): string {
150
157
  return query === -1 ? key : key.slice(0, query);
151
158
  }
152
159
 
153
- export type IsrRenderFn = (path: string) => string | Promise<string>;
160
+ /**
161
+ * A render that also answers a status — what a loader's `withStatus(404, …)` becomes once the
162
+ * document is built. A bare string is the 200 every render before this one was: the union is
163
+ * additive, and a render function that never learned the object shape keeps compiling.
164
+ */
165
+ export interface IsrRendered {
166
+ readonly html: string;
167
+ readonly status: number;
168
+ }
169
+
170
+ export type IsrRenderFn = (path: string) => string | IsrRendered | Promise<string | IsrRendered>;
171
+
172
+ /** One shape for the generator, so nothing below branches on what the render handed back. */
173
+ function renderedOf(rendered: string | IsrRendered): IsrRendered {
174
+ return typeof rendered === 'string' ? { html: rendered, status: 200 } : rendered;
175
+ }
154
176
 
155
177
  export interface IsrServeResult {
156
178
  readonly state: IsrState;
@@ -266,7 +288,7 @@ export function createIsrController(options: IsrControllerOptions = {}): IsrCont
266
288
  key: path,
267
289
  tags: (descriptor?.revalidateTags ?? []).map(parseWireTag),
268
290
  });
269
- const html = await render(path);
291
+ const { html, status } = renderedOf(await render(path));
270
292
  const entry: IsrEntry = {
271
293
  path,
272
294
  html,
@@ -274,6 +296,9 @@ export function createIsrController(options: IsrControllerOptions = {}): IsrCont
274
296
  generatedAt: now(),
275
297
  ttlMs: parseTtlMs(descriptor?.revalidateTtl),
276
298
  stale: false,
299
+ // Screened at generation, the one place a status enters the store: a `NaN` written here
300
+ // would be served for the whole TTL as a `RangeError` on every hit.
301
+ status: finiteStatus('IsrRenderFn', status),
277
302
  };
278
303
  // Refused, never published stale-flagged: the next request re-renders from rows that now
279
304
  // include the write, where a stored-but-stale entry would serve this pre-write body once
@@ -432,6 +457,20 @@ function cacheControl(ttlMs: number | null): string {
432
457
  return `public, max-age=0, s-maxage=${sMaxAge}, stale-while-revalidate=86400`;
433
458
  }
434
459
 
460
+ /**
461
+ * `entryTtlMs`'s reason, one field over: a store may hand back an entry with no `status`, or one
462
+ * that JSON turned into something else, on the request path. Absent is 200 — the only value any
463
+ * entry carried before the field existed — and anything the range refuses is 200 with a warning,
464
+ * because a stored number must not 500 the page for its whole TTL.
465
+ */
466
+ function entryStatus(entry: IsrEntry): number {
467
+ const status = entry.status;
468
+ if (status === undefined) return 200;
469
+ if (isRenderStatus(status)) return status;
470
+ logger.warn('isr.entry_status_invalid', { path: entry.path, status: String(status) });
471
+ return 200;
472
+ }
473
+
435
474
  function toResult(entry: IsrEntry, buildId: string, servedStale = false): RenderResult {
436
475
  const headers: Record<string, string> = {
437
476
  ...staticHeaders(entry.hash, buildId),
@@ -443,5 +482,5 @@ function toResult(entry: IsrEntry, buildId: string, servedStale = false): Render
443
482
  vary: 'accept-language',
444
483
  };
445
484
  if (servedStale) headers['x-ultimate-isr'] = 'stale';
446
- return { status: 200, headers, body: entry.html };
485
+ return { status: entryStatus(entry), headers, body: entry.html };
447
486
  }
@@ -0,0 +1,72 @@
1
+ // The one way a loader answers a response STATUS while still rendering the route's own page.
2
+ //
3
+ // Measured in ai-maxxing, 2026-09-07: `/fleet/nope` — a host id the fleet does not have — rendered
4
+ // the app's own "Not found" page inside its shell, the right page, and answered **200**. The only
5
+ // route to a 404 was throwing, and a throw renders the framework's error page OUTSIDE the shell.
6
+ // So an app that did the right thing for its visitor could not do the right thing for a crawler,
7
+ // a CDN or a monitor, and one that did the right thing for those lost its shell.
8
+ //
9
+ // The status rides ON the data, by identity: `withStatus(404, data)` hands the same object back,
10
+ // so `load`'s return type is untouched, `routeDataFor` still hands ONE object to `meta` and the
11
+ // page, and every consumer that never asks reads 200. A `WeakMap` and not a symbol property — the
12
+ // data may be frozen, may be a class instance, and is the author's; nothing here writes into it.
13
+ // A `RouteContext` method (`ctx.notFound()`) was the alternative and was refused: every builder of
14
+ // a context — `x dev`, the prerenderer, the SEO scan, both scaffold templates — would have had to
15
+ // learn to supply it, and an optional method is a second way.
16
+
17
+ import { RouteStatusInvalidError } from './errors';
18
+ import { finiteStatus } from './finite-status';
19
+
20
+ /** What every render answers when the loader said nothing, and what a no-`load` route answers. */
21
+ export const DEFAULT_ROUTE_STATUS = 200;
22
+
23
+ const STATUSES = new WeakMap<object, number>();
24
+
25
+ /**
26
+ * What `withStatus` can mark, read back off `unknown`: exactly TypeScript's `object` — a non-null
27
+ * object OR a function. Both are `WeakMap` keys and both satisfy `withStatus`'s constraint, so a
28
+ * loader answering `withStatus(404, () => …)` — data that is a function, which `load`'s type
29
+ * allows — must read back as 404 and not as the default. The two sides of the seam share this
30
+ * one predicate so they cannot disagree about what carries a status.
31
+ */
32
+ const canCarryStatus = (data: unknown): data is object =>
33
+ (typeof data === 'object' && data !== null) || typeof data === 'function';
34
+
35
+ const isRedirect = (status: number): boolean => status >= 300 && status < 400;
36
+
37
+ /**
38
+ * Answer `status` for this render, and render the page with `data` all the same.
39
+ *
40
+ * Any 2xx, 4xx or 5xx. A 3xx is refused by name: a redirect is a `Location` and no body, which is
41
+ * `@ultimat3/http`'s `redirect()` and never a page. Out of range is `finiteStatus`'s refusal, the
42
+ * same screen `renderSsr` and `streamResult` apply — `NaN` reaching `new Response` is a bare
43
+ * `RangeError` two frames above the loader that set it.
44
+ *
45
+ * A 4xx or 5xx is `robots: noindex` BY CONSTRUCTION — `defineRoute`'s `meta` wrapper applies it —
46
+ * so a page that does not exist is never indexed however its `meta` was written.
47
+ */
48
+ export function withStatus<TData extends object>(status: number, data: TData): TData {
49
+ const screened = finiteStatus('withStatus', status);
50
+ if (isRedirect(screened)) {
51
+ throw new RouteStatusInvalidError(
52
+ `withStatus(${String(screened)}, …) asks a page to be a redirect, and a rendered document has no Location to send`,
53
+ "answer 2xx, 4xx or 5xx from load; a redirect is `redirect(location)` from '@ultimat3/http', thrown or returned by the handler",
54
+ );
55
+ }
56
+ STATUSES.set(data, screened);
57
+ return data;
58
+ }
59
+
60
+ /**
61
+ * The status a render of `data` answers: what `withStatus` recorded, else 200. TOTAL, and asked
62
+ * of `unknown` on purpose — the callers are the render modes, and the data is whatever the app's
63
+ * loader returned, an object or not.
64
+ */
65
+ export function routeStatusOf(data: unknown): number {
66
+ if (!canCarryStatus(data)) return DEFAULT_ROUTE_STATUS;
67
+ const status = STATUSES.get(data);
68
+ return status === undefined ? DEFAULT_ROUTE_STATUS : status;
69
+ }
70
+
71
+ /** A 4xx or 5xx: a document a crawler must forget, whatever its `meta` said. */
72
+ export const isErrorStatus = (status: number): boolean => status >= 400;
package/src/route.ts CHANGED
@@ -22,6 +22,7 @@ import { RouteLoadInvalidError, RouteMetaMissingError, RouteOfflineMissingError
22
22
  import type { IslandSpec } from './island';
23
23
  import { drainDeclaredIslands } from './island';
24
24
  import { assertModeShape } from './modes';
25
+ import { isErrorStatus, routeStatusOf } from './route-status';
25
26
 
26
27
  /**
27
28
  * What a page that declares an island hydrates as when it says nothing. The most conservative of
@@ -274,7 +275,12 @@ export function defineRoute<TData = RouteData>(
274
275
  // Wrapped rather than stored: the declaration may be sync, the descriptor never is.
275
276
  // A meta that throws synchronously becomes a rejection here, so `await config.meta(d)`
276
277
  // is the one way to fail as well as the one way to succeed.
277
- meta: async (metaCtx: RouteMetaContext<TData>) => declaredMeta(metaCtx),
278
+ // And the one place a 4xx/5xx becomes `noindex`: every consumer that renders a `<head>` —
279
+ // `x dev`, the prerenderer, the SEO scan — calls THIS function, so a page whose loader said
280
+ // 404 is never indexed however its `meta` was written. A 200 hands the author's object back
281
+ // untouched, so an app that never sets a status is byte-identical.
282
+ meta: async (metaCtx: RouteMetaContext<TData>) =>
283
+ noindexOnError(await declaredMeta(metaCtx), routeStatusOf(metaCtx.data)),
278
284
  // Always an object. `budget.js` is the only reach a consumer needs, so an undeclared
279
285
  // budget is `{}` instead of a second undefined-check at every call site.
280
286
  budget: def.budget ?? {},
@@ -290,6 +296,12 @@ export function defineRoute<TData = RouteData>(
290
296
  return Object.freeze(config);
291
297
  }
292
298
 
299
+ /** Only `index` is decided here; `follow` and the rest stay the author's. */
300
+ function noindexOnError(meta: RouteMeta, status: number): RouteMeta {
301
+ if (!isErrorStatus(status)) return meta;
302
+ return { ...meta, robots: { ...meta.robots, index: false } };
303
+ }
304
+
293
305
  export function isRouteConfig(value: unknown): value is RouteConfig {
294
306
  return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'route';
295
307
  }
package/src/server.ts CHANGED
@@ -16,7 +16,13 @@ installRenderLoader();
16
16
 
17
17
  // ---- scss → css, and the scoped class map every `import styles from` receives -------------------
18
18
  export type { CompiledStylesheet } from './css-modules';
19
- export { compileStylesheet, isCssModule, isGlobalStylesheet, scopeClasses } from './css-modules';
19
+ export {
20
+ compileStylesheet,
21
+ isCssModule,
22
+ isGlobalStylesheet,
23
+ scopeClasses,
24
+ stripCharset,
25
+ } from './css-modules';
20
26
  // ---- the two Bun loaders: `.tsx` → the server JSX factory, `.scss` → css + a class map ----------
21
27
  export type { Stylesheet } from './module-loader';
22
28
  export {
@@ -35,6 +41,7 @@ export type {
35
41
  IsrController,
36
42
  IsrControllerOptions,
37
43
  IsrEntry,
44
+ IsrRendered,
38
45
  IsrRenderFn,
39
46
  IsrServeResult,
40
47
  IsrState,