@stratal/inertia 0.0.22 → 0.0.24

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.
Files changed (38) hide show
  1. package/README.md +33 -1
  2. package/dist/build-seo-tags-DBsHKxX9.mjs +123 -0
  3. package/dist/build-seo-tags-DBsHKxX9.mjs.map +1 -0
  4. package/dist/{decorate-CzXVx7ZH.mjs → decorate-B7nr7eBl.mjs} +1 -1
  5. package/dist/generator/type-generator.worker.mjs +1 -1
  6. package/dist/generator/type-generator.worker.mjs.map +1 -1
  7. package/dist/index.d.mts +209 -78
  8. package/dist/index.d.mts.map +1 -1
  9. package/dist/index.mjs +274 -60
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/quarry.d.mts +9 -0
  12. package/dist/quarry.d.mts.map +1 -1
  13. package/dist/quarry.mjs +56 -9
  14. package/dist/quarry.mjs.map +1 -1
  15. package/dist/react.d.mts +15 -3
  16. package/dist/react.d.mts.map +1 -1
  17. package/dist/react.mjs +21 -8
  18. package/dist/react.mjs.map +1 -1
  19. package/dist/seo-runtime.d.mts +1 -0
  20. package/dist/seo-runtime.mjs +56 -0
  21. package/dist/seo-runtime.mjs.map +1 -0
  22. package/dist/ssr.d.mts +65 -0
  23. package/dist/ssr.d.mts.map +1 -0
  24. package/dist/ssr.mjs +56 -0
  25. package/dist/ssr.mjs.map +1 -0
  26. package/dist/testing.d.mts +1 -1
  27. package/dist/testing.mjs.map +1 -1
  28. package/dist/{type-generator-bfo14BJI.mjs → type-generator-DFpha_Fp.mjs} +178 -28
  29. package/dist/type-generator-DFpha_Fp.mjs.map +1 -0
  30. package/dist/types-BhgXhWx6.d.mts +82 -0
  31. package/dist/types-BhgXhWx6.d.mts.map +1 -0
  32. package/dist/types-DzE1pdZs.d.mts +76 -0
  33. package/dist/types-DzE1pdZs.d.mts.map +1 -0
  34. package/dist/vite.d.mts.map +1 -1
  35. package/dist/vite.mjs +22 -2
  36. package/dist/vite.mjs.map +1 -1
  37. package/package.json +27 -18
  38. package/dist/type-generator-bfo14BJI.mjs.map +0 -1
package/dist/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
- import { t as __decorate } from "./decorate-CzXVx7ZH.mjs";
1
+ import { t as __decorate } from "./decorate-B7nr7eBl.mjs";
2
+ import { n as buildSeoTags, r as descriptorToHtml, t as DATA_SEO_ATTR } from "./build-seo-tags-DBsHKxX9.mjs";
2
3
  import { ApplicationError } from "stratal/errors";
3
4
  import { Module } from "stratal/module";
4
- import { Delete, Get, Patch, Post, Put, ROUTER_TOKENS, Route, RouterContext, SchemaValidationError } from "stratal/router";
5
- import { DI_TOKENS, Request, Singleton, Transient, inject } from "stratal/di";
5
+ import { Delete, Get, Patch, Post, Put, ROUTER_TOKENS, Route, RouterContext, SchemaValidationError, applyTrailingSlash } from "stratal/router";
6
+ import { CONTAINER_TOKEN, DI_TOKENS, Request, Singleton, Transient, inject } from "stratal/di";
6
7
  import { I18N_TOKENS } from "stratal/i18n";
7
- import { LOGGER_TOKENS } from "stratal/logger";
8
8
  import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie";
9
9
  import { z } from "stratal/validation";
10
10
  //#region src/augment/router-context.ts
@@ -39,6 +39,12 @@ function augmentRouterContext(resolveService) {
39
39
  const flashOut = this.c.get("inertiaFlashOut");
40
40
  if (flashOut) flashOut[key] = value;
41
41
  });
42
+ RouterContext.macro("share", function(key, value) {
43
+ resolveService(this).share(key, value);
44
+ });
45
+ RouterContext.macro("seo", function(data) {
46
+ resolveService(this).seo(data);
47
+ });
42
48
  RouterContext.macro("withoutSsr", function() {
43
49
  this.c.set("withoutSsr", true);
44
50
  });
