@pyreon/zero 0.14.0 → 0.16.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.
Files changed (114) hide show
  1. package/lib/api-routes-Ci0kVmM4.js +146 -0
  2. package/lib/client.js +7 -2
  3. package/lib/csp.js +19 -9
  4. package/lib/env.js +6 -6
  5. package/lib/font.js +3 -3
  6. package/lib/{fs-router-CQ7Zxeca.js → fs-router-MewHc5SB.js} +56 -24
  7. package/lib/i18n-routing.js +112 -1
  8. package/lib/image-plugin.js +4 -0
  9. package/lib/image.js +141 -108
  10. package/lib/index.js +253 -132
  11. package/lib/link.js +1 -49
  12. package/lib/og-image.js +5 -5
  13. package/lib/rolldown-runtime-CjeV3_4I.js +18 -0
  14. package/lib/script.js +115 -74
  15. package/lib/seo.js +186 -15
  16. package/lib/server.js +275 -1247
  17. package/lib/theme.js +1 -50
  18. package/lib/types/config.d.ts +275 -3
  19. package/lib/types/env.d.ts +2 -2
  20. package/lib/types/i18n-routing.d.ts +197 -6
  21. package/lib/types/image.d.ts +105 -5
  22. package/lib/types/index.d.ts +640 -178
  23. package/lib/types/link.d.ts +3 -3
  24. package/lib/types/script.d.ts +78 -6
  25. package/lib/types/seo.d.ts +128 -4
  26. package/lib/types/server.d.ts +603 -77
  27. package/lib/types/theme.d.ts +2 -2
  28. package/lib/vite-plugin-xjWZwudX.js +2454 -0
  29. package/package.json +16 -13
  30. package/src/adapters/bun.ts +20 -1
  31. package/src/adapters/cloudflare.ts +78 -1
  32. package/src/adapters/index.ts +25 -3
  33. package/src/adapters/netlify.ts +63 -1
  34. package/src/adapters/node.ts +25 -1
  35. package/src/adapters/static.ts +26 -1
  36. package/src/adapters/validate.ts +8 -1
  37. package/src/adapters/vercel.ts +76 -1
  38. package/src/adapters/warn-missing-env.ts +49 -0
  39. package/src/app.ts +35 -1
  40. package/src/client.ts +18 -0
  41. package/src/csp.ts +28 -12
  42. package/src/entry-server.ts +55 -5
  43. package/src/env.ts +7 -7
  44. package/src/font.ts +3 -3
  45. package/src/fs-router.ts +123 -4
  46. package/src/i18n-routing.ts +246 -12
  47. package/src/image.tsx +242 -91
  48. package/src/index.ts +4 -4
  49. package/src/isr.ts +24 -6
  50. package/src/manifest.ts +675 -0
  51. package/src/og-image.ts +5 -5
  52. package/src/script.tsx +159 -36
  53. package/src/seo.ts +346 -15
  54. package/src/server.ts +10 -2
  55. package/src/ssg-plugin.ts +1523 -0
  56. package/src/types.ts +329 -19
  57. package/src/vercel-revalidate-handler.ts +204 -0
  58. package/src/vite-plugin.ts +326 -68
  59. package/lib/actions.js.map +0 -1
  60. package/lib/ai.js.map +0 -1
  61. package/lib/api-routes.js.map +0 -1
  62. package/lib/cache.js.map +0 -1
  63. package/lib/client.js.map +0 -1
  64. package/lib/compression.js.map +0 -1
  65. package/lib/config.js.map +0 -1
  66. package/lib/cors.js.map +0 -1
  67. package/lib/csp.js.map +0 -1
  68. package/lib/env.js.map +0 -1
  69. package/lib/favicon.js.map +0 -1
  70. package/lib/font.js.map +0 -1
  71. package/lib/fs-router-3xzp-4Wj.js.map +0 -1
  72. package/lib/fs-router-CQ7Zxeca.js.map +0 -1
  73. package/lib/i18n-routing.js.map +0 -1
  74. package/lib/image-plugin.js.map +0 -1
  75. package/lib/image.js.map +0 -1
  76. package/lib/index.js.map +0 -1
  77. package/lib/link.js.map +0 -1
  78. package/lib/logger.js.map +0 -1
  79. package/lib/meta.js.map +0 -1
  80. package/lib/middleware.js.map +0 -1
  81. package/lib/og-image.js.map +0 -1
  82. package/lib/rate-limit.js.map +0 -1
  83. package/lib/script.js.map +0 -1
  84. package/lib/seo.js.map +0 -1
  85. package/lib/server.js.map +0 -1
  86. package/lib/testing.js.map +0 -1
  87. package/lib/theme.js.map +0 -1
  88. package/lib/types/actions.d.ts.map +0 -1
  89. package/lib/types/ai.d.ts.map +0 -1
  90. package/lib/types/api-routes.d.ts.map +0 -1
  91. package/lib/types/cache.d.ts.map +0 -1
  92. package/lib/types/client.d.ts.map +0 -1
  93. package/lib/types/compression.d.ts.map +0 -1
  94. package/lib/types/config.d.ts.map +0 -1
  95. package/lib/types/cors.d.ts.map +0 -1
  96. package/lib/types/csp.d.ts.map +0 -1
  97. package/lib/types/env.d.ts.map +0 -1
  98. package/lib/types/favicon.d.ts.map +0 -1
  99. package/lib/types/font.d.ts.map +0 -1
  100. package/lib/types/i18n-routing.d.ts.map +0 -1
  101. package/lib/types/image-plugin.d.ts.map +0 -1
  102. package/lib/types/image.d.ts.map +0 -1
  103. package/lib/types/index.d.ts.map +0 -1
  104. package/lib/types/link.d.ts.map +0 -1
  105. package/lib/types/logger.d.ts.map +0 -1
  106. package/lib/types/meta.d.ts.map +0 -1
  107. package/lib/types/middleware.d.ts.map +0 -1
  108. package/lib/types/og-image.d.ts.map +0 -1
  109. package/lib/types/rate-limit.d.ts.map +0 -1
  110. package/lib/types/script.d.ts.map +0 -1
  111. package/lib/types/seo.d.ts.map +0 -1
  112. package/lib/types/server.d.ts.map +0 -1
  113. package/lib/types/testing.d.ts.map +0 -1
  114. package/lib/types/theme.d.ts.map +0 -1
