@zerotal/inertia 1.13.1 → 1.13.3

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/CHANGELOG.md CHANGED
@@ -8,6 +8,21 @@ follows the Zerotal monorepo's unified versioning.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [1.13.2] — 2026-08-31
12
+
13
+ ### Fixed
14
+
15
+ - **`Inertia.stream()` honours `X-Inertia`.** It answered every request with
16
+ `text/html`, including the XHR a running Inertia client sends — so pointing a route at
17
+ it to get server rendering broke client-side navigation _to_ that route.
18
+
19
+ The shape of the failure is why it went unnoticed: the first load looks perfect, which
20
+ is what a person checks, and the second click does nothing. It fails for people already
21
+ in the app, silently, and only after the route works.
22
+
23
+ `render` and `stream` now share the branch that writes the page object, rather than one
24
+ of them having it — which is exactly how they came apart.
25
+
11
26
  ## [1.11.0] — 2026-08-31
12
27
 
13
28
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerotal/inertia",
3
- "version": "1.13.1",
3
+ "version": "1.13.3",
4
4
  "license": "MIT",
5
5
  "maturity": "stable",
6
6
  "private": false,
@@ -34,7 +34,7 @@
34
34
  "typecheck": "tsc --noEmit"
35
35
  },
36
36
  "dependencies": {
37
- "@zerotal/core": "1.13.1"
37
+ "@zerotal/core": "1.13.3"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "react": "^18 || ^19",
package/src/inertia.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { config, RequestContext } from "@zerotal/core";
2
+ import type { HttpContext } from "@zerotal/core";
2
3
  import { assetVersion as coreAssetVersion } from "@zerotal/core/assets";
3
4
  import { statSync } from "node:fs";
4
5
  import { InertiaTemplateNotLoadedError, InvalidComponentError } from "./errors.ts";
@@ -267,6 +268,15 @@ async function _inertiaStream(component: string, props: Record<string, unknown>)
267
268
 
268
269
  const pageObject = await buildPageObject(component, props);
269
270
 
271
+ // A running Inertia client asks for a page object, not a document — server
272
+ // rendering is about the *first* arrival. Answering an `X-Inertia` XHR with HTML
273
+ // broke client-side navigation to any route that used this: the cold load looked
274
+ // right and the next click did nothing, silently, for someone already in the app.
275
+ if (ctx.request.headers.get("X-Inertia") === "true") {
276
+ _writeInertiaJson(ctx, pageObject);
277
+ return;
278
+ }
279
+
270
280
  const [prefix = "", suffix = ""] = _bustAssets(_htmlTemplate).split("<!-- @inertia -->");
271
281
 
272
282
  const { modPath, framework } = await resolvePageModule(_getPagesDir(), component);
@@ -405,6 +415,30 @@ export const inertia: PageRenderer = Object.assign(
405
415
  },
406
416
  );
407
417
 
418
+ /**
419
+ * The XHR half of the Inertia protocol: a page object as JSON.
420
+ *
421
+ * Shared by {@link inertia} and {@link inertiaStream} rather than written twice,
422
+ * because having it in only one of them is precisely the bug this exists to close.
423
+ * `stream` used to answer every request with `text/html` — the first load looked
424
+ * perfect, which is what a person checks, and the *second* click did nothing,
425
+ * because the running client got a document where it expected a page object.
426
+ *
427
+ * `Vary: X-Inertia` is critical: without it a browser caches the JSON as the HTML
428
+ * for the same URL and shows a raw page object on Back or Refresh.
429
+ *
430
+ * @internal
431
+ */
432
+ function _writeInertiaJson(ctx: HttpContext, pageObject: unknown): void {
433
+ ctx.response = new Response(JSON.stringify(pageObject), {
434
+ headers: {
435
+ "Content-Type": "application/json",
436
+ "X-Inertia": "true",
437
+ Vary: "X-Inertia",
438
+ },
439
+ });
440
+ }
441
+
408
442
  async function _inertia(component: string, props: Record<string, unknown>): Promise<void> {
409
443
  const ctx = RequestContext.get();
410
444
  const isInertiaRequest = ctx.request.headers.get("X-Inertia") === "true";
@@ -412,16 +446,7 @@ async function _inertia(component: string, props: Record<string, unknown>): Prom
412
446
  const pageObject = await buildPageObject(component, props);
413
447
 
414
448
  if (isInertiaRequest) {
415
- // Subsequent XHR navigation — JSON only
416
- // Vary: X-Inertia is CRITICAL — prevents browser from caching
417
- // JSON as HTML, which would show raw JSON on browser Back/Refresh
418
- ctx.response = new Response(JSON.stringify(pageObject), {
419
- headers: {
420
- "Content-Type": "application/json",
421
- "X-Inertia": "true",
422
- Vary: "X-Inertia",
423
- },
424
- });
449
+ _writeInertiaJson(ctx, pageObject);
425
450
  return;
426
451
  }
427
452