@zerotal/inertia 1.13.4 → 1.14.0

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,51 @@ follows the Zerotal monorepo's unified versioning.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [1.14.0] — 2026-09-01
12
+
13
+ ### Changed — **BREAKING**
14
+
15
+ - **`inertia.ssr: true` no longer registers `POST /__ssr`.** Rendering and the endpoint are
16
+ separate decisions: `ssr` renders in-process, `ssrEndpoint` exposes the route.
17
+
18
+ Most apps need no change. `ssr: true` still means "server-render every first page load",
19
+ which is what it is named for. Add `ssrEndpoint: true` only if something outside the web
20
+ process calls `/__ssr` — a separate renderer, a second host.
21
+
22
+ The endpoint exists because upstream Inertia runs on hosts with no JavaScript runtime:
23
+ PHP cannot import a `.tsx`, so it posts to a Node process. Bun is a JavaScript runtime,
24
+ so `ssr` imports the component and renders it inline, with no serialisation and no
25
+ network hop. The endpoint remains for the case that is still real — deliberately moving
26
+ render CPU off the web process — and is no longer something an app gets by accident.
27
+
28
+ Turning rendering on should not open a route that renders arbitrary components from POST
29
+ input, however well guarded. One switch, one thing.
30
+
31
+ ## [1.13.5] — 2026-09-01
32
+
33
+ ### Fixed
34
+
35
+ - **`inertia.ssr: true` now actually server-renders.** The flag registered `POST /__ssr`
36
+ and nothing in the request path consulted it, so an app that set it — and read the
37
+ documentation, which said the server renders the component into the template — got
38
+ exactly the empty root it had before.
39
+
40
+ `Inertia.render()` renders the component into the root, injects the page's `<Head>` into
41
+ the served `<head>`, and marks the root `data-server-rendered`. The scaffolded `app.tsx`
42
+ already hydrated on that attribute, so the client half needed no change: an app turns SSR
43
+ on with one config line and nothing else.
44
+
45
+ Server rendering was previously reachable only by rewriting each route to
46
+ `Inertia.stream()`, one call site at a time, which is not what a global switch means. An
47
+ app that did that can go back to `render()`.
48
+
49
+ A component that fails to render falls back to the client-rendered document with a
50
+ warning rather than failing the route: the page still works in a browser, and taking a
51
+ route down because an _optimisation_ failed would make `ssr: true` a liability.
52
+
53
+ `POST /__ssr` is unchanged and stays documented for what it is — the contract for an
54
+ external renderer, not the in-process switch.
55
+
11
56
  ## [1.13.2] — 2026-08-31
12
57
 
13
58
  ### Fixed
