@marktiderman/genesis-sites 0.0.0-stage → 0.1.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.
@@ -0,0 +1,499 @@
1
+ import { z } from 'zod';
2
+
3
+ // src/core/urls.ts
4
+ var MAX_URL_LENGTH = 2048;
5
+ var SAFE_SCHEMES = /* @__PURE__ */ new Set(["https:", "http:", "mailto:", "tel:"]);
6
+ var UNSAFE_CHARS = /[\u0000-\u001f\u007f\\\s]/;
7
+ function isSafeHref(value) {
8
+ if (typeof value !== "string" || value.length === 0 || value.length > MAX_URL_LENGTH) return false;
9
+ if (UNSAFE_CHARS.test(value)) return false;
10
+ if (value.startsWith("#")) return true;
11
+ if (value.startsWith("/")) return !value.startsWith("//");
12
+ let url;
13
+ try {
14
+ url = new URL(value);
15
+ } catch {
16
+ return false;
17
+ }
18
+ if (!SAFE_SCHEMES.has(url.protocol)) return false;
19
+ if (url.protocol === "http:" || url.protocol === "https:") return url.hostname.length > 0 && !url.username && !url.password;
20
+ return true;
21
+ }
22
+ function isSafeImageSrc(value) {
23
+ if (!isSafeHref(value)) return false;
24
+ if (value.startsWith("/")) return true;
25
+ return value.startsWith("https://");
26
+ }
27
+ function isExternalHref(href) {
28
+ return /^https?:\/\//i.test(href);
29
+ }
30
+ var PAGE_PATH = /^\/(?:[a-z0-9]+(?:-[a-z0-9]+)*(?:\/[a-z0-9]+(?:-[a-z0-9]+)*){0,7})?$/;
31
+ var MAX_PAGE_PATH_LENGTH = 200;
32
+ var DEFAULT_RESERVED_PATHS = Object.freeze(["/api", "/_server", "/auth", "/_gs"]);
33
+ function isReservedPath(path, reserved = DEFAULT_RESERVED_PATHS) {
34
+ if (typeof reserved === "function") return reserved(path);
35
+ return reserved.some((prefix) => {
36
+ const p = prefix.endsWith("/") && prefix.length > 1 ? prefix.slice(0, -1) : prefix;
37
+ return path === p || path.startsWith(`${p}/`);
38
+ });
39
+ }
40
+ function pagePathProblems(path, reserved = DEFAULT_RESERVED_PATHS) {
41
+ if (typeof path !== "string") return ["A page path is text."];
42
+ const problems = [];
43
+ if (path.length > MAX_PAGE_PATH_LENGTH) problems.push(`A page path has at most ${MAX_PAGE_PATH_LENGTH} characters.`);
44
+ if (!PAGE_PATH.test(path)) {
45
+ problems.push('A page path starts with "/" and uses lowercase letters, digits and single hyphens, with no trailing slash (for example "/about").');
46
+ }
47
+ if (isReservedPath(path, reserved)) problems.push(`"${path}" is reserved for the app this site shares its address with.`);
48
+ return problems;
49
+ }
50
+ function validatePagePath(path, reserved = DEFAULT_RESERVED_PATHS) {
51
+ const problems = pagePathProblems(path, reserved);
52
+ if (problems.length) throw new Error(problems.join(" "));
53
+ return path;
54
+ }
55
+ function normalizeRequestPath(pathname) {
56
+ let path;
57
+ try {
58
+ path = decodeURIComponent(pathname);
59
+ } catch {
60
+ return null;
61
+ }
62
+ if (UNSAFE_CHARS.test(path) || path.includes("//")) return null;
63
+ if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1);
64
+ return path.toLowerCase();
65
+ }
66
+ if (typeof z.toJSONSchema !== "function") {
67
+ throw new Error("@marktiderman/genesis-sites needs zod 4 (zod@^4.0.0): z.toJSONSchema is missing.");
68
+ }
69
+ var Href = z.string().max(MAX_URL_LENGTH).refine(isSafeHref, { message: "Use a site path (/about), a #section, or an https, mailto or tel link." }).meta({ title: "Link", format: "uri-reference" });
70
+ var Link = z.object({
71
+ label: z.string().trim().min(1).max(80),
72
+ href: Href,
73
+ /** Opens in a new tab (external links only). */
74
+ newTab: z.boolean().optional()
75
+ }).meta({ title: "Link" });
76
+ var Image = z.object({
77
+ src: z.string().max(MAX_URL_LENGTH).refine(isSafeImageSrc, { message: "An image loads from a site path or an https URL." }),
78
+ alt: z.string().max(300).default(""),
79
+ decorative: z.boolean().optional(),
80
+ width: z.number().int().positive().max(1e4).optional(),
81
+ height: z.number().int().positive().max(1e4).optional(),
82
+ /** Where the subject is, for cropping: 0–100 from the left and top. */
83
+ focal: z.object({ x: z.number().min(0).max(100), y: z.number().min(0).max(100) }).optional()
84
+ }).meta({ title: "Image", "x-genesis-kind": "image" });
85
+ var RichText = z.object({
86
+ type: z.literal("doc"),
87
+ content: z.array(z.record(z.string(), z.unknown())).max(2e3).optional()
88
+ }).meta({ title: "Rich text", "x-genesis-kind": "rich-text" });
89
+ var Surface = z.enum(["base", "muted", "accent", "inverse"]).meta({ title: "Background" });
90
+ var Line = (max = 120) => z.string().trim().min(1).max(max);
91
+ var Copy = (max = 600) => z.string().trim().max(max);
92
+ var SectionId = z.string().regex(/^[A-Za-z0-9_-]{1,64}$/, "A section id is 1\u201364 letters, digits, - or _.");
93
+ var SectionNode = z.object({
94
+ id: SectionId,
95
+ /** The section's registry key, such as `hero`. */
96
+ type: z.string().regex(/^[a-z][a-z0-9-]{0,63}$/),
97
+ /** The props version the props were written at; `migrateNode` brings them up to date. */
98
+ v: z.number().int().min(1),
99
+ variant: z.string().max(64).optional(),
100
+ /** Kept in the page, not rendered (Shopify's `disabled`). */
101
+ hidden: z.boolean().optional(),
102
+ /** Hidden on phones or on wider screens. */
103
+ hideOn: z.array(z.enum(["mobile", "desktop"])).max(2).optional(),
104
+ /**
105
+ * Set by a template on content someone must replace (a sample quote). The
106
+ * publish check refuses a visible section that still has it; the editor
107
+ * clears it once the section is edited.
108
+ */
109
+ placeholder: z.boolean().optional(),
110
+ props: z.record(z.string(), z.unknown())
111
+ });
112
+ var Seo = z.object({
113
+ title: z.string().trim().max(70).optional(),
114
+ description: z.string().trim().max(200).optional(),
115
+ ogImage: Image.optional(),
116
+ /** An absolute canonical URL, when this page's content lives elsewhere. */
117
+ canonical: z.url({ protocol: /^https?$/ }).max(MAX_URL_LENGTH).optional(),
118
+ noindex: z.boolean().optional()
119
+ });
120
+ var PageDoc = z.object({
121
+ schemaVersion: z.literal(1),
122
+ root: z.object({
123
+ props: z.looseObject({
124
+ title: z.string().trim().min(1).max(120),
125
+ /** Leave out the site's header and footer (a focused landing page). */
126
+ hideSiteChrome: z.boolean().optional()
127
+ })
128
+ }),
129
+ content: z.array(SectionNode).max(60),
130
+ seo: Seo.optional()
131
+ });
132
+ var PageKind = z.enum(["page", "landing"]);
133
+ var PagePath = z.string().max(200).regex(PAGE_PATH, 'A page path looks like "/" or "/about".');
134
+ var MenuItem = z.object({
135
+ label: z.string().trim().min(1).max(40),
136
+ href: Href
137
+ });
138
+ var Redirect = z.object({
139
+ from: PagePath,
140
+ to: Href,
141
+ status: z.union([z.literal(301), z.literal(302)])
142
+ });
143
+ var FontChoice = z.enum([
144
+ "system",
145
+ "inter",
146
+ "dm-sans",
147
+ "work-sans",
148
+ "montserrat",
149
+ "poppins",
150
+ "space-grotesk",
151
+ "source-serif",
152
+ "merriweather",
153
+ "playfair-display",
154
+ "lora"
155
+ ]);
156
+ var TokenOverrides = z.object({
157
+ radius: z.enum(["none", "sm", "md", "lg", "full"]).optional(),
158
+ headingFont: FontChoice.optional(),
159
+ bodyFont: FontChoice.optional()
160
+ });
161
+ var SeoDefaults = z.object({
162
+ /** `%s` is the page's title: `"%s · Riverside Gym"`. */
163
+ titleTemplate: z.string().max(80).optional(),
164
+ description: z.string().trim().max(200).optional(),
165
+ ogImage: Image.optional(),
166
+ /** Organization facts for JSON-LD. */
167
+ organization: z.object({
168
+ name: z.string().trim().min(1).max(120),
169
+ logo: z.string().max(MAX_URL_LENGTH).refine(isSafeImageSrc).optional(),
170
+ sameAs: z.array(z.url({ protocol: /^https$/ }).max(MAX_URL_LENGTH)).max(10).optional()
171
+ }).optional(),
172
+ noindex: z.boolean().optional()
173
+ });
174
+ var SiteDoc = z.object({
175
+ schemaVersion: z.literal(1),
176
+ name: z.string().trim().min(1).max(120),
177
+ /** BCP 47 language of the site's pages. */
178
+ lang: z.string().regex(/^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/).default("en"),
179
+ theme: z.object({
180
+ colorScheme: z.enum(["light", "dark", "auto"]).default("light"),
181
+ overrides: TokenOverrides.optional()
182
+ }).default({ colorScheme: "light" }),
183
+ /** The site's header: usually one `nav` section. */
184
+ header: z.array(SectionNode).max(2).default([]),
185
+ /** The header's menu. */
186
+ nav: z.array(MenuItem).max(12).default([]),
187
+ /** The site's footer: usually one `footer` section. */
188
+ footer: z.array(SectionNode).max(3).default([]),
189
+ seo: SeoDefaults.default({}),
190
+ redirects: z.array(Redirect).max(500).default([])
191
+ });
192
+ var ReleasePage = z.object({
193
+ id: z.string().min(1),
194
+ path: PagePath,
195
+ kind: PageKind,
196
+ doc: PageDoc
197
+ });
198
+ var ReleaseSnapshot = z.object({
199
+ schemaVersion: z.literal(1),
200
+ site: SiteDoc,
201
+ pages: z.array(ReleasePage).max(500)
202
+ });
203
+ function parseSnapshot(value, report = (m) => console.error(`[genesis-sites] ${m}`)) {
204
+ const raw = value && typeof value === "object" ? value : {};
205
+ if (raw.schemaVersion !== 1) {
206
+ report("the snapshot isn't schemaVersion 1.");
207
+ return null;
208
+ }
209
+ const site = SiteDoc.safeParse(raw.site);
210
+ if (!site.success) {
211
+ report(`the snapshot's site document doesn't parse: ${site.error.message}`);
212
+ return null;
213
+ }
214
+ const pages = [];
215
+ for (const page of Array.isArray(raw.pages) ? raw.pages.slice(0, 500) : []) {
216
+ const parsed = ReleasePage.safeParse(page);
217
+ if (parsed.success) pages.push(parsed.data);
218
+ else report(`the page ${JSON.stringify(page?.path ?? "?")} doesn't parse and is left out: ${parsed.error.message}`);
219
+ }
220
+ return { schemaVersion: 1, site: site.data, pages };
221
+ }
222
+ var MAX_DOC_BYTES = 512 * 1024;
223
+ var SECTION_CATEGORIES = ["header", "intro", "content", "social-proof", "conversion", "footer"];
224
+ var SectionDefinitionError = class extends Error {
225
+ constructor(problems) {
226
+ super(`Invalid section definition:
227
+ ${problems.map((p) => ` - ${p}`).join("\n")}`);
228
+ this.problems = problems;
229
+ this.name = "SectionDefinitionError";
230
+ }
231
+ problems;
232
+ };
233
+ var TYPE = /^[a-z][a-z0-9-]{0,63}$/;
234
+ function definitionProblems(def) {
235
+ const problems = [];
236
+ if (!def || typeof def !== "object") return ["a section definition is an object"];
237
+ const at = typeof def.type === "string" ? `"${def.type}"` : "a section";
238
+ if (typeof def.type !== "string" || !TYPE.test(def.type)) problems.push(`${at}: \`type\` is lowercase letters, digits and -`);
239
+ if (!Number.isInteger(def.version) || def.version < 1) problems.push(`${at}: \`version\` is a whole number from 1`);
240
+ if (typeof def.label !== "string" || !def.label.trim()) problems.push(`${at}: needs a \`label\``);
241
+ if (!SECTION_CATEGORIES.includes(def.category)) problems.push(`${at}: \`category\` is one of ${SECTION_CATEGORIES.join(", ")}`);
242
+ if (!def.schema || typeof def.schema.safeParse !== "function") problems.push(`${at}: \`schema\` is a zod schema`);
243
+ if (typeof def.render !== "function") problems.push(`${at}: \`render\` is a function`);
244
+ const ids = /* @__PURE__ */ new Set();
245
+ if (!Array.isArray(def.variants) || def.variants.length === 0) problems.push(`${at}: needs at least one variant`);
246
+ else
247
+ for (const v of def.variants) {
248
+ if (!v || typeof v.id !== "string" || !/^[a-z][a-z0-9-]{0,63}$/.test(v.id) || typeof v.label !== "string") problems.push(`${at}: a variant is { id, label }`);
249
+ else if (ids.has(v.id)) problems.push(`${at}: variant "${v.id}" is listed twice`);
250
+ else ids.add(v.id);
251
+ }
252
+ if (!ids.has(def.defaultVariant)) problems.push(`${at}: \`defaultVariant\` "${String(def.defaultVariant)}" is not one of its variants`);
253
+ if (def.limit !== void 0 && (!Number.isInteger(def.limit) || def.limit < 1)) problems.push(`${at}: \`limit\` is a whole number from 1`);
254
+ if (def.placement !== void 0 && !["header", "body", "footer"].includes(def.placement)) problems.push(`${at}: \`placement\` is header, body or footer`);
255
+ for (const [key, fn] of Object.entries(def.migrations ?? {})) {
256
+ const n = Number(key);
257
+ if (!Number.isInteger(n) || n < 2 || n > def.version) problems.push(`${at}: migration ${key} must be between 2 and the version (${def.version})`);
258
+ if (typeof fn !== "function") problems.push(`${at}: migration ${key} is a function`);
259
+ }
260
+ for (let n = 2; n <= (Number.isInteger(def.version) ? def.version : 0); n++) {
261
+ if (!def.migrations?.[n]) problems.push(`${at}: version ${def.version} needs a migration to ${n}`);
262
+ }
263
+ if (!Array.isArray(def.presets) || def.presets.length === 0) problems.push(`${at}: needs at least one preset (the editor's Add picker)`);
264
+ else if (def.schema && typeof def.schema.safeParse === "function")
265
+ for (const preset of def.presets) {
266
+ if (!preset || typeof preset.name !== "string" || !ids.has(preset.variant)) {
267
+ problems.push(`${at}: preset ${JSON.stringify(preset?.name)} needs a name and one of its variants`);
268
+ continue;
269
+ }
270
+ const parsed = def.schema.safeParse(preset.props);
271
+ if (!parsed.success) problems.push(`${at}: preset "${preset.name}" doesn't match the schema: ${parsed.error.issues.map((i) => `${i.path.join(".") || "props"} ${i.message}`).join("; ")}`);
272
+ }
273
+ return problems;
274
+ }
275
+ function defineSection(def) {
276
+ const problems = definitionProblems(def);
277
+ if (problems.length) throw new SectionDefinitionError(problems);
278
+ return Object.freeze({
279
+ ...def,
280
+ variants: Object.freeze(def.variants.map((v) => Object.freeze({ ...v }))),
281
+ presets: Object.freeze(def.presets.map((p) => Object.freeze({ ...p }))),
282
+ ...def.enabledOn ? { enabledOn: Object.freeze([...def.enabledOn]) } : {},
283
+ ...def.migrations ? { migrations: Object.freeze({ ...def.migrations }) } : {}
284
+ });
285
+ }
286
+ var REGISTRY = /* @__PURE__ */ Symbol.for("@marktiderman/genesis-sites/sections@1");
287
+ var BUILT_IN = /* @__PURE__ */ Symbol.for("@marktiderman/genesis-sites/built-in-sections@1");
288
+ var scope = globalThis;
289
+ var registry = scope[REGISTRY] ??= /* @__PURE__ */ new Map();
290
+ var builtIns = scope[BUILT_IN] ??= /* @__PURE__ */ new Set();
291
+ var BUILT_IN_SECTION_TYPES = Object.freeze([
292
+ "nav",
293
+ "hero",
294
+ "features",
295
+ "cta",
296
+ "testimonials",
297
+ "faq",
298
+ "pricing",
299
+ "logos",
300
+ "stats",
301
+ "gallery",
302
+ "rich-text",
303
+ "form-embed",
304
+ "footer"
305
+ ]);
306
+ function registerSection(def, options = {}) {
307
+ const frozen = Object.isFrozen(def) && definitionProblems(def).length === 0 ? def : defineSection(def);
308
+ if (BUILT_IN_SECTION_TYPES.includes(frozen.type) && options.replace !== true) {
309
+ throw new Error(
310
+ `registerSection: "${frozen.type}" is a built-in section; pass { replace: true } to replace it for every site in this process.`
311
+ );
312
+ }
313
+ registry.set(frozen.type, frozen);
314
+ }
315
+ function registerBuiltInSection(def) {
316
+ if (!registry.has(def.type)) registry.set(def.type, def);
317
+ builtIns.add(def.type);
318
+ }
319
+ function getSection(type) {
320
+ return registry.get(type);
321
+ }
322
+ function listSections() {
323
+ return [...registry.values()];
324
+ }
325
+ function unregisterSection(type) {
326
+ if (BUILT_IN_SECTION_TYPES.includes(type)) throw new Error(`unregisterSection: "${type}" is built in; replace it with registerSection(\u2026, { replace: true }).`);
327
+ return registry.delete(type);
328
+ }
329
+ var jsonSchemas = /* @__PURE__ */ new WeakMap();
330
+ function sectionJsonSchema(typeOrDef) {
331
+ const def = typeof typeOrDef === "string" ? registry.get(typeOrDef) : typeOrDef;
332
+ if (!def) throw new Error(`sectionJsonSchema: no section "${String(typeOrDef)}" is registered.`);
333
+ let schema = jsonSchemas.get(def.schema);
334
+ if (!schema) {
335
+ schema = z.toJSONSchema(def.schema, { io: "input", unrepresentable: "any" });
336
+ jsonSchemas.set(def.schema, schema);
337
+ }
338
+ return schema;
339
+ }
340
+ function sectionManifests() {
341
+ return listSections().map((def) => ({
342
+ type: def.type,
343
+ version: def.version,
344
+ label: def.label,
345
+ ...def.description ? { description: def.description } : {},
346
+ category: def.category,
347
+ placement: def.placement ?? "body",
348
+ variants: def.variants,
349
+ defaultVariant: def.defaultVariant,
350
+ presets: def.presets,
351
+ ...def.limit ? { limit: def.limit } : {},
352
+ ...def.enabledOn ? { enabledOn: def.enabledOn } : {},
353
+ jsonSchema: sectionJsonSchema(def)
354
+ }));
355
+ }
356
+ var counter = 0;
357
+ function newSectionId(prefix = "s") {
358
+ counter = (counter + 1) % 1e6;
359
+ const random = Math.random().toString(36).slice(2, 8);
360
+ return `${prefix}${Date.now().toString(36)}${counter.toString(36)}${random}`.slice(0, 64);
361
+ }
362
+ function nodeFromPreset(type, presetName, id = newSectionId(type.replace(/-/g, "").slice(0, 6))) {
363
+ const def = registry.get(type);
364
+ if (!def) throw new Error(`nodeFromPreset: no section "${type}" is registered.`);
365
+ const preset = presetName ? def.presets.find((p) => p.name === presetName) : def.presets[0];
366
+ if (!preset) throw new Error(`nodeFromPreset: "${type}" has no preset "${presetName}".`);
367
+ return { id, type, v: def.version, variant: preset.variant, props: structuredClone(preset.props) };
368
+ }
369
+
370
+ // src/core/seo.ts
371
+ function absoluteUrl(origin, basePath, path) {
372
+ const base = origin.replace(/\/+$/, "") + (basePath ?? "").replace(/\/+$/, "");
373
+ return path === "/" ? `${base}/` : `${base}${path}`;
374
+ }
375
+ function imageUrl(image, origin, basePath) {
376
+ if (!image) return void 0;
377
+ return image.src.startsWith("/") ? absoluteUrl(origin, basePath, image.src) : image.src;
378
+ }
379
+ function pageTitle(site, doc, _path) {
380
+ const own = doc.seo?.title?.trim();
381
+ if (own) return own;
382
+ const title = doc.root.props.title;
383
+ const template = site.seo.titleTemplate;
384
+ if (!template || !template.includes("%s")) return title;
385
+ return template.replace("%s", title);
386
+ }
387
+ function pageMeta({ site, page, origin, basePath = "", preview = false }) {
388
+ const doc = page.doc;
389
+ const title = pageTitle(site, doc, page.path);
390
+ const description = doc.seo?.description?.trim() || site.seo.description?.trim() || void 0;
391
+ const canonical = doc.seo?.canonical ?? absoluteUrl(origin, basePath, page.path);
392
+ const noindex = preview || site.seo.noindex === true || doc.seo?.noindex === true;
393
+ const image = doc.seo?.ogImage ?? site.seo.ogImage;
394
+ const img = imageUrl(image, origin, basePath);
395
+ const openGraph = {
396
+ "og:type": "website",
397
+ "og:title": title,
398
+ "og:url": canonical,
399
+ "og:site_name": site.name,
400
+ "og:locale": site.lang.replace("-", "_")
401
+ };
402
+ if (description) openGraph["og:description"] = description;
403
+ if (img) {
404
+ openGraph["og:image"] = img;
405
+ if (image?.alt) openGraph["og:image:alt"] = image.alt;
406
+ if (image?.width) openGraph["og:image:width"] = String(image.width);
407
+ if (image?.height) openGraph["og:image:height"] = String(image.height);
408
+ }
409
+ const twitter = { "twitter:card": img ? "summary_large_image" : "summary", "twitter:title": title };
410
+ if (description) twitter["twitter:description"] = description;
411
+ if (img) twitter["twitter:image"] = img;
412
+ return {
413
+ title,
414
+ ...description ? { description } : {},
415
+ canonical,
416
+ robots: noindex ? "noindex, nofollow" : "index, follow",
417
+ openGraph,
418
+ twitter
419
+ };
420
+ }
421
+ function organizationJsonLd(site, origin, basePath = "") {
422
+ const org = site.seo.organization;
423
+ const out = {
424
+ "@context": "https://schema.org",
425
+ "@type": "Organization",
426
+ name: org?.name ?? site.name,
427
+ url: absoluteUrl(origin, basePath, "/")
428
+ };
429
+ if (org?.logo) out.logo = org.logo.startsWith("/") ? absoluteUrl(origin, basePath, org.logo) : org.logo;
430
+ if (org?.sameAs?.length) out.sameAs = org.sameAs;
431
+ return out;
432
+ }
433
+ function websiteJsonLd(site, origin, basePath = "") {
434
+ return {
435
+ "@context": "https://schema.org",
436
+ "@type": "WebSite",
437
+ name: site.name,
438
+ url: absoluteUrl(origin, basePath, "/"),
439
+ inLanguage: site.lang
440
+ };
441
+ }
442
+ function breadcrumbJsonLd(pages, page, origin, basePath = "") {
443
+ if (page.path === "/") return null;
444
+ const byPath = new Map(pages.map((p) => [p.path, p]));
445
+ const trail = [];
446
+ const home = byPath.get("/");
447
+ if (home) trail.push(home);
448
+ const segments = page.path.split("/").filter(Boolean);
449
+ for (let i = 1; i <= segments.length; i++) {
450
+ const ancestor = byPath.get(`/${segments.slice(0, i).join("/")}`);
451
+ if (ancestor) trail.push(ancestor);
452
+ }
453
+ if (trail.length < 2) return null;
454
+ return {
455
+ "@context": "https://schema.org",
456
+ "@type": "BreadcrumbList",
457
+ itemListElement: trail.map((p, i) => ({
458
+ "@type": "ListItem",
459
+ position: i + 1,
460
+ name: p.doc.root.props.title,
461
+ item: absoluteUrl(origin, basePath, p.path)
462
+ }))
463
+ };
464
+ }
465
+ function safeJsonForHtml(value) {
466
+ return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
467
+ }
468
+ function escapeXml(text) {
469
+ return text.replace(/[<>&'"]/g, (c) => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;", "'": "&apos;", '"': "&quot;" })[c]);
470
+ }
471
+ function sitemapXml({ site, pages, origin, basePath = "", lastModified }) {
472
+ const urls = site.seo.noindex ? [] : pages.filter((p) => !p.doc.seo?.noindex && !p.doc.seo?.canonical).sort((a, b) => a.path === "/" ? -1 : b.path === "/" ? 1 : a.path.localeCompare(b.path)).map((p) => {
473
+ const loc = `<loc>${escapeXml(absoluteUrl(origin, basePath, p.path))}</loc>`;
474
+ const mod = lastModified ? `<lastmod>${escapeXml(new Date(lastModified).toISOString())}</lastmod>` : "";
475
+ return `<url>${loc}${mod}</url>`;
476
+ });
477
+ return `<?xml version="1.0" encoding="UTF-8"?>
478
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${urls.join("")}</urlset>
479
+ `;
480
+ }
481
+ function robotsTxt({ origin, basePath = "", disallowAll = false }) {
482
+ const lines = ["User-agent: *", disallowAll ? "Disallow: /" : "Allow: /"];
483
+ if (!disallowAll) lines.push("", `Sitemap: ${absoluteUrl(origin, basePath, "/sitemap.xml")}`);
484
+ return `${lines.join("\n")}
485
+ `;
486
+ }
487
+ function redirectsAfterMove(redirects, from, to) {
488
+ if (from === to) return [...redirects];
489
+ const next = redirects.filter((r) => r.from !== to && r.from !== from).map((r) => r.to === from ? { ...r, to } : r).filter((r) => r.from !== r.to);
490
+ next.push({ from, to, status: 301 });
491
+ return next;
492
+ }
493
+ function matchRedirect(redirects, path) {
494
+ return redirects.find((r) => r.from === path && isSafeHref(r.to));
495
+ }
496
+
497
+ export { BUILT_IN_SECTION_TYPES, Copy, DEFAULT_RESERVED_PATHS, FontChoice, Href, Image, Line, Link, MAX_DOC_BYTES, MAX_PAGE_PATH_LENGTH, MAX_URL_LENGTH, MenuItem, PAGE_PATH, PageDoc, PageKind, PagePath, Redirect, ReleasePage, ReleaseSnapshot, RichText, SECTION_CATEGORIES, SectionDefinitionError, SectionId, SectionNode, Seo, SeoDefaults, SiteDoc, Surface, TokenOverrides, absoluteUrl, breadcrumbJsonLd, defineSection, escapeXml, getSection, isExternalHref, isReservedPath, isSafeHref, isSafeImageSrc, listSections, matchRedirect, newSectionId, nodeFromPreset, normalizeRequestPath, organizationJsonLd, pageMeta, pagePathProblems, pageTitle, parseSnapshot, redirectsAfterMove, registerBuiltInSection, registerSection, robotsTxt, safeJsonForHtml, sectionJsonSchema, sectionManifests, sitemapXml, unregisterSection, validatePagePath, websiteJsonLd };
498
+ //# sourceMappingURL=chunk-KEF3V5ZN.js.map
499
+ //# sourceMappingURL=chunk-KEF3V5ZN.js.map