package/lib/script.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { createRef, onMount, onUnmount } from "@pyreon/core";
2
+ import { signal } from "@pyreon/reactivity";
3
+ import { jsx } from "@pyreon/core/jsx-runtime";
2
4
 
3
5
  //#region src/utils/use-intersection-observer.ts
4
6
  /**
@@ -24,88 +26,70 @@ function useIntersectionObserver(getElement, onIntersect, rootMargin = "200px")
24
26
  });
25
27
  }
26
28
 
27
- //#endregion
28
- //#region ../../core/core/lib/jsx-runtime.js
29
- /**
30
- * Hyperscript function — the compiled output of JSX.
31
- * `<div class="x">hello</div>` → `h("div", { class: "x" }, "hello")`
32
- *
33
- * Generic on P so TypeScript validates props match the component's signature
34
- * at the call site, then stores the result in the loosely-typed VNode.
35
- */
36
- /** Shared empty props sentinel — identity-checked in mountElement to skip applyProps. */
37
- const EMPTY_PROPS = {};
38
- function h(type, props, ...children) {
39
- return {
40
- type,
41
- props: props ?? EMPTY_PROPS,
42
- children: normalizeChildren(children),
43
- key: props?.key ?? null
44
- };
45
- }
46
- function normalizeChildren(children) {
47
- for (let i = 0; i < children.length; i++) if (Array.isArray(children[i])) return flattenChildren(children);
48
- return children;
49
- }
50
- function flattenChildren(children) {
51
- const result = [];
52
- for (const child of children) if (Array.isArray(child)) result.push(...flattenChildren(child));
53
- else result.push(child);
54
- return result;
55
- }
56
- /**
57
- * JSX automatic runtime.
58
- *
59
- * When tsconfig has `"jsxImportSource": "@pyreon/core"`, the TS/bundler compiler
60
- * rewrites JSX to imports from this file automatically:
61
- * <div class="x" /> → jsx("div", { class: "x" })
62
- */
63
- function jsx(type, props, key) {
64
- const { children, ...rest } = props;
65
- const propsWithKey = key != null ? {
66
- ...rest,
67
- key
68
- } : rest;
69
- if (typeof type === "function") return h(type, children !== void 0 ? {
70
- ...propsWithKey,
71
- children
72
- } : propsWithKey);
73
- return h(type, propsWithKey, ...children === void 0 ? [] : Array.isArray(children) ? children : [children]);
74
- }
75
-
76
29
  //#endregion
