@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.
- package/lib/api-routes-Ci0kVmM4.js +146 -0
- package/lib/client.js +7 -2
- package/lib/csp.js +19 -9
- package/lib/env.js +6 -6
- package/lib/font.js +3 -3
- package/lib/{fs-router-CQ7Zxeca.js → fs-router-MewHc5SB.js} +56 -24
- package/lib/i18n-routing.js +112 -1
- package/lib/image-plugin.js +4 -0
- package/lib/image.js +141 -108
- package/lib/index.js +253 -132
- package/lib/link.js +1 -49
- package/lib/og-image.js +5 -5
- package/lib/rolldown-runtime-CjeV3_4I.js +18 -0
- package/lib/script.js +115 -74
- package/lib/seo.js +186 -15
- package/lib/server.js +275 -1247
- package/lib/theme.js +1 -50
- package/lib/types/config.d.ts +275 -3
- package/lib/types/env.d.ts +2 -2
- package/lib/types/i18n-routing.d.ts +197 -6
- package/lib/types/image.d.ts +105 -5
- package/lib/types/index.d.ts +640 -178
- package/lib/types/link.d.ts +3 -3
- package/lib/types/script.d.ts +78 -6
- package/lib/types/seo.d.ts +128 -4
- package/lib/types/server.d.ts +603 -77
- package/lib/types/theme.d.ts +2 -2
- package/lib/vite-plugin-xjWZwudX.js +2454 -0
- package/package.json +16 -13
- package/src/adapters/bun.ts +20 -1
- package/src/adapters/cloudflare.ts +78 -1
- package/src/adapters/index.ts +25 -3
- package/src/adapters/netlify.ts +63 -1
- package/src/adapters/node.ts +25 -1
- package/src/adapters/static.ts +26 -1
- package/src/adapters/validate.ts +8 -1
- package/src/adapters/vercel.ts +76 -1
- package/src/adapters/warn-missing-env.ts +49 -0
- package/src/app.ts +35 -1
- package/src/client.ts +18 -0
- package/src/csp.ts +28 -12
- package/src/entry-server.ts +55 -5
- package/src/env.ts +7 -7
- package/src/font.ts +3 -3
- package/src/fs-router.ts +123 -4
- package/src/i18n-routing.ts +246 -12
- package/src/image.tsx +242 -91
- package/src/index.ts +4 -4
- package/src/isr.ts +24 -6
- package/src/manifest.ts +675 -0
- package/src/og-image.ts +5 -5
- package/src/script.tsx +159 -36
- package/src/seo.ts +346 -15
- package/src/server.ts +10 -2
- package/src/ssg-plugin.ts +1523 -0
- package/src/types.ts +329 -19
- package/src/vercel-revalidate-handler.ts +204 -0
- package/src/vite-plugin.ts +326 -68
- package/lib/actions.js.map +0 -1
- package/lib/ai.js.map +0 -1
- package/lib/api-routes.js.map +0 -1
- package/lib/cache.js.map +0 -1
- package/lib/client.js.map +0 -1
- package/lib/compression.js.map +0 -1
- package/lib/config.js.map +0 -1
- package/lib/cors.js.map +0 -1
- package/lib/csp.js.map +0 -1
- package/lib/env.js.map +0 -1
- package/lib/favicon.js.map +0 -1
- package/lib/font.js.map +0 -1
- package/lib/fs-router-3xzp-4Wj.js.map +0 -1
- package/lib/fs-router-CQ7Zxeca.js.map +0 -1
- package/lib/i18n-routing.js.map +0 -1
- package/lib/image-plugin.js.map +0 -1
- package/lib/image.js.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/link.js.map +0 -1
- package/lib/logger.js.map +0 -1
- package/lib/meta.js.map +0 -1
- package/lib/middleware.js.map +0 -1
- package/lib/og-image.js.map +0 -1
- package/lib/rate-limit.js.map +0 -1
- package/lib/script.js.map +0 -1
- package/lib/seo.js.map +0 -1
- package/lib/server.js.map +0 -1
- package/lib/testing.js.map +0 -1
- package/lib/theme.js.map +0 -1
- package/lib/types/actions.d.ts.map +0 -1
- package/lib/types/ai.d.ts.map +0 -1
- package/lib/types/api-routes.d.ts.map +0 -1
- package/lib/types/cache.d.ts.map +0 -1
- package/lib/types/client.d.ts.map +0 -1
- package/lib/types/compression.d.ts.map +0 -1
- package/lib/types/config.d.ts.map +0 -1
- package/lib/types/cors.d.ts.map +0 -1
- package/lib/types/csp.d.ts.map +0 -1
- package/lib/types/env.d.ts.map +0 -1
- package/lib/types/favicon.d.ts.map +0 -1
- package/lib/types/font.d.ts.map +0 -1
- package/lib/types/i18n-routing.d.ts.map +0 -1
- package/lib/types/image-plugin.d.ts.map +0 -1
- package/lib/types/image.d.ts.map +0 -1
- package/lib/types/index.d.ts.map +0 -1
- package/lib/types/link.d.ts.map +0 -1
- package/lib/types/logger.d.ts.map +0 -1
- package/lib/types/meta.d.ts.map +0 -1
- package/lib/types/middleware.d.ts.map +0 -1
- package/lib/types/og-image.d.ts.map +0 -1
- package/lib/types/rate-limit.d.ts.map +0 -1
- package/lib/types/script.d.ts.map +0 -1
- package/lib/types/seo.d.ts.map +0 -1
- package/lib/types/server.d.ts.map +0 -1
- package/lib/types/testing.d.ts.map +0 -1
- 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
|
-
*
|
|
32
|
+
* Composable that provides all script loading behavior — strategy state
|
|
33
|
+
* machine (afterHydration / onIdle / onInteraction / onViewport),
|
|
34
|
+
* deduplication, load/error tracking.
|
|
80
35
|
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
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
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
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
|
|
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))
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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 (
|
|
108
|
-
case "beforeHydration":
|
|
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
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ?? []]
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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: {
|
|
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
|
|
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
|