@@ -50,10 +56,12 @@ const INERTIA_TOKENS = {
50
56
  InertiaService: Symbol.for("stratal:inertia:service"),
51
57
  TemplateService: Symbol.for("stratal:inertia:template"),
52
58
  ManifestService: Symbol.for("stratal:inertia:manifest"),
53
- SsrRenderer: Symbol.for("stratal:inertia:ssr-renderer")
59
+ SsrRenderer: Symbol.for("stratal:inertia:ssr-renderer"),
60
+ HreflangService: Symbol.for("stratal:inertia:hreflang"),
61
+ SeoService: Symbol.for("stratal:inertia:seo")
54
62
  };
55
63
  //#endregion
56
- //#region \0@oxc-project+runtime@0.129.0/helpers/decorateParam.js
64
+ //#region \0@oxc-project+runtime@0.133.0/helpers/esm/decorateParam.js
57
65
  function __decorateParam(paramIndex, decorator) {
58
66
  return function(target, key) {
59
67
  decorator(target, key, paramIndex);
@@ -105,6 +113,60 @@ let InertiaMiddleware = class InertiaMiddleware {
105
113
  };
106
114
  InertiaMiddleware = __decorate([Transient(), __decorateParam(0, inject(INERTIA_TOKENS.Options))], InertiaMiddleware);
107
115
  //#endregion
116
+ //#region src/services/hreflang.service.ts
117
+ let HreflangService = class HreflangService {
118
+ container;
119
+ constructor(container) {
120
+ this.container = container;
121
+ }
122
+ buildLinks(currentUrl) {
123
+ const i18n = this.container.tryResolve(I18N_TOKENS.Options);
124
+ if (!i18n) return [];
125
+ const locales = i18n.locales ?? ["en"];
126
+ if (locales.length < 2) return [];
127
+ const defaultLocale = i18n.defaultLocale ?? "en";
128
+ const trailingSlash = this.container.resolve(DI_TOKENS.Application).config.trailingSlash ?? "ignore";
129
+ const localeUrl = this.container.resolve(ROUTER_TOKENS.LocaleUrlService);
130
+ if (localeUrl.pathEnabled) return this.buildPathLinks(currentUrl, locales, defaultLocale, localeUrl, trailingSlash);
131
+ if ((i18n.detection && "strategy" in i18n.detection ? i18n.detection.strategy : void 0) === "querystring") return this.buildQuerystringLinks(currentUrl, locales, defaultLocale, trailingSlash);
132
+ return [];
133
+ }
134
+ buildPathLinks(url, locales, defaultLocale, localeUrl, trailingSlash) {
135
+ const basePath = localeUrl.stripPrefix(url.pathname);
136
+ const links = locales.map((locale) => this.linkTag(locale, this.compose(url, localeUrl.applyPrefix(basePath, locale), url.search, trailingSlash)));
137
+ links.push(this.linkTag("x-default", this.compose(url, localeUrl.applyPrefix(basePath, defaultLocale), url.search, trailingSlash)));
138
+ return links;
139
+ }
140
+ buildQuerystringLinks(url, locales, defaultLocale, trailingSlash) {
141
+ const params = new URLSearchParams(url.search);
142
+ params.delete("locale");
143
+ const baseQs = params.toString();
144
+ const links = locales.map((locale) => {
145
+ const qs = this.composeQuery(baseQs, locale === defaultLocale ? null : ["locale", locale]);
146
+ return this.linkTag(locale, this.compose(url, url.pathname, qs, trailingSlash));
147
+ });
148
+ const xDefaultQs = baseQs ? `?${baseQs}` : "";
149
+ links.push(this.linkTag("x-default", this.compose(url, url.pathname, xDefaultQs, trailingSlash)));
150
+ return links;
151
+ }
152
+ compose(url, pathname, search, mode) {
153
+ return applyTrailingSlash(url.origin + pathname + search, mode);
154
+ }
155
+ composeQuery(baseQs, extra) {
156
+ if (!extra) return baseQs ? `?${baseQs}` : "";
157
+ const tail = `${extra[0]}=${encodeURIComponent(extra[1])}`;
158
+ return baseQs ? `?${baseQs}&${tail}` : `?${tail}`;
159
+ }
160
+ linkTag(hreflang, href) {
161
+ return {
162
+ rel: "alternate",
163
+ hreflang,
164
+ href
165
+ };
166
+ }
167
+ };
168
+ HreflangService = __decorate([Singleton(), __decorateParam(0, inject(CONTAINER_TOKEN))], HreflangService);
169
+ //#endregion
108
170
  //#region src/types.ts
109
171
  const INERTIA_PROP_OPTIONAL = Symbol.for("stratal:inertia:prop:optional");
110
172
  const INERTIA_PROP_DEFERRED = Symbol.for("stratal:inertia:prop:deferred");
@@ -117,15 +179,20 @@ let InertiaService = class InertiaService {
117
179
  options;
118
180
  template;
119
181
  ssr;
182
+ seoService;
120
183
  sharedData = {};
121
- constructor(options, template, ssr) {
184
+ constructor(options, template, ssr, seoService) {
122
185
  this.options = options;
123
186
  this.template = template;
124
187
  this.ssr = ssr;
188
+ this.seoService = seoService;
125
189
  }
126
190
  share(key, value) {
127
191
  this.sharedData[key] = value;
128
192
  }
193
+ seo(data) {
194
+ this.seoService.set(data);
195
+ }
129
196
  location(url) {
130
197
  return new Response("", {
131
198
  status: 409,
@@ -170,14 +237,21 @@ let InertiaService = class InertiaService {
170
237
  async render(ctx, component, props = {}, renderOptions = {}) {
171
238
  const reqUrl = new URL(ctx.c.req.url);
172
239
  const url = reqUrl.search ? `${reqUrl.pathname}${reqUrl.search}` : reqUrl.pathname;
240
+ const pathname = reqUrl.pathname;
173
241
  const isInertia = ctx.c.get("inertia");
174
242
  const { shared: resolvedShared, sharedKeys } = await this.resolveSharedData(ctx);
243
+ const resolvedSeo = await this.seoService.resolve(ctx);
175
244
  const allProps = {
176
245
  ...resolvedShared,
177
246
  ...this.sharedData,
247
+ seo: this.always(() => resolvedSeo),
178
248
  ...props
179
249
  };
180
- const allSharedKeys = [...sharedKeys, ...Object.keys(this.sharedData)];
250
+ const allSharedKeys = [
251
+ ...sharedKeys,
252
+ ...Object.keys(this.sharedData),
253
+ "seo"
254
+ ];
181
255
  const result = await this.processProps(allProps, ctx, component, isInertia);
182
256
  const { errors: flashErrors, ...flash } = ctx.c.get("inertiaFlash") ?? {};
183
257
  const errors = flashErrors && typeof flashErrors === "object" && !Array.isArray(flashErrors) ? flashErrors : {};
@@ -213,12 +287,17 @@ let InertiaService = class InertiaService {
213
287
  "Vary": "X-Inertia"
214
288
  }
215
289
  });
216
- const ssrResult = ctx.c.get("withoutSsr") || this.isSsrDisabled(url) ? {
217
- head: [],
218
- body: ""
219
- } : await this.ssr.render(page);
220
- const html = this.template.render(page, ssrResult.head, ssrResult.body);
221
- return new Response(html, {
290
+ const seoTags = this.seoService.tagsFor(resolvedSeo);
291
+ if (ctx.c.get("withoutSsr") || this.isSsrDisabled(pathname) || !this.options.ssr) {
292
+ const html = this.template.renderClientOnly(page, seoTags);
293
+ return new Response(html, {
294
+ status,
295
+ headers: { "Content-Type": "text/html; charset=utf-8" }
296
+ });
297
+ }
298
+ const { head, stream } = await this.ssr.render(page);
299
+ const body = this.template.renderStream(page, [...head, ...seoTags], stream);
300
+ return new Response(body, {
222
301
  status,
223
302
  headers: { "Content-Type": "text/html; charset=utf-8" }
224
303
  });
@@ -248,6 +327,7 @@ let InertiaService = class InertiaService {
248
327
  const uri = container.resolve(ROUTER_TOKENS.Uri);
249
328
  const name = registry.findNameByRoute(ctx.c.req.method, ctx.c.req.routePath) ?? null;
250
329
  const params = { ...ctx.param() };
330
+ const localePathService = container.resolve(ROUTER_TOKENS.LocalePathService);
251
331
  shared.routes = this.serializeRoutes(registry.named());
252
332
  shared.trailingSlash = application.config.trailingSlash ?? "ignore";
253
333
  shared.route = {
@@ -255,6 +335,10 @@ let InertiaService = class InertiaService {
255
335
  params,
256
336
  defaults: uri.getDefaults()
257
337
  };
338
+ shared.localeConfig = {
339
+ defaultLocale: localePathService.localePathConfig?.defaultLocale ?? null,
340
+ prefixDefaultLocale: localePathService.prefixDefaultLocale
341
+ };
258
342
  }
259
343
  return {
260
344
  shared,
@@ -393,7 +477,8 @@ InertiaService = __decorate([
393
477
  Request(INERTIA_TOKENS.InertiaService),
394
478
  __decorateParam(0, inject(INERTIA_TOKENS.Options)),
395
479
  __decorateParam(1, inject(INERTIA_TOKENS.TemplateService)),
396
- __decorateParam(2, inject(INERTIA_TOKENS.SsrRenderer))
480
+ __decorateParam(2, inject(INERTIA_TOKENS.SsrRenderer)),
481
+ __decorateParam(3, inject(INERTIA_TOKENS.SeoService))
397
482
  ], InertiaService);
398
483
  //#endregion
399
484
  //#region src/services/manifest.service.ts
@@ -402,12 +487,20 @@ let ManifestService = class ManifestService {
402
487
  manifest;
403
488
  entryClientPath;
404
489
  isDev = Boolean(import.meta.env.DEV);
490
+ headTags = null;
491
+ scriptTags = null;
405
492
  constructor(options) {
406
493
  this.manifest = globalThis.__STRATAL_INERTIA_MANIFEST__ ?? null;
407
494
  this.entryClientPath = (options.entryClientPath ?? DEFAULT_ENTRY_CLIENT_PATH).replace(/^\/+/, "");
408
495
  if (!this.isDev && !this.manifest) throw new Error("@stratal/inertia: production build is missing the Vite client manifest. This is wired by stratalInertia() in vite.config.ts — confirm it is in your plugin list and that the client environment built successfully before the worker environment.");
409
496
  }
410
497
  getHeadTags() {
498
+ return this.headTags ??= this.buildHeadTags();
499
+ }
500
+ getScriptTags() {
501
+ return this.scriptTags ??= this.buildScriptTags();
502
+ }
503
+ buildHeadTags() {
411
504
  if (this.isDev) return "<link rel=\"stylesheet\" href=\"/__inertia/ssr-css\" data-ssr-css />";
412
505
  const tags = [];
413
506
  const seen = /* @__PURE__ */ new Set();
@@ -418,7 +511,7 @@ let ManifestService = class ManifestService {
418
511
  }
419
512
  return tags.join("\n");
420
513
  }
421
- getScriptTags() {
514
+ buildScriptTags() {
422
515
  if (this.isDev) return [
423
516
  "<script type=\"module\" src=\"/@vite/client\"><\/script>",
424
517
  `<script type="module">
@@ -435,28 +528,90 @@ hot.on("vite:afterUpdate", () => {
435
528
  return tags.join("\n");
436
529
  }
437
530
  };
438
- ManifestService = __decorate([Transient(), __decorateParam(0, inject(INERTIA_TOKENS.Options))], ManifestService);
531
+ ManifestService = __decorate([Singleton(), __decorateParam(0, inject(INERTIA_TOKENS.Options))], ManifestService);
532
+ //#endregion
533
+ //#region src/services/seo.service.ts
534
+ let SeoService = class SeoService {
535
+ options;
536
+ hreflang;
537
+ accumulated = {};
538
+ constructor(options, hreflang) {
539
+ this.options = options;
540
+ this.hreflang = hreflang;
541
+ }
542
+ /** Merges the given metadata into the request's accumulated SEO data. */
543
+ set(data) {
544
+ this.accumulated = mergeSeo(this.accumulated, data);
545
+ }
546
+ /**
547
+ * Resolves the final SEO data: module defaults (base) merged with the
548
+ * request's accumulated data, then the title template applied. Resolver
549
+ * functions for `defaults`/`titleTemplate` are awaited with the request `ctx`.
550
+ * Locale-aware `hreflang` alternates are appended last so they ride the same
551
+ * head injection and SPA reconciliation as the rest of the SEO tags.
552
+ *
553
+ * The resolved `title` is ALWAYS a string (falling back to `''`). This makes
554
+ * the `<title>` descriptor deterministic: every navigation — including to a
555
+ * page with no SEO — produces a title, so the client head-sync sets
556
+ * `document.title` rather than leaving the previous page's title stale.
557
+ */
558
+ async resolve(ctx) {
559
+ const seo = this.options.seo;
560
+ const resolved = mergeSeo(typeof seo?.defaults === "function" ? await seo.defaults(ctx) : seo?.defaults ?? {}, this.accumulated);
561
+ const template = seo?.titleTemplate;
562
+ if (typeof template === "function") resolved.title = await template(resolved.title, ctx);
563
+ else if (typeof template === "string" && this.accumulated.title != null) resolved.title = template.split("%s").join(this.accumulated.title);
564
+ resolved.title ??= "";
565
+ const hreflang = this.hreflang.buildLinks(new URL(ctx.c.req.url));
566
+ if (hreflang.length > 0) resolved.link = [...resolved.link ?? [], ...hreflang];
567
+ return resolved;
568
+ }
569
+ /** Renders resolved SEO data into a list of head-tag HTML strings. */
570
+ tagsFor(resolved) {
571
+ return buildSeoTags(resolved).map(descriptorToHtml);
572
+ }
573
+ };
574
+ SeoService = __decorate([
575
+ Request(INERTIA_TOKENS.SeoService),
576
+ __decorateParam(0, inject(INERTIA_TOKENS.Options)),
577
+ __decorateParam(1, inject(INERTIA_TOKENS.HreflangService))
578
+ ], SeoService);
579
+ /** Merges `b` over `a`: `openGraph`/`twitter` shallow-merge, `meta`/`link` concat, scalars overwrite. */
580
+ function mergeSeo(a, b) {
581
+ return {
582
+ ...a,
583
+ ...b,
584
+ ...a.openGraph || b.openGraph ? { openGraph: {
585
+ ...a.openGraph,
586
+ ...b.openGraph
587
+ } } : {},
588
+ ...a.twitter || b.twitter ? { twitter: {
589
+ ...a.twitter,
590
+ ...b.twitter
591
+ } } : {},
592
+ ...a.meta || b.meta ? { meta: [...a.meta ?? [], ...b.meta ?? []] } : {},
593
+ ...a.link || b.link ? { link: [...a.link ?? [], ...b.link ?? []] } : {}
594
+ };
595
+ }
439
596
  //#endregion
440
597
  //#region src/services/ssr-renderer.service.ts
441
598
  let SsrRendererService = class SsrRendererService {
442
599
  options;
443
- logger;
444
600
  bundle = null;
445
601
  loadPromise = null;
446
- constructor(options, logger) {
602
+ constructor(options) {
447
603
  this.options = options;
448
- this.logger = logger;
449
604
  }
605
+ /**
606
+ * Render a page to a streaming SSR result.
607
+ *
608
+ * The SSR bundle is imported once per worker (memoized). Bundle-load and render
609
+ * errors propagate — there is no silent client-side fallback. Callers must only
610
+ * invoke this when `options.ssr` is configured.
611
+ */
450
612
  async render(page) {
451
- if (!this.options.ssr) return {
452
- head: [],
453
- body: ""
454
- };
613
+ if (!this.options.ssr) throw new ApplicationError("[stratal:inertia] SSR bundle is not configured.");
455
614
  await this.ensureBundle();
456
- if (!this.bundle) return {
457
- head: [],
458
- body: ""
459
- };
460
615
  return this.bundle.render(page);
461
616
  }
462
617
  async ensureBundle() {
@@ -464,52 +619,84 @@ let SsrRendererService = class SsrRendererService {
464
619
  this.loadPromise ??= this.loadBundle();
465
620
  try {
466
621
  await this.loadPromise;
467
- } catch {}
468
- }
469
- async loadBundle() {
470
- if (!this.options.ssr) return;
471
- try {
472
- const mod = await this.options.ssr.bundle();
473
- const resolved = "default" in mod ? mod.default : mod;
474
- this.bundle = resolved;
475
622
  } catch (error) {
476
- this.logger.warn("[stratal:inertia] Failed to load SSR bundle. Falling back to client-side rendering.", { error });
477
623
  this.loadPromise = null;
624
+ throw error;
478
625
  }
479
626
  }
627
+ async loadBundle() {
628
+ const mod = await this.options.ssr.bundle();
629
+ this.bundle = "default" in mod ? mod.default : mod;
630
+ }
480
631
  };
481
- SsrRendererService = __decorate([
482
- Singleton(),
483
- __decorateParam(0, inject(INERTIA_TOKENS.Options)),
484
- __decorateParam(1, inject(LOGGER_TOKENS.LoggerService))
485
- ], SsrRendererService);
632
+ SsrRendererService = __decorate([Singleton(), __decorateParam(0, inject(INERTIA_TOKENS.Options))], SsrRendererService);
486
633
  //#endregion
487
634
  //#region src/services/template.service.ts
635
+ const APP_ID = "app";
488
636
  let TemplateService = class TemplateService {
489
- options;
490
637
  manifest;
638
+ pre;
639
+ post;
491
640
  constructor(options, manifest) {
492
- this.options = options;
493
641
  this.manifest = manifest;
642
+ const match = /@inertia\b/.exec(options.rootView);
643
+ if (!match) throw new ApplicationError("[stratal:inertia] rootView template is missing the @inertia placeholder.");
644
+ this.pre = options.rootView.slice(0, match.index);
645
+ this.post = options.rootView.slice(match.index + 8);
646
+ }
647
+ /**
648
+ * Compose the streamed HTML response: the document shell (head + opening
649
+ * `#app` wrapper) is flushed first, the React stream is piped verbatim, then
650
+ * the wrapper is closed and the trailing scripts are appended.
651
+ *
652
+ * Reproduces Inertia's `buildSSRBody` markup: a `<script data-page>` JSON tag
653
+ * (parsed before hydration) followed by `<div data-server-rendered id="app">`.
654
+ */
655
+ renderStream(page, head, reactStream) {
656
+ const encoder = new TextEncoder();
657
+ const shellPre = this.fillPlaceholders(this.pre, head) + `<script data-page="${APP_ID}" type="application/json">${this.serialize(page)}<\/script><div data-server-rendered="true" id="${APP_ID}">`;
658
+ const shellPost = `</div>${this.fillPlaceholders(this.post, head)}`;
659
+ let reader;
660
+ return new ReadableStream({
661
+ async start(controller) {
662
+ controller.enqueue(encoder.encode(shellPre));
663
+ reader = reactStream.getReader();
664
+ try {
665
+ for (;;) {
666
+ const { done, value } = await reader.read();
667
+ if (done) break;
668
+ controller.enqueue(value);
669
+ }
670
+ controller.enqueue(encoder.encode(shellPost));
671
+ controller.close();
672
+ } catch (error) {
673
+ controller.error(error);
674
+ } finally {
675
+ reader.releaseLock();
676
+ reader = void 0;
677
+ }
678
+ },
679
+ cancel(reason) {
680
+ return reader?.cancel(reason) ?? reactStream.cancel(reason);
681
+ }
682
+ });
683
+ }
684
+ /**
685
+ * Buffered, client-only document used when SSR is disabled for the request.
686
+ * Emits an empty `#app` div for the client bundle to hydrate.
687
+ */
688
+ renderClientOnly(page, head) {
689
+ return this.fillPlaceholders(this.pre, head) + `<script data-page="${APP_ID}" type="application/json">${this.serialize(page)}<\/script><div id="${APP_ID}"></div>` + this.fillPlaceholders(this.post, head);
690
+ }
691
+ fillPlaceholders(segment, head) {
692
+ return segment.replace("@inertiaHead", () => head.join("\n")).replace("@viteHead", () => this.manifest.getHeadTags()).replace("@viteScripts", () => this.manifest.getScriptTags());
494
693
  }
495
- render(page, ssrHead, ssrBody) {
496
- const appHtml = ssrBody || this.buildClientOnlyBody(page);
497
- const headTags = ssrHead.length > 0 ? ssrHead.join("\n") : "";
498
- const viteHead = this.manifest.getHeadTags();
499
- const viteScripts = this.manifest.getScriptTags();
500
- let html = this.options.rootView;
501
- html = html.replace("@inertiaHead", headTags);
502
- html = html.replace("@inertia", appHtml);
503
- html = html.replace("@viteHead", viteHead);
504
- html = html.replace("@viteScripts", viteScripts);
505
- return html;
506
- }
507
- buildClientOnlyBody(page) {
508
- return `<script data-page="app" type="application/json">${JSON.stringify(page).replace(/\//g, "\\/")}<\/script><div id="app"></div>`;
694
+ serialize(page) {
695
+ return JSON.stringify(page).replace(/\//g, "\\/");
509
696
  }
510
697
  };
511
698
  TemplateService = __decorate([
512
- Transient(),
699
+ Singleton(),
513
700
  __decorateParam(0, inject(INERTIA_TOKENS.Options)),
514
701
  __decorateParam(1, inject(INERTIA_TOKENS.ManifestService))
515
702
  ], TemplateService);
@@ -544,6 +731,7 @@ let InertiaModule = _InertiaModule = class InertiaModule {
544
731
  if (context.type !== "http") return void 0;
545
732
  if (this.isPrecognitionRequest(context)) return this.handlePrecognitionValidationError(error, context);
546
733
  if (!this.isInertiaRequest(context)) return void 0;
734
+ if (this.isReadRequest(context)) return void 0;
547
735
  const issues = error.issues ?? [];
548
736
  const errors = {};
549
737
  for (const issue of issues) errors[issue.path] = issue.message;
@@ -555,6 +743,7 @@ let InertiaModule = _InertiaModule = class InertiaModule {
555
743
  const message = error.message;
556
744
  if (this.isPrecognitionRequest(context)) return this.createPrecognitionErrorResponse({ _form: message });
557
745
  if (!this.isInertiaRequest(context)) return void 0;
746
+ if (this.isReadRequest(context)) return void 0;
558
747
  context.ctx.flash("errors", { _form: message });
559
748
  return this.redirectBack(context);
560
749
  });
@@ -577,6 +766,23 @@ let InertiaModule = _InertiaModule = class InertiaModule {
577
766
  isInertiaRequest(context) {
578
767
  return context.ctx.header("x-inertia") === "true";
579
768
  }
769
+ /**
770
+ * GET/HEAD requests are idempotent navigations — including Inertia deferred
771
+ * partial reloads, which fetch deferred props over a follow-up XHR that still
772
+ * carries `X-Inertia: true`.
773
+ *
774
+ * Such requests must NOT use the flash-errors + redirect-back convention: the
775
+ * redirect points back at the very URL that just threw, so an error raised
776
+ * while resolving a deferred prop would redirect → re-request → throw again
777
+ * in an infinite loop (`ERR_TOO_MANY_REDIRECTS`). For these we fall through to
778
+ * the errorPage pipeline, which renders `Errors/${status}` in place as an
779
+ * Inertia response. Redirect-back stays for mutations (POST/PUT/PATCH/DELETE),
780
+ * where it drives the post-submit form-error flow.
781
+ */
782
+ isReadRequest(context) {
783
+ const method = context.ctx.c.req.method.toUpperCase();
784
+ return method === "GET" || method === "HEAD";
785
+ }
580
786
  isPrecognitionRequest(context) {
581
787
  return context.ctx.header("precognition") === "true";
582
788
  }
@@ -637,6 +843,14 @@ InertiaModule = _InertiaModule = __decorate([Module({ providers: [
637
843
  {
638
844
  provide: INERTIA_TOKENS.SsrRenderer,
639
845
  useClass: SsrRendererService
846
+ },
847
+ {
848
+ provide: INERTIA_TOKENS.HreflangService,
849
+ useClass: HreflangService
850
+ },
851
+ {
852
+ provide: INERTIA_TOKENS.SeoService,
853
+ useClass: SeoService
640
854
  }
641
855
  ] })], InertiaModule);
642
856
  //#endregion
@@ -829,6 +1043,6 @@ let HandlePrecognitiveRequests = class HandlePrecognitiveRequests {
829
1043
  };
830
1044
  HandlePrecognitiveRequests = __decorate([Transient()], HandlePrecognitiveRequests);
831
1045
  //#endregion
832
- export { CookieFlashStore, HandlePrecognitiveRequests, INERTIA_TOKENS, InertiaDelete, InertiaGet, InertiaMiddleware, InertiaModule, InertiaPatch, InertiaPost, InertiaPut, InertiaRoute, InertiaService, ManifestService, SsrRendererService, TemplateService };
1046
+ export { CookieFlashStore, DATA_SEO_ATTR, HandlePrecognitiveRequests, INERTIA_TOKENS, InertiaDelete, InertiaGet, InertiaMiddleware, InertiaModule, InertiaPatch, InertiaPost, InertiaPut, InertiaRoute, InertiaService, ManifestService, SeoService, SsrRendererService, TemplateService, buildSeoTags, descriptorToHtml };
833
1047
 
834
1048
  //# sourceMappingURL=index.mjs.map