@marktiderman/genesis-sites 0.1.0 → 0.3.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 (60) hide show
  1. package/README.md +241 -13
  2. package/dist/assets/islands.77d11f5697.js +1 -0
  3. package/dist/assets/sites-forms.732cc7e0c1.css +10 -0
  4. package/dist/assets/sites-forms.css +1 -1
  5. package/dist/assets/sites.716b4ac1a5.css +2 -0
  6. package/dist/assets/sites.css +1 -1
  7. package/dist/assets.generated-QSB7EJWX.js +3 -0
  8. package/dist/assets.generated-QSB7EJWX.js.map +1 -0
  9. package/dist/blocks.d.ts +1 -1
  10. package/dist/blocks.js +5 -1
  11. package/dist/blocks.js.map +1 -1
  12. package/dist/chunk-3VKTBE7S.js +69 -0
  13. package/dist/chunk-3VKTBE7S.js.map +1 -0
  14. package/dist/chunk-5IXDFFEG.js +465 -0
  15. package/dist/chunk-5IXDFFEG.js.map +1 -0
  16. package/dist/chunk-C375ZPU2.js +2241 -0
  17. package/dist/chunk-C375ZPU2.js.map +1 -0
  18. package/dist/{chunk-GA7U3CIN.js → chunk-FBQF2L7O.js} +317 -97
  19. package/dist/chunk-FBQF2L7O.js.map +1 -0
  20. package/dist/chunk-H6ED3K4R.js +18 -0
  21. package/dist/chunk-H6ED3K4R.js.map +1 -0
  22. package/dist/chunk-KPOX7D4S.js +250 -0
  23. package/dist/chunk-KPOX7D4S.js.map +1 -0
  24. package/dist/chunk-VZUDRPFG.js +13 -0
  25. package/dist/chunk-VZUDRPFG.js.map +1 -0
  26. package/dist/{chunk-KEF3V5ZN.js → chunk-ZGWGQGT2.js} +169 -73
  27. package/dist/chunk-ZGWGQGT2.js.map +1 -0
  28. package/dist/editor.d.ts +526 -0
  29. package/dist/editor.js +2480 -0
  30. package/dist/editor.js.map +1 -0
  31. package/dist/handler-QHH453YG.js +9 -0
  32. package/dist/handler-QHH453YG.js.map +1 -0
  33. package/dist/import.d.ts +130 -0
  34. package/dist/import.js +330 -0
  35. package/dist/import.js.map +1 -0
  36. package/dist/index.d.ts +47 -8
  37. package/dist/index.js +5 -250
  38. package/dist/index.js.map +1 -1
  39. package/dist/{registry-CdgVymsu.d.ts → registry-HBn1X60x.d.ts} +274 -8
  40. package/dist/sections.d.ts +422 -6
  41. package/dist/sections.js +4 -2
  42. package/dist/{store-BzGw2i5J.d.ts → store-BXiZeZzH.d.ts} +119 -53
  43. package/dist/supabase.d.ts +5 -5
  44. package/dist/supabase.js +115 -6
  45. package/dist/supabase.js.map +1 -1
  46. package/dist/web.d.ts +26 -8
  47. package/dist/web.js +7 -353
  48. package/dist/web.js.map +1 -1
  49. package/migrations/0001_genesis_sites.sql +64 -6
  50. package/migrations/0002_genesis_sites_posts.sql +404 -0
  51. package/migrations/tests/0001_genesis_sites.test.sql +30 -1
  52. package/migrations/tests/0002_genesis_sites_posts.test.sql +242 -0
  53. package/package.json +28 -10
  54. package/dist/assets/islands.230b20b9c4.js +0 -1
  55. package/dist/assets/sites-forms.5e6ebf31c3.css +0 -10
  56. package/dist/assets/sites.b28b796c6f.css +0 -2
  57. package/dist/chunk-43XUAO63.js +0 -1255
  58. package/dist/chunk-43XUAO63.js.map +0 -1
  59. package/dist/chunk-GA7U3CIN.js.map +0 -1
  60. package/dist/chunk-KEF3V5ZN.js.map +0 -1
