@marktiderman/genesis-sites 0.0.0-stage → 0.2.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 (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +465 -2
  3. package/dist/assets/form-island.c0f6fac859.js +51 -0
  4. package/dist/assets/islands.77d11f5697.js +1 -0
  5. package/dist/assets/sites-forms.4e6aa42497.css +10 -0
  6. package/dist/assets/sites-forms.css +10 -0
  7. package/dist/assets/sites.css +2 -0
  8. package/dist/assets/sites.e1ffd2708b.css +2 -0
  9. package/dist/blocks.d.ts +13 -0
  10. package/dist/blocks.js +26 -0
  11. package/dist/blocks.js.map +1 -0
  12. package/dist/chunk-3VKTBE7S.js +69 -0
  13. package/dist/chunk-3VKTBE7S.js.map +1 -0
  14. package/dist/chunk-C375ZPU2.js +2241 -0
  15. package/dist/chunk-C375ZPU2.js.map +1 -0
  16. package/dist/chunk-IXMY4B56.js +877 -0
  17. package/dist/chunk-IXMY4B56.js.map +1 -0
  18. package/dist/chunk-VZUDRPFG.js +13 -0
  19. package/dist/chunk-VZUDRPFG.js.map +1 -0
  20. package/dist/chunk-ZGWGQGT2.js +595 -0
  21. package/dist/chunk-ZGWGQGT2.js.map +1 -0
  22. package/dist/import.d.ts +130 -0
  23. package/dist/import.js +330 -0
  24. package/dist/import.js.map +1 -0
  25. package/dist/index.d.ts +273 -0
  26. package/dist/index.js +254 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/registry-D_DCWRTc.d.ts +1030 -0
  29. package/dist/sections.d.ts +802 -0
  30. package/dist/sections.js +6 -0
  31. package/dist/sections.js.map +1 -0
  32. package/dist/store-DkSycDhq.d.ts +443 -0
  33. package/dist/supabase.d.ts +41 -0
  34. package/dist/supabase.js +283 -0
  35. package/dist/supabase.js.map +1 -0
  36. package/dist/web.d.ts +136 -0
  37. package/dist/web.js +479 -0
  38. package/dist/web.js.map +1 -0
  39. package/migrations/0001_genesis_sites.sql +821 -0
  40. package/migrations/0002_genesis_sites_posts.sql +404 -0
  41. package/migrations/tests/0001_genesis_sites.test.sql +349 -0
  42. package/migrations/tests/0002_genesis_sites_posts.test.sql +242 -0
  43. package/package.json +152 -3
@@ -0,0 +1,595 @@
1
+ import { MAX_URL_LENGTH, isSafeHref, isSafeImageSrc, PAGE_PATH } from './chunk-3VKTBE7S.js';
2
+ import { z } from 'zod';
3
+
4
+ if (typeof z.toJSONSchema !== "function") {
5
+ throw new Error("@marktiderman/genesis-sites needs zod 4 (zod@^4.0.0): z.toJSONSchema is missing.");
6
+ }
7
+ 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" });
8
+ var LinkEvent = z.string().regex(/^[a-z0-9][a-z0-9_.:-]{0,47}$/, "An event name is lowercase letters, digits and _ . : -").meta({ title: "Analytics event" });
9
+ var Link = z.object({
10
+ label: z.string().trim().min(1).max(80),
11
+ href: Href,
12
+ /** Opens in a new tab (http(s) links only). The same as `target: "_blank"`. */
13
+ newTab: z.boolean().optional(),
14
+ /** Where an http(s) link opens: this tab (`_self`, the default) or a new one (`_blank`). */
15
+ target: z.enum(["_self", "_blank"]).optional().meta({ title: "Opens in" }),
16
+ /** A link that leaves the site (a booking system): http(s) only, `rel="noopener noreferrer"`. */
17
+ external: z.boolean().optional().meta({ title: "Outbound link" }),
18
+ event: LinkEvent.optional()
19
+ }).refine((link) => !link.external || /^https?:\/\//i.test(link.href), { message: "An outbound link is an http(s) URL.", path: ["href"] }).meta({ title: "Link" });
20
+ var opensInNewTab = (link) => /^https?:\/\//i.test(link.href) && (link.target === "_blank" || link.target === void 0 && link.newTab === true);
21
+ var FileRef = z.object({
22
+ src: z.string().max(MAX_URL_LENGTH).refine(isSafeImageSrc, { message: "A file loads from a site path or an https URL." }),
23
+ /** The file's type; PDF unless it is an image. */
24
+ mime: z.enum(["application/pdf", "image/png", "image/jpeg", "image/webp", "image/avif", "image/gif"]).default("application/pdf"),
25
+ /** Its size, shown beside the link ("PDF, 1.2 MB"). */
26
+ bytes: z.number().int().positive().max(100 * 1024 * 1024).optional()
27
+ }).meta({ title: "File", "x-genesis-kind": "file" });
28
+ var Image = z.object({
29
+ src: z.string().max(MAX_URL_LENGTH).refine(isSafeImageSrc, { message: "An image loads from a site path or an https URL." }),
30
+ alt: z.string().max(300).default(""),
31
+ decorative: z.boolean().optional(),
32
+ width: z.number().int().positive().max(1e4).optional(),
33
+ height: z.number().int().positive().max(1e4).optional(),
34
+ /** Where the subject is, for cropping: 0–100 from the left and top. */
35
+ focal: z.object({ x: z.number().min(0).max(100), y: z.number().min(0).max(100) }).optional()
36
+ }).meta({ title: "Image", "x-genesis-kind": "image" });
37
+ var RichText = z.object({
38
+ type: z.literal("doc"),
39
+ content: z.array(z.record(z.string(), z.unknown())).max(2e3).optional()
40
+ }).meta({ title: "Rich text", "x-genesis-kind": "rich-text" });
41
+ var Surface = z.enum(["base", "muted", "accent", "inverse"]).meta({ title: "Background" });
42
+ var Line = (max = 120) => z.string().trim().min(1).max(max);
43
+ var Copy = (max = 600) => z.string().trim().max(max);
44
+ var SectionId = z.string().regex(/^[A-Za-z0-9_-]{1,64}$/, "A section id is 1\u201364 letters, digits, - or _.");
45
+ var SectionNode = z.object({
46
+ id: SectionId,
47
+ /** The section's registry key, such as `hero`. */
48
+ type: z.string().regex(/^[a-z][a-z0-9-]{0,63}$/),
49
+ /** The props version the props were written at; `migrateNode` brings them up to date. */
50
+ v: z.number().int().min(1),
51
+ variant: z.string().max(64).optional(),
52
+ /** Kept in the page, not rendered (Shopify's `disabled`). */
53
+ hidden: z.boolean().optional(),
54
+ /** Hidden on phones or on wider screens. */
55
+ hideOn: z.array(z.enum(["mobile", "desktop"])).max(2).optional(),
56
+ /**
57
+ * Set by a template on content someone must replace (a sample quote). The
58
+ * publish check refuses a visible section that still has it; the editor
59
+ * clears it once the section is edited.
60
+ */
61
+ placeholder: z.boolean().optional(),
62
+ props: z.record(z.string(), z.unknown())
63
+ });
64
+ var Seo = z.object({
65
+ title: z.string().trim().max(70).optional(),
66
+ description: z.string().trim().max(200).optional(),
67
+ ogImage: Image.optional(),
68
+ /** An absolute canonical URL, when this page's content lives elsewhere. */
69
+ canonical: z.url({ protocol: /^https?$/ }).max(MAX_URL_LENGTH).optional(),
70
+ noindex: z.boolean().optional()
71
+ });
72
+ var PageDoc = z.object({
73
+ schemaVersion: z.literal(1),
74
+ root: z.object({
75
+ props: z.looseObject({
76
+ title: z.string().trim().min(1).max(120),
77
+ /** Leave out the site's header and footer (a focused landing page). */
78
+ hideSiteChrome: z.boolean().optional()
79
+ })
80
+ }),
81
+ content: z.array(SectionNode).max(60),
82
+ seo: Seo.optional()
83
+ });
84
+ var PageKind = z.enum(["page", "landing", "post"]);
85
+ var PagePath = z.string().max(200).regex(PAGE_PATH, 'A page path looks like "/" or "/about".');
86
+ var MenuItem = z.object({
87
+ label: z.string().trim().min(1).max(40),
88
+ href: Href
89
+ });
90
+ var Redirect = z.object({
91
+ from: PagePath,
92
+ to: Href,
93
+ status: z.union([z.literal(301), z.literal(302)])
94
+ });
95
+ var FontChoice = z.enum([
96
+ "system",
97
+ "inter",
98
+ "dm-sans",
99
+ "work-sans",
100
+ "montserrat",
101
+ "poppins",
102
+ "space-grotesk",
103
+ "source-serif",
104
+ "merriweather",
105
+ "playfair-display",
106
+ "lora"
107
+ ]);
108
+ var TokenOverrides = z.object({
109
+ radius: z.enum(["none", "sm", "md", "lg", "full"]).optional(),
110
+ headingFont: FontChoice.optional(),
111
+ bodyFont: FontChoice.optional()
112
+ });
113
+ var SeoDefaults = z.object({
114
+ /** `%s` is the page's title: `"%s · Riverside Gym"`. */
115
+ titleTemplate: z.string().max(80).optional(),
116
+ description: z.string().trim().max(200).optional(),
117
+ ogImage: Image.optional(),
118
+ /** Organization facts for JSON-LD. */
119
+ organization: z.object({
120
+ name: z.string().trim().min(1).max(120),
121
+ logo: z.string().max(MAX_URL_LENGTH).refine(isSafeImageSrc).optional(),
122
+ sameAs: z.array(z.url({ protocol: /^https$/ }).max(MAX_URL_LENGTH)).max(10).optional()
123
+ }).optional(),
124
+ noindex: z.boolean().optional()
125
+ });
126
+ var SiteDoc = z.object({
127
+ schemaVersion: z.literal(1),
128
+ name: z.string().trim().min(1).max(120),
129
+ /** BCP 47 language of the site's pages. */
130
+ lang: z.string().regex(/^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/).default("en"),
131
+ theme: z.object({
132
+ colorScheme: z.enum(["light", "dark", "auto"]).default("light"),
133
+ overrides: TokenOverrides.optional()
134
+ }).default({ colorScheme: "light" }),
135
+ /** The site's header: usually one `nav` section. */
136
+ header: z.array(SectionNode).max(2).default([]),
137
+ /** The header's menu. */
138
+ nav: z.array(MenuItem).max(12).default([]),
139
+ /** The site's footer: usually one `footer` section. */
140
+ footer: z.array(SectionNode).max(3).default([]),
141
+ seo: SeoDefaults.default({}),
142
+ redirects: z.array(Redirect).max(500).default([])
143
+ });
144
+ var ReleasePage = z.object({
145
+ id: z.string().min(1),
146
+ path: PagePath,
147
+ kind: PageKind,
148
+ doc: PageDoc
149
+ });
150
+ var ReleaseSnapshot = z.object({
151
+ schemaVersion: z.literal(1),
152
+ site: SiteDoc,
153
+ pages: z.array(ReleasePage).max(500)
154
+ });
155
+ function parseSnapshot(value, report = (m) => console.error(`[genesis-sites] ${m}`)) {
156
+ const raw = value && typeof value === "object" ? value : {};
157
+ if (raw.schemaVersion !== 1) {
158
+ report("the snapshot isn't schemaVersion 1.");
159
+ return null;
160
+ }
161
+ const site = SiteDoc.safeParse(raw.site);
162
+ if (!site.success) {
163
+ report(`the snapshot's site document doesn't parse: ${site.error.message}`);
164
+ return null;
165
+ }
166
+ const pages = [];
167
+ for (const page of Array.isArray(raw.pages) ? raw.pages.slice(0, 500) : []) {
168
+ const parsed = ReleasePage.safeParse(page);
169
+ if (parsed.success) pages.push(parsed.data);
170
+ else report(`the page ${JSON.stringify(page?.path ?? "?")} doesn't parse and is left out: ${parsed.error.message}`);
171
+ }
172
+ return { schemaVersion: 1, site: site.data, pages };
173
+ }
174
+ var MAX_DOC_BYTES = 512 * 1024;
175
+ var SECTION_CATEGORIES = ["header", "intro", "content", "social-proof", "conversion", "footer"];
176
+ var SectionDefinitionError = class extends Error {
177
+ constructor(problems) {
178
+ super(`Invalid section definition:
179
+ ${problems.map((p) => ` - ${p}`).join("\n")}`);
180
+ this.problems = problems;
181
+ this.name = "SectionDefinitionError";
182
+ }
183
+ problems;
184
+ };
185
+ var TYPE = /^[a-z][a-z0-9-]{0,63}$/;
186
+ function definitionProblems(def) {
187
+ const problems = [];
188
+ if (!def || typeof def !== "object") return ["a section definition is an object"];
189
+ const at = typeof def.type === "string" ? `"${def.type}"` : "a section";
190
+ if (typeof def.type !== "string" || !TYPE.test(def.type)) problems.push(`${at}: \`type\` is lowercase letters, digits and -`);
191
+ if (!Number.isInteger(def.version) || def.version < 1) problems.push(`${at}: \`version\` is a whole number from 1`);
192
+ if (typeof def.label !== "string" || !def.label.trim()) problems.push(`${at}: needs a \`label\``);
193
+ if (!SECTION_CATEGORIES.includes(def.category)) problems.push(`${at}: \`category\` is one of ${SECTION_CATEGORIES.join(", ")}`);
194
+ if (!def.schema || typeof def.schema.safeParse !== "function") problems.push(`${at}: \`schema\` is a zod schema`);
195
+ if (typeof def.render !== "function") problems.push(`${at}: \`render\` is a function`);
196
+ const ids = /* @__PURE__ */ new Set();
197
+ if (!Array.isArray(def.variants) || def.variants.length === 0) problems.push(`${at}: needs at least one variant`);
198
+ else
199
+ for (const v of def.variants) {
200
+ 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 }`);
201
+ else if (ids.has(v.id)) problems.push(`${at}: variant "${v.id}" is listed twice`);
202
+ else ids.add(v.id);
203
+ }
204
+ if (!ids.has(def.defaultVariant)) problems.push(`${at}: \`defaultVariant\` "${String(def.defaultVariant)}" is not one of its variants`);
205
+ if (def.limit !== void 0 && (!Number.isInteger(def.limit) || def.limit < 1)) problems.push(`${at}: \`limit\` is a whole number from 1`);
206
+ if (def.placement !== void 0 && !["header", "body", "footer"].includes(def.placement)) problems.push(`${at}: \`placement\` is header, body or footer`);
207
+ if (def.frameOrigins !== void 0 && typeof def.frameOrigins !== "function") problems.push(`${at}: \`frameOrigins\` is a function`);
208
+ for (const [key, fn] of Object.entries(def.migrations ?? {})) {
209
+ const n = Number(key);
210
+ if (!Number.isInteger(n) || n < 2 || n > def.version) problems.push(`${at}: migration ${key} must be between 2 and the version (${def.version})`);
211
+ if (typeof fn !== "function") problems.push(`${at}: migration ${key} is a function`);
212
+ }
213
+ for (let n = 2; n <= (Number.isInteger(def.version) ? def.version : 0); n++) {
214
+ if (!def.migrations?.[n]) problems.push(`${at}: version ${def.version} needs a migration to ${n}`);
215
+ }
216
+ if (!Array.isArray(def.presets) || def.presets.length === 0) problems.push(`${at}: needs at least one preset (the editor's Add picker)`);
217
+ else if (def.schema && typeof def.schema.safeParse === "function")
218
+ for (const preset of def.presets) {
219
+ if (!preset || typeof preset.name !== "string" || !ids.has(preset.variant)) {
220
+ problems.push(`${at}: preset ${JSON.stringify(preset?.name)} needs a name and one of its variants`);
221
+ continue;
222
+ }
223
+ const parsed = def.schema.safeParse(preset.props);
224
+ 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("; ")}`);
225
+ }
226
+ return problems;
227
+ }
228
+ function defineSection(def) {
229
+ const problems = definitionProblems(def);
230
+ if (problems.length) throw new SectionDefinitionError(problems);
231
+ return Object.freeze({
232
+ ...def,
233
+ variants: Object.freeze(def.variants.map((v) => Object.freeze({ ...v }))),
234
+ presets: Object.freeze(def.presets.map((p) => Object.freeze({ ...p }))),
235
+ ...def.enabledOn ? { enabledOn: Object.freeze([...def.enabledOn]) } : {},
236
+ ...def.migrations ? { migrations: Object.freeze({ ...def.migrations }) } : {}
237
+ });
238
+ }
239
+ var REGISTRY = /* @__PURE__ */ Symbol.for("@marktiderman/genesis-sites/sections@1");
240
+ var BUILT_IN = /* @__PURE__ */ Symbol.for("@marktiderman/genesis-sites/built-in-sections@1");
241
+ var scope = globalThis;
242
+ var registry = scope[REGISTRY] ??= /* @__PURE__ */ new Map();
243
+ var builtIns = scope[BUILT_IN] ??= /* @__PURE__ */ new Set();
244
+ var BUILT_IN_SECTION_TYPES = Object.freeze([
245
+ "nav",
246
+ "hero",
247
+ "features",
248
+ "cta",
249
+ "testimonials",
250
+ "faq",
251
+ "pricing",
252
+ "logos",
253
+ "stats",
254
+ "gallery",
255
+ "rich-text",
256
+ "form-embed",
257
+ "footer",
258
+ "schedule",
259
+ "team",
260
+ "blog-list",
261
+ "post-body",
262
+ "map",
263
+ "timeline",
264
+ "file"
265
+ ]);
266
+ function registerSection(def, options = {}) {
267
+ const frozen = Object.isFrozen(def) && definitionProblems(def).length === 0 ? def : defineSection(def);
268
+ if (BUILT_IN_SECTION_TYPES.includes(frozen.type) && options.replace !== true) {
269
+ throw new Error(
270
+ `registerSection: "${frozen.type}" is a built-in section; pass { replace: true } to replace it for every site in this process.`
271
+ );
272
+ }
273
+ registry.set(frozen.type, frozen);
274
+ }
275
+ function registerBuiltInSection(def) {
276
+ if (!registry.has(def.type)) registry.set(def.type, def);
277
+ builtIns.add(def.type);
278
+ }
279
+ function getSection(type) {
280
+ return registry.get(type);
281
+ }
282
+ function listSections() {
283
+ return [...registry.values()];
284
+ }
285
+ function unregisterSection(type) {
286
+ if (BUILT_IN_SECTION_TYPES.includes(type)) throw new Error(`unregisterSection: "${type}" is built in; replace it with registerSection(\u2026, { replace: true }).`);
287
+ return registry.delete(type);
288
+ }
289
+ var jsonSchemas = /* @__PURE__ */ new WeakMap();
290
+ function sectionJsonSchema(typeOrDef) {
291
+ const def = typeof typeOrDef === "string" ? registry.get(typeOrDef) : typeOrDef;
292
+ if (!def) throw new Error(`sectionJsonSchema: no section "${String(typeOrDef)}" is registered.`);
293
+ let schema = jsonSchemas.get(def.schema);
294
+ if (!schema) {
295
+ schema = z.toJSONSchema(def.schema, { io: "input", unrepresentable: "any" });
296
+ jsonSchemas.set(def.schema, schema);
297
+ }
298
+ return schema;
299
+ }
300
+ function sectionManifests() {
301
+ return listSections().map((def) => ({
302
+ type: def.type,
303
+ version: def.version,
304
+ label: def.label,
305
+ ...def.description ? { description: def.description } : {},
306
+ category: def.category,
307
+ placement: def.placement ?? "body",
308
+ variants: def.variants,
309
+ defaultVariant: def.defaultVariant,
310
+ presets: def.presets,
311
+ ...def.limit ? { limit: def.limit } : {},
312
+ ...def.enabledOn ? { enabledOn: def.enabledOn } : {},
313
+ jsonSchema: sectionJsonSchema(def)
314
+ }));
315
+ }
316
+ var counter = 0;
317
+ function newSectionId(prefix = "s") {
318
+ counter = (counter + 1) % 1e6;
319
+ const random = Math.random().toString(36).slice(2, 8);
320
+ return `${prefix}${Date.now().toString(36)}${counter.toString(36)}${random}`.slice(0, 64);
321
+ }
322
+ function nodeFromPreset(type, presetName, id = newSectionId(type.replace(/-/g, "").slice(0, 6))) {
323
+ const def = registry.get(type);
324
+ if (!def) throw new Error(`nodeFromPreset: no section "${type}" is registered.`);
325
+ const preset = presetName ? def.presets.find((p) => p.name === presetName) : def.presets[0];
326
+ if (!preset) throw new Error(`nodeFromPreset: "${type}" has no preset "${presetName}".`);
327
+ return { id, type, v: def.version, variant: preset.variant, props: structuredClone(preset.props) };
328
+ }
329
+ var PostSlug = z.string().max(80).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "A post slug is lowercase letters, digits and single hyphens.");
330
+ var PostAuthor = z.object({
331
+ name: Line(80),
332
+ /** Their page: a site path or an http(s) URL. */
333
+ url: Href.optional()
334
+ });
335
+ var PostDoc = z.object({
336
+ schemaVersion: z.literal(1),
337
+ title: Line(160),
338
+ /** A sentence or two for the list, search results and the feed. */
339
+ excerpt: Copy(400).optional(),
340
+ body: RichText,
341
+ cover: Image.optional(),
342
+ author: PostAuthor.optional(),
343
+ category: Line(60).optional(),
344
+ tags: z.array(Line(40)).max(12).optional(),
345
+ seo: Seo.optional()
346
+ });
347
+ var PostSummarySchema = z.object({
348
+ slug: PostSlug,
349
+ title: Line(160),
350
+ excerpt: Copy(400).optional(),
351
+ cover: Image.optional(),
352
+ author: PostAuthor.optional(),
353
+ category: Line(60).optional(),
354
+ noindex: z.boolean().optional(),
355
+ publishedAt: z.string().refine((s) => !Number.isNaN(Date.parse(s)), "a date"),
356
+ updatedAt: z.string().refine((s) => !Number.isNaN(Date.parse(s)), "a date")
357
+ });
358
+ var MAX_LIVE_POSTS = 500;
359
+ var MAX_IMPORT_POSTS = 500;
360
+ function postSummary(post) {
361
+ const d = post.doc;
362
+ return {
363
+ slug: post.slug,
364
+ title: d.title,
365
+ ...d.excerpt ? { excerpt: d.excerpt } : {},
366
+ ...d.cover ? { cover: d.cover } : {},
367
+ ...d.author ? { author: d.author } : {},
368
+ ...d.category ? { category: d.category } : {},
369
+ ...d.seo?.noindex || d.seo?.canonical ? { noindex: true } : {},
370
+ publishedAt: post.publishedAt,
371
+ updatedAt: post.updatedAt
372
+ };
373
+ }
374
+ function richTextPlain(value, max = 400) {
375
+ const out = [];
376
+ const walk = (node) => {
377
+ if (out.join(" ").length > max || !node || typeof node !== "object") return;
378
+ const n = node;
379
+ if (n.type === "text" && typeof n.text === "string") out.push(n.text);
380
+ if (Array.isArray(n.content)) n.content.forEach(walk);
381
+ };
382
+ walk(value);
383
+ const text = out.join(" ").replace(/\s+/g, " ").trim();
384
+ return text.length > max ? `${text.slice(0, max - 1).trimEnd()}\u2026` : text;
385
+ }
386
+ function validatePostSlug(slug) {
387
+ const parsed = PostSlug.safeParse(slug);
388
+ if (!parsed.success) throw new Error(`"${slug}": ${parsed.error.issues[0]?.message ?? "not a post slug"}`);
389
+ return slug;
390
+ }
391
+
392
+ // src/core/seo.ts
393
+ function absoluteUrl(origin, basePath, path) {
394
+ const base = origin.replace(/\/+$/, "") + (basePath ?? "").replace(/\/+$/, "");
395
+ return path === "/" ? `${base}/` : `${base}${path}`;
396
+ }
397
+ function imageUrl(image, origin, basePath) {
398
+ if (!image) return void 0;
399
+ return image.src.startsWith("/") ? absoluteUrl(origin, basePath, image.src) : image.src;
400
+ }
401
+ function pageTitle(site, doc, _path) {
402
+ const own = doc.seo?.title?.trim();
403
+ if (own) return own;
404
+ const title = doc.root.props.title;
405
+ const template = site.seo.titleTemplate;
406
+ if (!template || !template.includes("%s")) return title;
407
+ return template.replace("%s", title);
408
+ }
409
+ function pageMeta({ site, page, origin, basePath = "", preview = false, article }) {
410
+ const doc = page.doc;
411
+ const title = pageTitle(site, doc, page.path);
412
+ const description = doc.seo?.description?.trim() || site.seo.description?.trim() || void 0;
413
+ const canonical = doc.seo?.canonical ?? absoluteUrl(origin, basePath, page.path);
414
+ const noindex = preview || site.seo.noindex === true || doc.seo?.noindex === true;
415
+ const image = doc.seo?.ogImage ?? site.seo.ogImage;
416
+ const img = imageUrl(image, origin, basePath);
417
+ const openGraph = {
418
+ "og:type": "website",
419
+ "og:title": title,
420
+ "og:url": canonical,
421
+ "og:site_name": site.name,
422
+ "og:locale": site.lang.replace("-", "_")
423
+ };
424
+ if (description) openGraph["og:description"] = description;
425
+ if (article) {
426
+ openGraph["og:type"] = "article";
427
+ openGraph["article:published_time"] = new Date(article.publishedTime).toISOString();
428
+ if (article.modifiedTime) openGraph["article:modified_time"] = new Date(article.modifiedTime).toISOString();
429
+ if (article.author) openGraph["article:author"] = article.author;
430
+ if (article.section) openGraph["article:section"] = article.section;
431
+ }
432
+ if (img) {
433
+ openGraph["og:image"] = img;
434
+ if (image?.alt) openGraph["og:image:alt"] = image.alt;
435
+ if (image?.width) openGraph["og:image:width"] = String(image.width);
436
+ if (image?.height) openGraph["og:image:height"] = String(image.height);
437
+ }
438
+ const twitter = { "twitter:card": img ? "summary_large_image" : "summary", "twitter:title": title };
439
+ if (description) twitter["twitter:description"] = description;
440
+ if (img) twitter["twitter:image"] = img;
441
+ return {
442
+ title,
443
+ ...description ? { description } : {},
444
+ canonical,
445
+ robots: noindex ? "noindex, nofollow" : "index, follow",
446
+ openGraph,
447
+ twitter
448
+ };
449
+ }
450
+ function organizationJsonLd(site, origin, basePath = "") {
451
+ const org = site.seo.organization;
452
+ const out = {
453
+ "@context": "https://schema.org",
454
+ "@type": "Organization",
455
+ name: org?.name ?? site.name,
456
+ url: absoluteUrl(origin, basePath, "/")
457
+ };
458
+ if (org?.logo) out.logo = org.logo.startsWith("/") ? absoluteUrl(origin, basePath, org.logo) : org.logo;
459
+ if (org?.sameAs?.length) out.sameAs = org.sameAs;
460
+ return out;
461
+ }
462
+ function websiteJsonLd(site, origin, basePath = "") {
463
+ return {
464
+ "@context": "https://schema.org",
465
+ "@type": "WebSite",
466
+ name: site.name,
467
+ url: absoluteUrl(origin, basePath, "/"),
468
+ inLanguage: site.lang
469
+ };
470
+ }
471
+ function breadcrumbJsonLd(pages, page, origin, basePath = "") {
472
+ if (page.path === "/") return null;
473
+ const byPath = new Map(pages.map((p) => [p.path, p]));
474
+ const trail = [];
475
+ const home = byPath.get("/");
476
+ if (home) trail.push(home);
477
+ const segments = page.path.split("/").filter(Boolean);
478
+ for (let i = 1; i <= segments.length; i++) {
479
+ const ancestor = byPath.get(`/${segments.slice(0, i).join("/")}`);
480
+ if (ancestor) trail.push(ancestor);
481
+ }
482
+ if (trail.length < 2) return null;
483
+ return {
484
+ "@context": "https://schema.org",
485
+ "@type": "BreadcrumbList",
486
+ itemListElement: trail.map((p, i) => ({
487
+ "@type": "ListItem",
488
+ position: i + 1,
489
+ name: p.doc.root.props.title,
490
+ item: absoluteUrl(origin, basePath, p.path)
491
+ }))
492
+ };
493
+ }
494
+ function isoDate(value) {
495
+ if (typeof value !== "string" && typeof value !== "number") return null;
496
+ const d = new Date(value);
497
+ return Number.isNaN(d.getTime()) ? null : d.toISOString();
498
+ }
499
+ var textOf = (value) => typeof value === "string" && value ? value : void 0;
500
+ function postPath(postsPath, slug) {
501
+ return `${postsPath === "/" ? "" : postsPath}/${slug}`;
502
+ }
503
+ function articleJsonLd(site, post, url, origin, basePath = "") {
504
+ const d = post.doc;
505
+ const org = site.seo.organization;
506
+ const publisher = { "@type": "Organization", name: org?.name ?? site.name, url: absoluteUrl(origin, basePath, "/") };
507
+ if (org?.logo) publisher.logo = { "@type": "ImageObject", url: org.logo.startsWith("/") ? absoluteUrl(origin, basePath, org.logo) : org.logo };
508
+ const description = d.excerpt ?? d.seo?.description;
509
+ const image = imageUrl(d.cover ?? d.seo?.ogImage, origin, basePath);
510
+ return {
511
+ "@context": "https://schema.org",
512
+ "@type": "Article",
513
+ headline: d.title.slice(0, 110),
514
+ ...description ? { description } : {},
515
+ ...image ? { image: [image] } : {},
516
+ datePublished: new Date(post.publishedAt).toISOString(),
517
+ dateModified: new Date(post.updatedAt).toISOString(),
518
+ author: d.author ? { "@type": "Person", name: d.author.name, ...d.author.url && isSafeHref(d.author.url) ? { url: d.author.url.startsWith("/") ? absoluteUrl(origin, basePath, d.author.url) : d.author.url } : {} } : { "@type": "Organization", name: org?.name ?? site.name },
519
+ publisher,
520
+ mainEntityOfPage: { "@type": "WebPage", "@id": url },
521
+ ...d.category ? { articleSection: d.category } : {},
522
+ ...d.tags?.length ? { keywords: d.tags.join(", ") } : {},
523
+ inLanguage: site.lang
524
+ };
525
+ }
526
+ function rssXml({ site, posts, postsPath, title, origin, basePath = "", limit = 20 }) {
527
+ const link = absoluteUrl(origin, basePath, postsPath);
528
+ const self = absoluteUrl(origin, basePath, "/rss.xml");
529
+ const usable = posts.filter((p) => !p.noindex && textOf(p.title) && textOf(p.slug) && isoDate(p.publishedAt));
530
+ const items = usable.slice(0, limit).map((p) => {
531
+ const url = absoluteUrl(origin, basePath, postPath(postsPath, p.slug));
532
+ const excerpt = textOf(p.excerpt);
533
+ const category = textOf(p.category);
534
+ const author = textOf(p.author?.name);
535
+ return [
536
+ "<item>",
537
+ `<title>${escapeXml(p.title)}</title>`,
538
+ `<link>${escapeXml(url)}</link>`,
539
+ `<guid isPermaLink="true">${escapeXml(url)}</guid>`,
540
+ `<pubDate>${new Date(isoDate(p.publishedAt)).toUTCString()}</pubDate>`,
541
+ excerpt ? `<description>${escapeXml(excerpt)}</description>` : "",
542
+ category ? `<category>${escapeXml(category)}</category>` : "",
543
+ author ? `<dc:creator>${escapeXml(author)}</dc:creator>` : "",
544
+ "</item>"
545
+ ].join("");
546
+ });
547
+ const newest = usable[0];
548
+ return `<?xml version="1.0" encoding="UTF-8"?>
549
+ <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>${escapeXml(title ?? site.name)}</title><link>${escapeXml(link)}</link><description>${escapeXml(site.seo.description ?? `Posts from ${site.name}`)}</description><language>${escapeXml(site.lang)}</language><atom:link href="${escapeXml(self)}" rel="self" type="application/rss+xml"/>` + (newest ? `<lastBuildDate>${new Date(isoDate(newest.updatedAt) ?? isoDate(newest.publishedAt)).toUTCString()}</lastBuildDate>` : "") + `${items.join("")}</channel></rss>
550
+ `;
551
+ }
552
+ var postDescription = (doc) => doc.excerpt ?? doc.seo?.description ?? (richTextPlain(doc.body, 200) || void 0);
553
+ function safeJsonForHtml(value) {
554
+ return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
555
+ }
556
+ function escapeXml(text) {
557
+ return text.replace(/[<>&'"]/g, (c) => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;", "'": "&apos;", '"': "&quot;" })[c]);
558
+ }
559
+ function sitemapXml({ site, pages, origin, basePath = "", lastModified, posts }) {
560
+ 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) => {
561
+ const loc = `<loc>${escapeXml(absoluteUrl(origin, basePath, p.path))}</loc>`;
562
+ const mod = lastModified ? `<lastmod>${escapeXml(new Date(lastModified).toISOString())}</lastmod>` : "";
563
+ return `<url>${loc}${mod}</url>`;
564
+ });
565
+ const blogPage = posts ? pages.find((p) => p.path === posts.path) : void 0;
566
+ if (posts && !site.seo.noindex && blogPage && !blogPage.doc.seo?.noindex) {
567
+ for (const p of posts.items) {
568
+ if (p.noindex) continue;
569
+ const mod = isoDate(p.updatedAt) ?? isoDate(p.publishedAt);
570
+ urls.push(`<url><loc>${escapeXml(absoluteUrl(origin, basePath, postPath(posts.path, p.slug)))}</loc>${mod ? `<lastmod>${escapeXml(mod)}</lastmod>` : ""}</url>`);
571
+ }
572
+ }
573
+ return `<?xml version="1.0" encoding="UTF-8"?>
574
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${urls.join("")}</urlset>
575
+ `;
576
+ }
577
+ function robotsTxt({ origin, basePath = "", disallowAll = false }) {
578
+ const lines = ["User-agent: *", disallowAll ? "Disallow: /" : "Allow: /"];
579
+ if (!disallowAll) lines.push("", `Sitemap: ${absoluteUrl(origin, basePath, "/sitemap.xml")}`);
580
+ return `${lines.join("\n")}
581
+ `;
582
+ }
583
+ function redirectsAfterMove(redirects, from, to) {
584
+ if (from === to) return [...redirects];
585
+ 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);
586
+ next.push({ from, to, status: 301 });
587
+ return next;
588
+ }
589
+ function matchRedirect(redirects, path) {
590
+ return redirects.find((r) => r.from === path && isSafeHref(r.to));
591
+ }
592
+
593
+ export { BUILT_IN_SECTION_TYPES, Copy, FileRef, FontChoice, Href, Image, Line, Link, LinkEvent, MAX_DOC_BYTES, MAX_IMPORT_POSTS, MAX_LIVE_POSTS, MenuItem, PageDoc, PageKind, PagePath, PostAuthor, PostDoc, PostSlug, PostSummarySchema, Redirect, ReleasePage, ReleaseSnapshot, RichText, SECTION_CATEGORIES, SectionDefinitionError, SectionId, SectionNode, Seo, SeoDefaults, SiteDoc, Surface, TokenOverrides, absoluteUrl, articleJsonLd, breadcrumbJsonLd, defineSection, escapeXml, getSection, listSections, matchRedirect, newSectionId, nodeFromPreset, opensInNewTab, organizationJsonLd, pageMeta, pageTitle, parseSnapshot, postDescription, postPath, postSummary, redirectsAfterMove, registerBuiltInSection, registerSection, richTextPlain, robotsTxt, rssXml, safeJsonForHtml, sectionJsonSchema, sectionManifests, sitemapXml, unregisterSection, validatePostSlug, websiteJsonLd };
594
+ //# sourceMappingURL=chunk-ZGWGQGT2.js.map
595
+ //# sourceMappingURL=chunk-ZGWGQGT2.js.map