77
30
  //#region src/script.tsx
78
31
  /**
79
- * Optimized script loading component.
32
+ * Composable that provides all script loading behavior — strategy state
33
+ * machine (afterHydration / onIdle / onInteraction / onViewport),
34
+ * deduplication, load/error tracking.
80
35
  *
81
- * @example
82
- * // Load analytics after page is interactive
83
- * <Script src="https://analytics.example.com/script.js" strategy="onIdle" />
36
+ * Returns reactive signals (`loaded`, `errored`, `pending`) so consumers
37
+ * can render loading indicators, retry buttons, or analytics-readiness
38
+ * gates without re-implementing the strategy machine.
84
39
  *
85
- * // Load chat widget when user scrolls
86
- * <Script src="/chat-widget.js" strategy="onViewport" />
87
- *
88
- * // Inline script with deferred execution
89
- * <Script strategy="afterHydration">
90
- * {`console.log("App hydrated!")`}
91
- * <\/Script>
40
+ * @example
41
+ * function MyScript(props: ScriptProps) {
42
+ * const s = useScript(props)
43
+ * return (
44
+ * <>
45
+ * {() => s.loaded() ? <Analytics /> : <Skeleton />}
46
+ * {() => s.needsSentinel && <div ref={s.sentinelRef} style="width:0;height:0" />}
47
+ * </>
48
+ * )
49
+ * }
92
50
  */