@@ -0,0 +1,250 @@
1
+ import { brandDisplayName } from './chunk-FBQF2L7O.js';
2
+
3
+ // src/core/templates.ts
4
+ var year = () => (/* @__PURE__ */ new Date()).getUTCFullYear();
5
+ function node(id, type, variant, props, extra = {}) {
6
+ return { id, type, v: 1, variant, props, ...extra };
7
+ }
8
+ function footerNode(name, contact, variant = "simple") {
9
+ const links = [];
10
+ if (contact?.email) links.push({ label: contact.email, href: `mailto:${contact.email}` });
11
+ if (contact?.phone) links.push({ label: contact.phone, href: `tel:${contact.phone.replace(/[^+\d]/g, "")}` });
12
+ return node("footer", "footer", variant, {
13
+ ...links.length ? { links } : {},
14
+ copyright: `\xA9 ${year()} ${name}`
15
+ });
16
+ }
17
+ function seo(name, message) {
18
+ return {
19
+ titleTemplate: `%s \xB7 ${name}`,
20
+ ...message.subhead ? { description: message.subhead.slice(0, 200) } : {},
21
+ organization: { name }
22
+ };
23
+ }
24
+ function primaryCta(input) {
25
+ return input.message.cta ?? { label: "Get in touch", href: "#contact" };
26
+ }
27
+ function benefitsNode(message) {
28
+ if (message.benefits?.length) {
29
+ return node("features", "features", "grid", {
30
+ heading: "What you get",
31
+ items: message.benefits.slice(0, 12).map((b, i) => ({ title: b.title, body: b.body, icon: ["check", "star", "heart", "zap", "shield", "clock"][i % 6] }))
32
+ });
33
+ }
34
+ return node(
35
+ "features",
36
+ "features",
37
+ "grid",
38
+ {
39
+ heading: "What you get",
40
+ items: [
41
+ { title: "Your first offer", body: "Say what someone gets and why it matters to them.", icon: "check" },
42
+ { title: "Your second offer", body: "Keep each one to a sentence or two.", icon: "star" },
43
+ { title: "Your third offer", body: "Three is plenty for a landing page.", icon: "heart" }
44
+ ]
45
+ },
46
+ { placeholder: true }
47
+ );
48
+ }
49
+ var testimonialsNode = () => node(
50
+ "testimonials",
51
+ "testimonials",
52
+ "grid",
53
+ {
54
+ heading: "What people say",
55
+ items: [
56
+ { quote: "Add a quote from someone you have helped, in their words and with their permission.", name: "Their name", role: "What they did with you" },
57
+ { quote: "A second quote, from someone different.", name: "Their name" }
58
+ ]
59
+ },
60
+ { placeholder: true }
61
+ );
62
+ function formNode(input) {
63
+ if (!input.form) return null;
64
+ return node("contact", "form-embed", "split", {
65
+ heading: input.form.heading ?? "Get in touch",
66
+ ...input.form.body ? { body: input.form.body } : { body: "Tell us a little about yourself and we'll get back to you." },
67
+ form: input.form.slug,
68
+ anchor: "contact"
69
+ });
70
+ }
71
+ function landingPage(input) {
72
+ const name = brandDisplayName(input.brand);
73
+ const cta = primaryCta(input);
74
+ const form = formNode(input);
75
+ const content = [
76
+ node("hero", "hero", "centered", {
77
+ heading: input.message.headline,
78
+ ...input.message.subhead ? { body: input.message.subhead } : {},
79
+ primaryCta: cta
80
+ }),
81
+ benefitsNode(input.message),
82
+ testimonialsNode(),
83
+ node("cta", "cta", "banner", { heading: "Ready when you are", primaryCta: cta, surface: "accent" })
84
+ ];
85
+ if (form) content.push(form);
86
+ return {
87
+ site: {
88
+ schemaVersion: 1,
89
+ name,
90
+ header: [node("nav", "nav", "minimal", { cta })],
91
+ nav: [],
92
+ footer: [footerNode(name, input.contact, "minimal")],
93
+ seo: seo(name, input.message)
94
+ },
95
+ pages: [{ path: "/", kind: "landing", doc: { schemaVersion: 1, root: { props: { title: input.message.headline.slice(0, 120) } }, content, seo: input.message.subhead ? { description: input.message.subhead.slice(0, 200) } : void 0 } }]
96
+ };
97
+ }
98
+ function onePageSite(input) {
99
+ const brand = { name: input.name, colors: input.colors, ...input.fonts ? { fonts: input.fonts } : {}, ...input.logo ? { logo: input.logo } : {} };
100
+ const cta = input.message.cta ?? (input.contact?.email ? { label: "Email us", href: `mailto:${input.contact.email}` } : { label: "Get in touch", href: "#contact" });
101
+ return {
102
+ brand,
103
+ site: {
104
+ schemaVersion: 1,
105
+ name: input.name,
106
+ header: [node("nav", "nav", "minimal", { cta })],
107
+ nav: [],
108
+ footer: [footerNode(input.name, input.contact)],
109
+ seo: seo(input.name, input.message)
110
+ },
111
+ pages: [
112
+ {
113
+ path: "/",
114
+ kind: "page",
115
+ doc: {
116
+ schemaVersion: 1,
117
+ root: { props: { title: input.message.headline.slice(0, 120) } },
118
+ content: [
119
+ node("hero", "hero", "centered", {
120
+ heading: input.message.headline,
121
+ ...input.message.subhead ? { body: input.message.subhead } : {},
122
+ primaryCta: cta
123
+ }),
124
+ node("contact", "cta", "card", { heading: `Talk to ${input.name}`, primaryCta: cta, anchor: "contact" })
125
+ ],
126
+ ...input.message.subhead ? { seo: { description: input.message.subhead.slice(0, 200) } } : {}
127
+ }
128
+ }
129
+ ]
130
+ };
131
+ }
132
+ function starterSite(input) {
133
+ const name = brandDisplayName(input.brand);
134
+ const cta = input.message.cta ?? { label: "Contact us", href: "/contact" };
135
+ const form = formNode(input);
136
+ const page = (title, content, description) => ({
137
+ schemaVersion: 1,
138
+ root: { props: { title } },
139
+ content,
140
+ ...description ? { seo: { description } } : {}
141
+ });
142
+ return {
143
+ site: {
144
+ schemaVersion: 1,
145
+ name,
146
+ header: [node("nav", "nav", "simple", { cta })],
147
+ nav: [
148
+ { label: "Home", href: "/" },
149
+ { label: "About", href: "/about" },
150
+ { label: "Contact", href: "/contact" }
151
+ ],
152
+ footer: [
153
+ node("footer", "footer", "columns", {
154
+ tagline: input.message.subhead?.slice(0, 200),
155
+ columns: [
156
+ { heading: name, links: [{ label: "About", href: "/about" }, { label: "Contact", href: "/contact" }] },
157
+ ...input.contact?.email || input.contact?.phone ? [
158
+ {
159
+ heading: "Get in touch",
160
+ links: [
161
+ ...input.contact.email ? [{ label: input.contact.email, href: `mailto:${input.contact.email}` }] : [],
162
+ ...input.contact.phone ? [{ label: input.contact.phone, href: `tel:${input.contact.phone.replace(/[^+\d]/g, "")}` }] : []
163
+ ]
164
+ }
165
+ ] : []
166
+ ],
167
+ copyright: `\xA9 ${year()} ${name}`
168
+ })
169
+ ],
170
+ seo: seo(name, input.message)
171
+ },
172
+ pages: [
173
+ {
174
+ path: "/",
175
+ kind: "page",
176
+ doc: page(
177
+ "Home",
178
+ [
179
+ node("hero", "hero", "split", {
180
+ heading: input.message.headline,
181
+ ...input.message.subhead ? { body: input.message.subhead } : {},
182
+ primaryCta: cta,
183
+ secondaryCta: { label: "About us", href: "/about" }
184
+ }),
185
+ benefitsNode(input.message),
186
+ testimonialsNode(),
187
+ node("cta", "cta", "banner", { heading: "Come and see for yourself", primaryCta: cta, surface: "accent" })
188
+ ],
189
+ input.message.subhead?.slice(0, 200)
190
+ )
191
+ },
192
+ {
193
+ path: "/about",
194
+ kind: "page",
195
+ doc: page(
196
+ "About",
197
+ [
198
+ node("hero", "hero", "centered", { eyebrow: "About", heading: `About ${name}` }),
199
+ node(
200
+ "story",
201
+ "rich-text",
202
+ "narrow",
203
+ {
204
+ body: {
205
+ type: "doc",
206
+ content: [
207
+ { type: "heading", attrs: { level: 2 }, content: [{ type: "text", text: "Our story" }] },
208
+ { type: "paragraph", content: [{ type: "text", text: "Tell people how you started, who you help and what you believe." }] }
209
+ ]
210
+ }
211
+ },
212
+ { placeholder: true }
213
+ )
214
+ ],
215
+ `About ${name}.`
216
+ )
217
+ },
218
+ {
219
+ path: "/contact",
220
+ kind: "page",
221
+ doc: page(
222
+ "Contact",
223
+ [
224
+ node("hero", "hero", "centered", { eyebrow: "Contact", heading: "Get in touch", ...input.message.subhead ? { body: input.message.subhead } : {} }),
225
+ ...form ? [form] : [node("contact", "cta", "card", { heading: `Talk to ${name}`, primaryCta: input.contact?.email ? { label: "Email us", href: `mailto:${input.contact.email}` } : cta })]
226
+ ],
227
+ `How to reach ${name}.`
228
+ )
229
+ }
230
+ ]
231
+ };
232
+ }
233
+
234
+ // src/core/examples.ts
235
+ var exampleBrands = Object.freeze({
236
+ riverside: {
237
+ name: "Riverside Gym",
238
+ colors: { primary: "#0f766e", primaryDark: "#2dd4bf" },
239
+ fonts: { heading: "space-grotesk", body: "inter" }
240
+ },
241
+ hilltop: {
242
+ name: "Hilltop Fitness",
243
+ colors: { primary: "#b45309", primaryDark: "#fbbf24" },
244
+ fonts: { heading: "playfair-display", body: "work-sans" }
245
+ }
246
+ });
247
+
248
+ export { exampleBrands, landingPage, onePageSite, starterSite };
249
+ //# sourceMappingURL=chunk-KPOX7D4S.js.map
250
+ //# sourceMappingURL=chunk-KPOX7D4S.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/templates.ts","../src/core/examples.ts"],"names":[],"mappings":"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","file":"chunk-KPOX7D4S.js","sourcesContent":["/**\n * Templates: whole sites from a little input.\n *\n * - `landingPage`: the one-click landing page from a brand and a message.\n * Nav, hero, features, testimonials, a call to action, a form embed and a\n * footer, on one page.\n * - `onePageSite`: the smallest site a brand with no site can be seeded\n * with: just a name, a logo, a message, and the brand's colours and fonts.\n * - `starterSite`: three pages (home, about, contact) with nav and footer.\n *\n * Each returns documents (`site`, `pages`) for any `SiteStore` to create.\n * Nothing is invented and passed off as real: copy a template can't know\n * (a client's quote, a statistic) is marked `placeholder`, and the publish\n * check refuses it until someone replaces it or hides the section.\n */\nimport type { Link, PageDoc, PageKind, SectionNode, SiteDocInput } from \"./schemas\";\nimport { brandDisplayName, type BrandInput, type SiteBrand } from \"./theme\";\n\nexport interface TemplateMessage {\n /** The hero's headline: what you do, for whom. */\n headline: string;\n /** One or two sentences under it. */\n subhead?: string;\n /** The main button. Default: \"Get in touch\", to the form (or `#contact`). */\n cta?: Link;\n /** What you offer, as the features band. Without it the band is a placeholder to fill in. */\n benefits?: { title: string; body: string }[];\n}\n\nexport interface TemplateInput {\n brand: BrandInput;\n message: TemplateMessage;\n /** A form to embed (its slug, for the host's forms resolver). */\n form?: { slug: string; heading?: string; body?: string };\n /** For the footer. */\n contact?: { email?: string; phone?: string };\n}\n\nexport interface TemplateResult {\n site: SiteDocInput;\n pages: { path: string; kind: PageKind; doc: PageDoc }[];\n}\n\nconst year = () => new Date().getUTCFullYear();\n\nfunction node(id: string, type: string, variant: string, props: Record<string, unknown>, extra: Partial<SectionNode> = {}): SectionNode {\n return { id, type, v: 1, variant, props, ...extra };\n}\n\nfunction footerNode(name: string, contact: TemplateInput[\"contact\"], variant = \"simple\"): SectionNode {\n const links: Link[] = [];\n if (contact?.email) links.push({ label: contact.email, href: `mailto:${contact.email}` });\n if (contact?.phone) links.push({ label: contact.phone, href: `tel:${contact.phone.replace(/[^+\\d]/g, \"\")}` });\n return node(\"footer\", \"footer\", variant, {\n ...(links.length ? { links } : {}),\n copyright: `© ${year()} ${name}`,\n });\n}\n\nfunction seo(name: string, message: TemplateMessage) {\n return {\n titleTemplate: `%s · ${name}`,\n ...(message.subhead ? { description: message.subhead.slice(0, 200) } : {}),\n organization: { name },\n };\n}\n\nfunction primaryCta(input: TemplateInput): Link {\n return input.message.cta ?? { label: \"Get in touch\", href: \"#contact\" };\n}\n\nfunction benefitsNode(message: TemplateMessage): SectionNode {\n if (message.benefits?.length) {\n return node(\"features\", \"features\", \"grid\", {\n heading: \"What you get\",\n items: message.benefits.slice(0, 12).map((b, i) => ({ title: b.title, body: b.body, icon: ([\"check\", \"star\", \"heart\", \"zap\", \"shield\", \"clock\"] as const)[i % 6] })),\n });\n }\n return node(\n \"features\",\n \"features\",\n \"grid\",\n {\n heading: \"What you get\",\n items: [\n { title: \"Your first offer\", body: \"Say what someone gets and why it matters to them.\", icon: \"check\" },\n { title: \"Your second offer\", body: \"Keep each one to a sentence or two.\", icon: \"star\" },\n { title: \"Your third offer\", body: \"Three is plenty for a landing page.\", icon: \"heart\" },\n ],\n },\n { placeholder: true },\n );\n}\n\nconst testimonialsNode = () =>\n node(\n \"testimonials\",\n \"testimonials\",\n \"grid\",\n {\n heading: \"What people say\",\n items: [\n { quote: \"Add a quote from someone you have helped, in their words and with their permission.\", name: \"Their name\", role: \"What they did with you\" },\n { quote: \"A second quote, from someone different.\", name: \"Their name\" },\n ],\n },\n { placeholder: true },\n );\n\nfunction formNode(input: TemplateInput): SectionNode | null {\n if (!input.form) return null;\n return node(\"contact\", \"form-embed\", \"split\", {\n heading: input.form.heading ?? \"Get in touch\",\n ...(input.form.body ? { body: input.form.body } : { body: \"Tell us a little about yourself and we'll get back to you.\" }),\n form: input.form.slug,\n anchor: \"contact\",\n });\n}\n\n/**\n * The one-click landing page: brand plus message, on one page.\n *\n * @stability Beta\n */\nexport function landingPage(input: TemplateInput): TemplateResult {\n const name = brandDisplayName(input.brand);\n const cta = primaryCta(input);\n const form = formNode(input);\n const content: SectionNode[] = [\n node(\"hero\", \"hero\", \"centered\", {\n heading: input.message.headline,\n ...(input.message.subhead ? { body: input.message.subhead } : {}),\n primaryCta: cta,\n }),\n benefitsNode(input.message),\n testimonialsNode(),\n node(\"cta\", \"cta\", \"banner\", { heading: \"Ready when you are\", primaryCta: cta, surface: \"accent\" }),\n ];\n if (form) content.push(form);\n return {\n site: {\n schemaVersion: 1,\n name,\n header: [node(\"nav\", \"nav\", \"minimal\", { cta })],\n nav: [],\n footer: [footerNode(name, input.contact, \"minimal\")],\n seo: seo(name, input.message),\n },\n pages: [{ path: \"/\", kind: \"landing\", doc: { schemaVersion: 1, root: { props: { title: input.message.headline.slice(0, 120) } }, content, seo: input.message.subhead ? { description: input.message.subhead.slice(0, 200) } : undefined } }],\n };\n}\n\nexport interface OnePageSiteInput {\n name: string;\n logo?: { src: string; alt?: string };\n message: TemplateMessage;\n /** The brand's colours and fonts. */\n colors: SiteBrand[\"colors\"];\n fonts?: SiteBrand[\"fonts\"];\n contact?: TemplateInput[\"contact\"];\n}\n\n/**\n * The smallest site, for seeding a brand that has none: a header, a hero,\n * a call to action and a footer. `brand` is the minimal brand to store\n * beside it (the host's `BrandSource` returns it).\n *\n * @stability Beta\n */\nexport function onePageSite(input: OnePageSiteInput): TemplateResult & { brand: SiteBrand } {\n const brand: SiteBrand = { name: input.name, colors: input.colors, ...(input.fonts ? { fonts: input.fonts } : {}), ...(input.logo ? { logo: input.logo } : {}) };\n const cta = input.message.cta ?? (input.contact?.email ? { label: \"Email us\", href: `mailto:${input.contact.email}` } : { label: \"Get in touch\", href: \"#contact\" });\n return {\n brand,\n site: {\n schemaVersion: 1,\n name: input.name,\n header: [node(\"nav\", \"nav\", \"minimal\", { cta })],\n nav: [],\n footer: [footerNode(input.name, input.contact)],\n seo: seo(input.name, input.message),\n },\n pages: [\n {\n path: \"/\",\n kind: \"page\",\n doc: {\n schemaVersion: 1,\n root: { props: { title: input.message.headline.slice(0, 120) } },\n content: [\n node(\"hero\", \"hero\", \"centered\", {\n heading: input.message.headline,\n ...(input.message.subhead ? { body: input.message.subhead } : {}),\n primaryCta: cta,\n }),\n node(\"contact\", \"cta\", \"card\", { heading: `Talk to ${input.name}`, primaryCta: cta, anchor: \"contact\" }),\n ],\n ...(input.message.subhead ? { seo: { description: input.message.subhead.slice(0, 200) } } : {}),\n },\n },\n ],\n };\n}\n\n/**\n * A three-page starter site: home, about and contact, with the menu, a\n * footer and redirects-ready paths.\n *\n * @stability Beta\n */\nexport function starterSite(input: TemplateInput): TemplateResult {\n const name = brandDisplayName(input.brand);\n const cta: Link = input.message.cta ?? { label: \"Contact us\", href: \"/contact\" };\n const form = formNode(input);\n const page = (title: string, content: SectionNode[], description?: string): PageDoc => ({\n schemaVersion: 1,\n root: { props: { title } },\n content,\n ...(description ? { seo: { description } } : {}),\n });\n return {\n site: {\n schemaVersion: 1,\n name,\n header: [node(\"nav\", \"nav\", \"simple\", { cta })],\n nav: [\n { label: \"Home\", href: \"/\" },\n { label: \"About\", href: \"/about\" },\n { label: \"Contact\", href: \"/contact\" },\n ],\n footer: [\n node(\"footer\", \"footer\", \"columns\", {\n tagline: input.message.subhead?.slice(0, 200),\n columns: [\n { heading: name, links: [{ label: \"About\", href: \"/about\" }, { label: \"Contact\", href: \"/contact\" }] },\n ...(input.contact?.email || input.contact?.phone\n ? [\n {\n heading: \"Get in touch\",\n links: [\n ...(input.contact.email ? [{ label: input.contact.email, href: `mailto:${input.contact.email}` }] : []),\n ...(input.contact.phone ? [{ label: input.contact.phone, href: `tel:${input.contact.phone.replace(/[^+\\d]/g, \"\")}` }] : []),\n ],\n },\n ]\n : []),\n ],\n copyright: `© ${year()} ${name}`,\n }),\n ],\n seo: seo(name, input.message),\n },\n pages: [\n {\n path: \"/\",\n kind: \"page\",\n doc: page(\n \"Home\",\n [\n node(\"hero\", \"hero\", \"split\", {\n heading: input.message.headline,\n ...(input.message.subhead ? { body: input.message.subhead } : {}),\n primaryCta: cta,\n secondaryCta: { label: \"About us\", href: \"/about\" },\n }),\n benefitsNode(input.message),\n testimonialsNode(),\n node(\"cta\", \"cta\", \"banner\", { heading: \"Come and see for yourself\", primaryCta: cta, surface: \"accent\" }),\n ],\n input.message.subhead?.slice(0, 200),\n ),\n },\n {\n path: \"/about\",\n kind: \"page\",\n doc: page(\n \"About\",\n [\n node(\"hero\", \"hero\", \"centered\", { eyebrow: \"About\", heading: `About ${name}` }),\n node(\n \"story\",\n \"rich-text\",\n \"narrow\",\n {\n body: {\n type: \"doc\",\n content: [\n { type: \"heading\", attrs: { level: 2 }, content: [{ type: \"text\", text: \"Our story\" }] },\n { type: \"paragraph\", content: [{ type: \"text\", text: \"Tell people how you started, who you help and what you believe.\" }] },\n ],\n },\n },\n { placeholder: true },\n ),\n ],\n `About ${name}.`,\n ),\n },\n {\n path: \"/contact\",\n kind: \"page\",\n doc: page(\n \"Contact\",\n [\n node(\"hero\", \"hero\", \"centered\", { eyebrow: \"Contact\", heading: \"Get in touch\", ...(input.message.subhead ? { body: input.message.subhead } : {}) }),\n ...(form ? [form] : [node(\"contact\", \"cta\", \"card\", { heading: `Talk to ${name}`, primaryCta: input.contact?.email ? { label: \"Email us\", href: `mailto:${input.contact.email}` } : cta })]),\n ],\n `How to reach ${name}.`,\n ),\n },\n ],\n };\n}\n","/**\n * Made-up brands for demos, tests and docs. They are not anyone's real\n * business.\n */\nimport type { SiteBrand } from \"./theme\";\n\nexport const exampleBrands = Object.freeze({\n riverside: {\n name: \"Riverside Gym\",\n colors: { primary: \"#0f766e\", primaryDark: \"#2dd4bf\" },\n fonts: { heading: \"space-grotesk\", body: \"inter\" },\n },\n hilltop: {\n name: \"Hilltop Fitness\",\n colors: { primary: \"#b45309\", primaryDark: \"#fbbf24\" },\n fonts: { heading: \"playfair-display\", body: \"work-sans\" },\n },\n} satisfies Record<string, SiteBrand>);\n"]}
@@ -0,0 +1,13 @@
1
+ // src/core/forms.ts
2
+ function isPlainEmail(value) {
3
+ return typeof value === "string" && value.length <= 254 && /^[A-Za-z0-9._+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(value);
4
+ }
5
+ function mailtoHref(form) {
6
+ if (!isPlainEmail(form.email)) return null;
7
+ const subject = typeof form.subject === "string" ? form.subject.replace(/[\r\n]+/g, " ").slice(0, 200).trim() : "";
8
+ return `mailto:${form.email}${subject ? `?subject=${encodeURIComponent(subject)}` : ""}`;
9
+ }
10
+
11
+ export { isPlainEmail, mailtoHref };
12
+ //# sourceMappingURL=chunk-VZUDRPFG.js.map
13
+ //# sourceMappingURL=chunk-VZUDRPFG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/forms.ts"],"names":[],"mappings":";AAyDO,SAAS,aAAa,KAAA,EAAiC;AAC5D,EAAA,OAAO,OAAO,UAAU,QAAA,IAAY,KAAA,CAAM,UAAU,GAAA,IAAO,sDAAA,CAAuD,KAAK,KAAK,CAAA;AAC9H;AAGO,SAAS,WAAW,IAAA,EAAgE;AACzF,EAAA,IAAI,CAAC,YAAA,CAAa,IAAA,CAAK,KAAK,GAAG,OAAO,IAAA;AACtC,EAAA,MAAM,UAAU,OAAO,IAAA,CAAK,OAAA,KAAY,QAAA,GAAW,KAAK,OAAA,CAAQ,OAAA,CAAQ,UAAA,EAAY,GAAG,EAAE,KAAA,CAAM,CAAA,EAAG,GAAG,CAAA,CAAE,MAAK,GAAI,EAAA;AAChH,EAAA,OAAO,CAAA,OAAA,EAAU,IAAA,CAAK,KAAK,CAAA,EAAG,OAAA,GAAU,YAAY,kBAAA,CAAmB,OAAO,CAAC,CAAA,CAAA,GAAK,EAAE,CAAA,CAAA;AACxF","file":"chunk-VZUDRPFG.js","sourcesContent":["/**\n * The form embed's contract with the host. A `form-embed` section names a\n * form by its slug; the host says what that slug is, through a resolver\n * passed to the renderer. This package assumes no forms table.\n *\n * - `definition`: a genesis-forms definition, sent to an endpoint. The page\n * renders it on the server (a plain HTML form when it needs no engine,\n * forms-web's `<Form>` and the form island otherwise), and answers are\n * POSTed as JSON (`{ form, answers }`) to `submitUrl`: a site path (the\n * host's own endpoint, for example one that runs genesis-forms\n * destinations) or an https URL (another service's endpoint).\n * - `mailto`: no form at all, a button that opens the visitor's email to\n * `email`. The fallback for a site with no endpoint yet: nothing is\n * stored, and a page never posts a form to `mailto:`.\n * - `embed`: an https URL the page shows in an iframe (a hosted form).\n */\nexport type ResolvedForm =\n | {\n kind: \"definition\";\n /** A canonical genesis-forms definition (`defineForm(...).definition`, or a stored one). */\n definition: unknown;\n /** Where the form POSTs `{ form, answers }` (form: the slug): a site path or an https URL. */\n submitUrl: string;\n layout?: \"classic\" | \"focus\";\n submitLabel?: string;\n }\n | {\n kind: \"mailto\";\n /** The address the button writes to. */\n email: string;\n /** The email's subject line. */\n subject?: string;\n /** The button's label (default \"Email us\"). */\n label?: string;\n /** A line above the button. */\n note?: string;\n }\n | {\n kind: \"embed\";\n /** An https URL. */\n url: string;\n /** The iframe's accessible name. */\n title: string;\n /** The iframe's height in CSS pixels (default 640). */\n height?: number;\n };\n\nexport interface FormsResolverContext {\n siteId: string;\n pagePath: string;\n mode: \"live\" | \"preview\";\n}\n\n/** slug → the form to show, or null when the host has no such form (the section then renders nothing and logs). */\nexport type FormsResolver = (slug: string, ctx: FormsResolverContext) => ResolvedForm | null | Promise<ResolvedForm | null>;\n\n/** A plain email address (no display name, no `%` escapes, no header injection): what a `mailto` form may write to. */\nexport function isPlainEmail(value: unknown): value is string {\n return typeof value === \"string\" && value.length <= 254 && /^[A-Za-z0-9._+-]+@[A-Za-z0-9-]+(?:\\.[A-Za-z0-9-]+)+$/.test(value);\n}\n\n/** The `mailto:` link for a mailto form, or null when its address isn't a plain one. */\nexport function mailtoHref(form: Extract<ResolvedForm, { kind: \"mailto\" }>): string | null {\n if (!isPlainEmail(form.email)) return null;\n const subject = typeof form.subject === \"string\" ? form.subject.replace(/[\\r\\n]+/g, \" \").slice(0, 200).trim() : \"\";\n return `mailto:${form.email}${subject ? `?subject=${encodeURIComponent(subject)}` : \"\"}`;\n}\n"]}
@@ -1,78 +1,30 @@
1
+ import { MAX_URL_LENGTH, isSafeHref, isSafeImageSrc, PAGE_PATH } from './chunk-3VKTBE7S.js';
1
2
  import { z } from 'zod';
2
3
 
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
4
  if (typeof z.toJSONSchema !== "function") {
67
5
  throw new Error("@marktiderman/genesis-sites needs zod 4 (zod@^4.0.0): z.toJSONSchema is missing.");
68
6
  }
69
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" });
70
9
  var Link = z.object({
71
10
  label: z.string().trim().min(1).max(80),
72
11
  href: Href,
73
- /** Opens in a new tab (external links only). */
74
- newTab: z.boolean().optional()
75
- }).meta({ title: "Link" });
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" });
76
28
  var Image = z.object({
77
29
  src: z.string().max(MAX_URL_LENGTH).refine(isSafeImageSrc, { message: "An image loads from a site path or an https URL." }),
78
30
  alt: z.string().max(300).default(""),
@@ -129,7 +81,7 @@ var PageDoc = z.object({
129
81
  content: z.array(SectionNode).max(60),
130
82
  seo: Seo.optional()
131
83
  });
132
- var PageKind = z.enum(["page", "landing"]);
84
+ var PageKind = z.enum(["page", "landing", "post"]);
133
85
  var PagePath = z.string().max(200).regex(PAGE_PATH, 'A page path looks like "/" or "/about".');
134
86
  var MenuItem = z.object({
135
87
  label: z.string().trim().min(1).max(40),
@@ -252,6 +204,7 @@ function definitionProblems(def) {
252
204
  if (!ids.has(def.defaultVariant)) problems.push(`${at}: \`defaultVariant\` "${String(def.defaultVariant)}" is not one of its variants`);
253
205
  if (def.limit !== void 0 && (!Number.isInteger(def.limit) || def.limit < 1)) problems.push(`${at}: \`limit\` is a whole number from 1`);
254
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`);
255
208
  for (const [key, fn] of Object.entries(def.migrations ?? {})) {
256
209
  const n = Number(key);
257
210
  if (!Number.isInteger(n) || n < 2 || n > def.version) problems.push(`${at}: migration ${key} must be between 2 and the version (${def.version})`);
@@ -301,7 +254,14 @@ var BUILT_IN_SECTION_TYPES = Object.freeze([
301
254
  "gallery",
302
255
  "rich-text",
303
256
  "form-embed",
304
- "footer"
257
+ "footer",
258
+ "schedule",
259
+ "team",
260
+ "blog-list",
261
+ "post-body",
262
+ "map",
263
+ "timeline",
264
+ "file"
305
265
  ]);
306
266
  function registerSection(def, options = {}) {
307
267
  const frozen = Object.isFrozen(def) && definitionProblems(def).length === 0 ? def : defineSection(def);
@@ -366,6 +326,68 @@ function nodeFromPreset(type, presetName, id = newSectionId(type.replace(/-/g, "
366
326
  if (!preset) throw new Error(`nodeFromPreset: "${type}" has no preset "${presetName}".`);
367
327
  return { id, type, v: def.version, variant: preset.variant, props: structuredClone(preset.props) };
368
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
+ }
369
391
 
370
392
  // src/core/seo.ts
371
393
  function absoluteUrl(origin, basePath, path) {
@@ -384,7 +406,7 @@ function pageTitle(site, doc, _path) {
384
406
  if (!template || !template.includes("%s")) return title;
385
407
  return template.replace("%s", title);
386
408
  }
387
- function pageMeta({ site, page, origin, basePath = "", preview = false }) {
409
+ function pageMeta({ site, page, origin, basePath = "", preview = false, article }) {
388
410
  const doc = page.doc;
389
411
  const title = pageTitle(site, doc, page.path);
390
412
  const description = doc.seo?.description?.trim() || site.seo.description?.trim() || void 0;
@@ -400,6 +422,13 @@ function pageMeta({ site, page, origin, basePath = "", preview = false }) {
400
422
  "og:locale": site.lang.replace("-", "_")
401
423
  };
402
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
+ }
403
432
  if (img) {
404
433
  openGraph["og:image"] = img;
405
434
  if (image?.alt) openGraph["og:image:alt"] = image.alt;
@@ -462,18 +491,85 @@ function breadcrumbJsonLd(pages, page, origin, basePath = "") {
462
491
  }))
463
492
  };
464
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);
465
553
  function safeJsonForHtml(value) {
466
554
  return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
467
555
  }
468
556
  function escapeXml(text) {
469
557
  return text.replace(/[<>&'"]/g, (c) => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;", "'": "&apos;", '"': "&quot;" })[c]);
470
558
  }
471
- function sitemapXml({ site, pages, origin, basePath = "", lastModified }) {
559
+ function sitemapXml({ site, pages, origin, basePath = "", lastModified, posts }) {
472
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) => {
473
561
  const loc = `<loc>${escapeXml(absoluteUrl(origin, basePath, p.path))}</loc>`;
474
562
  const mod = lastModified ? `<lastmod>${escapeXml(new Date(lastModified).toISOString())}</lastmod>` : "";
475
563
  return `<url>${loc}${mod}</url>`;
476
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
+ }
477
573
  return `<?xml version="1.0" encoding="UTF-8"?>
478
574
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${urls.join("")}</urlset>
479
575
  `;
@@ -494,6 +590,6 @@ function matchRedirect(redirects, path) {
494
590
  return redirects.find((r) => r.from === path && isSafeHref(r.to));
495
591
  }
496
592
 
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
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