package/api-surface.md CHANGED
@@ -241,6 +241,7 @@ interface InertiaConfigShape = {
241
241
  htmlTemplate: string
242
242
  pagesDir: string
243
243
  ssr: boolean
244
+ ssrEndpoint: boolean
244
245
  ssrSecret: string
245
246
  version: string
246
247
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerotal/inertia",
3
- "version": "1.13.4",
3
+ "version": "1.14.0",
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.4"
37
+ "@zerotal/core": "1.14.0"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "react": "^18 || ^19",
package/src/config.ts CHANGED
@@ -14,20 +14,44 @@ export interface InertiaConfigShape {
14
14
  */
15
15
  pagesDir: string;
16
16
  /**
17
- * Enable server-side rendering.
17
+ * Server-render every first page load.
18
18
  *
19
- * When true, InertiaProvider registers POST /__ssr which accepts
20
- * { component, props, url } and returns { body, head } - the same
21
- * contract as the Inertia Node SSR server. The Inertia client calls
22
- * this endpoint when rendering the first page load on the server.
19
+ * `Inertia.render()` renders the component into the root, injects the page's
20
+ * `<Head>` into the served `<head>`, and marks the root `data-server-rendered`
21
+ * so the client hydrates it. One line; no controller changes.
23
22
  *
24
- * Pages are rendered with the framework they're authored in: React `.tsx`
25
- * via `react-dom/server`, or Vue `.vue` via `@inertiajs/vue3` + `vue/server-renderer`.
26
- * Install the server renderer for the framework(s) the app uses.
23
+ * Pages render with the framework they are authored in: React `.tsx` via
24
+ * `react-dom/server`, Vue `.vue` via `@inertiajs/vue3` + `vue/server-renderer`.
25
+ * Install the server renderer for the framework the app uses.
26
+ *
27
+ * **This renders in-process and registers no route.** Until 1.14.0 it did the
28
+ * opposite — it registered `POST /__ssr` and rendered nothing — so an app that
29
+ * set it got an HTTP endpoint it had not asked for and the empty root it
30
+ * already had. See {@link ssrEndpoint} for the endpoint, which is now its own
31
+ * decision.
27
32
  *
28
33
  * Default: false
29
34
  */
30
35
  ssr: boolean;
36
+ /**
37
+ * Expose `POST /__ssr` for a renderer running outside this process.
38
+ *
39
+ * The endpoint accepts `{ component, props, url }` and returns `{ body, head }` —
40
+ * the contract upstream Inertia uses, where the web framework has no JavaScript
41
+ * runtime and must hand rendering to a separate Node process.
42
+ *
43
+ * **Zerotal does not need that boundary.** Bun *is* a JavaScript runtime, so
44
+ * {@link ssr} imports the component and renders it inline. The endpoint remains
45
+ * for the one case that is still real: deliberately moving render CPU off the
46
+ * web process, onto another process or another host.
47
+ *
48
+ * Separate from {@link ssr} since 1.14.0. Turning rendering on should not open a
49
+ * route that renders arbitrary components from POST input, however well guarded —
50
+ * one switch, one thing.
51
+ *
52
+ * Default: false
53
+ */
54
+ ssrEndpoint: boolean;
31
55
  /**
32
56
  * Shared secret required to reach `POST /__ssr` from off-box.
33
57
  *
@@ -105,6 +129,7 @@ const defaults: InertiaConfigShape = {
105
129
  assetsUrl: "/",
106
130
  pagesDir: DEFAULT_PAGES_DIR,
107
131
  ssr: false,
132
+ ssrEndpoint: false,
108
133
  ssrSecret: "",
109
134
  encryptHistory: false,
110
135
  devtools: {
@@ -135,7 +160,7 @@ function _envFlag(name: string): boolean | null {
135
160
  * export default InertiaConfig({
136
161
  * htmlTemplate: './resources/app.html',
137
162
  * version: Bun.env['ASSET_VERSION'] ?? '1',
138
- * ssr: true, // enable SSR endpoint at POST /__ssr
163
+ * ssr: true, // server-render every first page load
139
164
  * });
140
165
  */
141
166
  export function InertiaConfig(options: Partial<InertiaConfigShape> = {}): InertiaConfigShape {
package/src/inertia.ts CHANGED
@@ -464,14 +464,12 @@ async function _renderedHtml(component: string, pageObject: PageObject): Promise
464
464
  const { modPath, framework } = await resolvePageModule(_getPagesDir(), component);
465
465
  const { body, head } = await renderInertiaPage(pageObject, modPath, framework);
466
466
 
467
- return (
468
- injectHead(prefix, head) +
469
- pageScript(pageObject) +
470
- rootOpen(true) +
471
- body +
472
- ROOT_CLOSE +
473
- suffix
474
- );
467
+ // `body` is already the whole mount root — `renderInertiaPage` returns
468
+ // `pageScript + rootOpen + html + ROOT_CLOSE` for React, and Vue's SSR result
469
+ // carries its own root too. Wrapping it again emits two `<div id="app">` and
470
+ // two `data-page` scripts, and the client hydrates against the wrong one.
471
+ // Composed exactly as `inertiaStream` composes it, so the two cannot drift.
472
+ return injectHead(prefix, head) + body + suffix;
475
473
  } catch (error) {
476
474
  console.warn(
477
475
  `[Inertia] SSR render failed for "${component}", serving the client-rendered ` +
@@ -103,8 +103,11 @@ export class InertiaProvider extends ServiceProvider {
103
103
  // installed, so React apps are unaffected.
104
104
  await registerVueRuntimeLoader(process.cwd());
105
105
 
106
- // 3. Register SSR endpoint when enabled
107
- if (config.get<boolean>("inertia.ssr", false)) {
106
+ // 3. Register the SSR endpoint when the app asks for it — `ssrEndpoint`, not
107
+ // `ssr`. Rendering happens in-process (see `inertia.ts`); this route exists only
108
+ // for a renderer running somewhere else, and turning rendering on should not
109
+ // open a route that renders arbitrary components from POST input.
110
+ if (config.get<boolean>("inertia.ssrEndpoint", false)) {
108
111
  // Throttled as well as loopback-gated: rendering a page is real CPU, and this route
109
112
  // sits outside every application guard.
110
113
  Router.post(