93
- function Script(props) {
51
+ function useScript(props) {
52
+ const strategy = props.strategy ?? "afterHydration";
53
+ const loaded = signal(false);
54
+ const errored = signal(false);
55
+ const pending = signal(strategy !== "beforeHydration" && strategy !== "afterHydration");
56
+ const sentinelRef = strategy === "onViewport" ? createRef() : void 0;
94
57
  function loadScript() {
95
58
  if (typeof document === "undefined") return;
96
- if (props.id && document.getElementById(props.id)) return;
59
+ if (props.id && document.getElementById(props.id)) {
60
+ loaded.set(true);
61
+ pending.set(false);
62
+ return;
63
+ }
97
64
  const script = document.createElement("script");
98
65
  if (props.src) script.src = props.src;
99
66
  if (props.id) script.id = props.id;
100
67
  script.async = props.async !== false;
101
- if (props.onLoad) script.onload = props.onLoad;
102
- if (props.onError) script.onerror = () => props.onError?.(/* @__PURE__ */ new Error(`Failed to load: ${props.src}`));
103
- if (props.children && !props.src) script.textContent = props.children;
68
+ script.onload = () => {
69
+ loaded.set(true);
70
+ pending.set(false);
71
+ props.onLoad?.();
72
+ };
73
+ script.onerror = () => {
74
+ errored.set(true);
75
+ pending.set(false);
76
+ props.onError?.(/* @__PURE__ */ new Error(`Failed to load: ${props.src}`));
77
+ };
78
+ if (props.children && !props.src) {
79
+ script.textContent = props.children;
80
+ setTimeout(() => {
81
+ loaded.set(true);
82
+ pending.set(false);
83
+ }, 0);
84
+ }
104
85
  document.head.appendChild(script);
105
86
  }
106
87
  onMount(() => {
107
- switch (props.strategy ?? "afterHydration") {
108
- case "beforeHydration": break;
88
+ switch (strategy) {
89
+ case "beforeHydration":
90
+ loaded.set(true);
91
+ pending.set(false);
92
+ break;
109
93
  case "afterHydration":
110
94
  loadScript();
111
95
  break;
@@ -136,16 +120,73 @@ function Script(props) {
136
120
  case "onViewport": break;
137
121
  }
138
122
  });
139
- const sentinelRef = createRef();
140
- const strategy = props.strategy ?? "afterHydration";
141
123
  if (strategy === "onViewport") useIntersectionObserver(() => sentinelRef.current ?? void 0, () => loadScript());
142
- if (strategy === "onViewport") return /* @__PURE__ */ jsx("div", {
143
- ref: sentinelRef,
124
+ return {
125
+ sentinelRef,
126
+ loaded,
127
+ errored,
128
+ pending,
129
+ needsSentinel: strategy === "onViewport",
130
+ load: loadScript
131
+ };
132
+ }
133
+ /**
134
+ * Higher-order component that wraps any component with script load behavior.
135
+ *
136
+ * The wrapped component receives {@link ScriptRenderProps} with the sentinel
137
+ * ref, load-state signals, and a `needsSentinel` flag. Use this when you want
138
+ * to render a loading indicator, retry button, or custom analytics-readiness
139
+ * gate around the script load.
140
+ *
141
+ * @example
142
+ * // Script with a loading indicator
143
+ * const TrackedScript = createScript((props) => (
144
+ * <>
145
+ * {() => props.pending() && <Spinner />}
146
+ * {() => props.errored() && <button onClick={() => location.reload()}>Retry</button>}
147
+ * {props.needsSentinel && <div ref={props.sentinelRef} style="width:0;height:0" />}
148
+ * </>
149
+ * ))
150
+ *
151
+ * <TrackedScript src="/analytics.js" strategy="onIdle" />
152
+ */
153
+ function createScript(Component) {
154
+ return function WrappedScript(props) {
155
+ const s = useScript(props);
156
+ return /* @__PURE__ */ jsx(Component, {
157
+ sentinelRef: s.sentinelRef,
158
+ needsSentinel: s.needsSentinel,
159
+ loaded: s.loaded,
160
+ errored: s.errored,
161
+ pending: s.pending
162
+ });
163
+ };
164
+ }
165
+ /**
166
+ * Default optimized script component. Renders a 0×0 sentinel `<div>` for the
167
+ * `onViewport` strategy (so IntersectionObserver has an element to observe),
168
+ * `null` for every other strategy.
169
+ *
170
+ * @example
171
+ * // Load analytics after page is interactive
172
+ * <Script src="https://analytics.example.com/script.js" strategy="onIdle" />
173
+ *
174
+ * // Load chat widget when user scrolls
175
+ * <Script src="/chat-widget.js" strategy="onViewport" />
176
+ *
177
+ * // Inline script with deferred execution
178
+ * <Script strategy="afterHydration">
179
+ * {`console.log("App hydrated!")`}
180
+ * <\/Script>
181
+ */
182
+ const Script = createScript((props) => {
183
+ if (!props.needsSentinel) return null;
184
+ return /* @__PURE__ */ jsx("div", {
185
+ ref: props.sentinelRef,
144
186
  style: "width:0;height:0;overflow:hidden"
145
187
  });
146
- return null;
147
- }
188
+ });
148
189
 
149
190
  //#endregion
150
- export { Script };
191
+ export { Script, createScript, useScript };
151
192
  //# sourceMappingURL=script.js.map
package/lib/seo.js CHANGED
@@ -1,12 +1,20 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile, rm, writeFile } from "node:fs/promises";
3
+ import { join, resolve } from "node:path";
4
+
1
5
  //#region src/seo.ts
2
6
  /**
3
7
  * Generate a sitemap.xml string from route file paths.
8
+ *
9
+ * When `i18n` is set (PR K — passed by `seoPlugin` after reading the
10
+ * i18n config from `zero({ i18n: ... })`), URLs are clustered by their
11
+ * un-prefixed (default-locale) form and each `<url>` carries
12
+ * `<xhtml:link rel="alternate" hreflang="...">` siblings for every
13
+ * locale variant + an `x-default` entry pointing at the default locale.
4
14
  */
5
- function generateSitemap(routeFiles, config) {
15
+ function generateSitemap(routeFiles, config, i18n) {
6
16
  const { origin, exclude = [], changefreq = "weekly", priority = .7 } = config;
7
- return `<?xml version="1.0" encoding="UTF-8"?>
8
- <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
9
- ${[...routeFiles.filter((f) => {
17
+ const clusters = clusterPathsByLocale([...routeFiles.filter((f) => {
10
18
  const name = f.split("/").pop()?.replace(/\.\w+$/, "");
11
19
  return name !== "_layout" && name !== "_error" && name !== "_loading";
12
20
  }).map((f) => {
@@ -19,19 +27,141 @@ ${[...routeFiles.filter((f) => {
19
27
  path: p,
20
28
  changefreq,
21
29
  priority
22
- })), ...config.additionalPaths ?? []].map((entry) => {
23
- return ` <url>
24
- <loc>${escapeXml(`${origin}${entry.path === "/" ? "" : entry.path}`)}</loc>
25
- <changefreq>${entry.changefreq ?? changefreq}</changefreq>
26
- <priority>${entry.priority ?? priority}</priority>${entry.lastmod ? `\n <lastmod>${entry.lastmod}</lastmod>` : ""}
27
- </url>`;
28
- }).join("\n")}
30
+ })), ...config.additionalPaths ?? []], i18n);
31
+ return `<?xml version="1.0" encoding="UTF-8"?>
32
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"${i18n != null && i18n.locales.length > 0 ? " xmlns:xhtml=\"http://www.w3.org/1999/xhtml\"" : ""}>
33
+ ${clusters.map((cluster) => renderClusterEntry(cluster, origin, changefreq, priority, i18n)).join("\n")}
29
34
  </urlset>`;
30
35
  }
36
+ /**
37
+ * Cluster URL entries by their un-prefixed (default-locale) form.
38
+ *
39
+ * Each output cluster has:
40
+ * - `canonical`: the SitemapEntry that should be used as the `<url>`
41
+ * payload (default-locale variant; falls back to the first variant
42
+ * if no default-locale entry exists in the cluster).
43
+ * - `variantsByLocale`: Map of locale → SitemapEntry for the cluster.
44
+ *
45
+ * Without i18n, every entry becomes its own single-variant cluster.
46
+ *
47
+ * @internal — exported for unit testing.
48
+ */
49
+ function clusterPathsByLocale(entries, i18n) {
50
+ if (i18n == null || i18n.locales.length === 0) return entries.map((entry) => ({
51
+ canonical: entry,
52
+ variantsByLocale: new Map([[null, entry]])
53
+ }));
54
+ const strategy = i18n.strategy ?? "prefix-except-default";
55
+ const { defaultLocale, locales } = i18n;
56
+ const byUnPrefixed = /* @__PURE__ */ new Map();
57
+ for (const entry of entries) {
58
+ const { unPrefixed, locale } = stripLocalePrefix(entry.path, locales, defaultLocale, strategy);
59
+ let cluster = byUnPrefixed.get(unPrefixed);
60
+ if (!cluster) {
61
+ cluster = /* @__PURE__ */ new Map();
62
+ byUnPrefixed.set(unPrefixed, cluster);
63
+ }
64
+ cluster.set(locale, entry);
65
+ }
66
+ const out = [];
67
+ for (const variantsByLocale of byUnPrefixed.values()) {
68
+ const canonical = variantsByLocale.get(defaultLocale) ?? variantsByLocale.get(null) ?? [...variantsByLocale.values()][0];
69
+ out.push({
70
+ canonical,
71
+ variantsByLocale
72
+ });
73
+ }
74
+ return out;
75
+ }
76
+ /**
77
+ * Strip the locale prefix from a path under the i18n strategy.
78
+ *
79
+ * Returns `{ unPrefixed, locale }`:
80
+ * - `/about` under `prefix-except-default` (default=en) → `{ unPrefixed: '/about', locale: 'en' }`
81
+ * - `/de/about` under either strategy → `{ unPrefixed: '/about', locale: 'de' }`
82
+ * - `/de` (locale root) → `{ unPrefixed: '/', locale: 'de' }`
83
+ * - `/about` under `prefix` → no locale match, returns `{ unPrefixed: '/about', locale: null }`
84
+ * (the URL doesn't fit any locale subtree — sitemap treats it as standalone).
85
+ *
86
+ * @internal — exported for unit testing.
87
+ */
88
+ function stripLocalePrefix(path, locales, defaultLocale, strategy) {
89
+ for (const locale of locales) {
90
+ if (path === `/${locale}`) return {
91
+ unPrefixed: "/",
92
+ locale
93
+ };
94
+ if (path.startsWith(`/${locale}/`)) return {
95
+ unPrefixed: path.slice(`/${locale}`.length),
96
+ locale
97
+ };
98
+ }
99
+ if (strategy === "prefix-except-default") return {
100
+ unPrefixed: path,
101
+ locale: defaultLocale
102
+ };
103
+ return {
104
+ unPrefixed: path,
105
+ locale: null
106
+ };
107
+ }
108
+ function renderClusterEntry(cluster, origin, changefreq, priority, i18n) {
109
+ const { canonical, variantsByLocale } = cluster;
110
+ const lines = [
111
+ " <url>",
112
+ ` <loc>${escapeXml(`${origin}${canonical.path === "/" ? "" : canonical.path}`)}</loc>`,
113
+ ` <changefreq>${canonical.changefreq ?? changefreq}</changefreq>`,
114
+ ` <priority>${canonical.priority ?? priority}</priority>`
115
+ ];
116
+ if (canonical.lastmod) lines.push(` <lastmod>${canonical.lastmod}</lastmod>`);
117
+ if (i18n != null && i18n.locales.length > 0 && variantsByLocale.size > 1) {
118
+ for (const locale of i18n.locales) {
119
+ const variant = variantsByLocale.get(locale);
120
+ if (!variant) continue;
121
+ const variantLoc = `${origin}${variant.path === "/" ? "" : variant.path}`;
122
+ lines.push(` <xhtml:link rel="alternate" hreflang="${escapeXml(locale)}" href="${escapeXml(variantLoc)}"/>`);
123
+ }
124
+ const defaultVariant = variantsByLocale.get(i18n.defaultLocale);
125
+ if (defaultVariant) {
126
+ const defaultLoc = `${origin}${defaultVariant.path === "/" ? "" : defaultVariant.path}`;
127
+ lines.push(` <xhtml:link rel="alternate" hreflang="x-default" href="${escapeXml(defaultLoc)}"/>`);
128
+ }
129
+ }
130
+ lines.push(" </url>");
131
+ return lines.join("\n");
132
+ }
31
133
  function escapeXml(str) {
32
134
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
33
135
  }
34
136
  /**
137
+ * Resolve the i18n config to feed `generateSitemap` for hreflang
138
+ * emission. Priority order:
139
+ * 1. Explicit user config — `hreflang: I18nRoutingConfig` (object)
140
+ * 2. Auto-detect from SSG manifest — `hreflang: true` + `manifestI18n`
141
+ * present (only happens in SSG mode where the manifest exists)
142
+ * 3. Nothing — emit plain sitemap without xhtml:link siblings
143
+ *
144
+ * @internal — exported for unit testing.
145
+ */
146
+ function resolveHreflangI18n(hreflang, manifestI18n) {
147
+ if (hreflang == null || hreflang === false) return void 0;
148
+ if (hreflang === true) return manifestI18n;
149
+ return hreflang;
150
+ }
151
+ /**
152
+ * Duck-type guard for `I18nRoutingConfig`. The SSG manifest is JSON,
153
+ * so the embedded i18n field could in principle be malformed if a
154
+ * downstream user hand-edits the manifest (don't). Validate the shape
155
+ * before trusting it.
156
+ *
157
+ * @internal
158
+ */
159
+ function isI18nRoutingConfig(value) {
160
+ if (value == null || typeof value !== "object") return false;
161
+ const v = value;
162
+ return Array.isArray(v.locales) && v.locales.every((l) => typeof l === "string") && typeof v.defaultLocale === "string";
163
+ }
164
+ /**
35
165
  * Generate a robots.txt string.
36
166
  */
37
167
  function generateRobots(config = {}) {
@@ -82,22 +212,33 @@ function jsonLd(data) {
82
212
  * pyreon(),
83
213
  * zero(),
84
214
  * seoPlugin({
85
- * sitemap: { origin: "https://example.com" },
215
+ * sitemap: {
216
+ * origin: "https://example.com",
217
+ * useSsgPaths: true, // include dynamic-route enumerations
218
+ * },
86
219
  * robots: { sitemap: "https://example.com/sitemap.xml" },
87
220
  * }),
88
221
  * ],
89
222
  * }
90
223
  */
91
224
  function seoPlugin(config = {}) {
225
+ const useSsgPaths = config.sitemap?.useSsgPaths === true;
226
+ let distDir = "";
92
227
  return {
93
228
  name: "pyreon-zero-seo",
94
229
  apply: "build",
230
+ ...useSsgPaths ? { enforce: "post" } : {},
231
+ configResolved(resolved) {
232
+ distDir = resolve(resolved.root, resolved.build.outDir);
233
+ },
95
234
  async generateBundle(_, _bundle) {
96
- if (config.sitemap) {
235
+ if (config.sitemap && !useSsgPaths) {
97
236
  const { scanRouteFiles } = await import("./fs-router-3xzp-4Wj.js");
98
237
  const routesDir = `${process.cwd()}/src/routes`;
99
238
  try {
100
- const sitemap = generateSitemap(await scanRouteFiles(routesDir), config.sitemap);
239
+ const files = await scanRouteFiles(routesDir);
240
+ const hreflangI18n = resolveHreflangI18n(config.sitemap.hreflang, void 0);
241
+ const sitemap = generateSitemap(files, config.sitemap, hreflangI18n);
101
242
  this.emitFile({
102
243
  type: "asset",
103
244
  fileName: "sitemap.xml",
@@ -113,6 +254,36 @@ function seoPlugin(config = {}) {
113
254
  source: robots
114
255
  });
115
256
  }
257
+ },
258
+ async closeBundle() {
259
+ if (!config.sitemap || !useSsgPaths) return;
260
+ const { scanRouteFiles } = await import("./fs-router-3xzp-4Wj.js");
261
+ const routesDir = `${process.cwd()}/src/routes`;
262
+ const manifestPath = join(distDir, "_pyreon-ssg-paths.json");
263
+ try {
264
+ let ssgPaths = [];
265
+ let manifestI18n;
266
+ if (existsSync(manifestPath)) {
267
+ const raw = await readFile(manifestPath, "utf-8");
268
+ const parsed = JSON.parse(raw);
269
+ if (Array.isArray(parsed.paths)) ssgPaths = parsed.paths.filter((p) => typeof p === "string").map((path) => ({ path }));
270
+ if (isI18nRoutingConfig(parsed.i18n)) manifestI18n = parsed.i18n;
271
+ try {
272
+ await rm(manifestPath, { force: true });
273
+ } catch {}
274
+ }
275
+ let files = [];
276
+ try {
277
+ files = await scanRouteFiles(routesDir);
278
+ } catch {}
279
+ const merged = {
280
+ ...config.sitemap,
281
+ additionalPaths: [...ssgPaths, ...config.sitemap.additionalPaths ?? []]
282
+ };
283
+ const hreflangI18n = resolveHreflangI18n(config.sitemap.hreflang, manifestI18n);
284
+ const sitemap = generateSitemap(files, merged, hreflangI18n);
285
+ await writeFile(join(distDir, "sitemap.xml"), sitemap, "utf-8");
286
+ } catch {}
116
287
  }
117
288
  };
118
289
  }
@@ -132,5 +303,5 @@ function seoMiddleware(config = {}) {
132
303
  }
133
304
 
134
305
  //#endregion
135
- export { generateRobots, generateSitemap, jsonLd, seoMiddleware, seoPlugin };
306
+ export { clusterPathsByLocale, generateRobots, generateSitemap, jsonLd, resolveHreflangI18n, seoMiddleware, seoPlugin, stripLocalePrefix };
136
307
  //# sourceMappingURL=seo.js.map