@xiwebdesign/xicms-core 7.23.0 → 7.24.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 (97) hide show
  1. package/dist/admin/AdminApp.js +2 -1
  2. package/dist/admin/AdminApp.js.map +1 -1
  3. package/dist/admin/CoreVersion.d.ts +6 -0
  4. package/dist/admin/CoreVersion.js +42 -1
  5. package/dist/admin/CoreVersion.js.map +1 -1
  6. package/dist/admin/SuperadminPanel.js +19 -3
  7. package/dist/admin/SuperadminPanel.js.map +1 -1
  8. package/dist/admin/dashboard/DashboardPanel.js +46 -9
  9. package/dist/admin/dashboard/DashboardPanel.js.map +1 -1
  10. package/dist/admin/forms/FormsPanel.d.ts +9 -0
  11. package/dist/admin/forms/FormsPanel.js +102 -16
  12. package/dist/admin/forms/FormsPanel.js.map +1 -1
  13. package/dist/admin/forms/gezien.d.ts +3 -0
  14. package/dist/admin/forms/gezien.js +22 -0
  15. package/dist/admin/forms/gezien.js.map +1 -0
  16. package/dist/admin/lib/kleur.d.ts +32 -0
  17. package/dist/admin/lib/kleur.js +60 -0
  18. package/dist/admin/lib/kleur.js.map +1 -0
  19. package/dist/admin/lib/pagesClient.d.ts +32 -0
  20. package/dist/admin/lib/pagesClient.js +60 -1
  21. package/dist/admin/lib/pagesClient.js.map +1 -1
  22. package/dist/admin/lib/seoHulp.d.ts +28 -0
  23. package/dist/admin/lib/seoHulp.js +33 -0
  24. package/dist/admin/lib/seoHulp.js.map +1 -0
  25. package/dist/admin/media/MediaManager.js +133 -51
  26. package/dist/admin/media/MediaManager.js.map +1 -1
  27. package/dist/admin/pages/PageEditor.d.ts +14 -0
  28. package/dist/admin/pages/PageEditor.js +192 -123
  29. package/dist/admin/pages/PageEditor.js.map +1 -1
  30. package/dist/admin/pages/PagesPanel.js +88 -40
  31. package/dist/admin/pages/PagesPanel.js.map +1 -1
  32. package/dist/admin/settings/SettingsPanel.js +100 -19
  33. package/dist/admin/settings/SettingsPanel.js.map +1 -1
  34. package/dist/admin/shop/ProductsTab.js +1 -1
  35. package/dist/admin/shop/ProductsTab.js.map +1 -1
  36. package/dist/admin/shop/ShopPanel.js +5 -2
  37. package/dist/admin/shop/ShopPanel.js.map +1 -1
  38. package/dist/admin/stats/StatsPanel.js +30 -2
  39. package/dist/admin/stats/StatsPanel.js.map +1 -1
  40. package/dist/admin/stats/lib.d.ts +14 -1
  41. package/dist/admin/stats/lib.js +46 -2
  42. package/dist/admin/stats/lib.js.map +1 -1
  43. package/dist/admin/visual/VisualEditor.js +80 -4
  44. package/dist/admin/visual/VisualEditor.js.map +1 -1
  45. package/dist/admin.css +1 -1
  46. package/dist/blocks/builtins/FormBlockRender.js +8 -2
  47. package/dist/blocks/builtins/FormBlockRender.js.map +1 -1
  48. package/dist/blocks/builtins/lib/submitForm.d.ts +3 -1
  49. package/dist/blocks/builtins/lib/submitForm.js +2 -2
  50. package/dist/blocks/builtins/lib/submitForm.js.map +1 -1
  51. package/dist/components/EditBar.d.ts +3 -1
  52. package/dist/components/EditBar.js +30 -4
  53. package/dist/components/EditBar.js.map +1 -1
  54. package/dist/components/News.js +8 -4
  55. package/dist/components/News.js.map +1 -1
  56. package/dist/components/PageView.d.ts +5 -0
  57. package/dist/components/PageView.js +24 -2
  58. package/dist/components/PageView.js.map +1 -1
  59. package/dist/components/ResponsiveImage.d.ts +1 -1
  60. package/dist/components/ResponsiveImage.js +3 -1
  61. package/dist/components/ResponsiveImage.js.map +1 -1
  62. package/dist/components/site/SiteNav.d.ts +29 -0
  63. package/dist/components/site/SiteNav.js +82 -0
  64. package/dist/components/site/SiteNav.js.map +1 -0
  65. package/dist/content/types.d.ts +8 -0
  66. package/dist/index.d.ts +3 -1
  67. package/dist/index.js +8 -1
  68. package/dist/index.js.map +1 -1
  69. package/dist/media/ImageContext.d.ts +1 -6
  70. package/dist/media/ImageContext.js +6 -1
  71. package/dist/media/ImageContext.js.map +1 -1
  72. package/dist/releaseNotes.d.ts +15 -0
  73. package/dist/releaseNotes.js +28 -0
  74. package/dist/releaseNotes.js.map +1 -0
  75. package/dist/schema/settings.d.ts +105 -0
  76. package/dist/schema/settings.js +45 -5
  77. package/dist/schema/settings.js.map +1 -1
  78. package/dist/schema/siteConfig.d.ts +9 -0
  79. package/dist/server/blockData.js +6 -2
  80. package/dist/server/blockData.js.map +1 -1
  81. package/dist/server/draft.d.ts +11 -0
  82. package/dist/server/draft.js +20 -1
  83. package/dist/server/draft.js.map +1 -1
  84. package/dist/server/index.d.ts +3 -1
  85. package/dist/server/index.js +7 -1
  86. package/dist/server/index.js.map +1 -1
  87. package/dist/server/pages.js +4 -1
  88. package/dist/server/pages.js.map +1 -1
  89. package/dist/server/previewToken.d.ts +34 -0
  90. package/dist/server/previewToken.js +44 -0
  91. package/dist/server/previewToken.js.map +1 -0
  92. package/dist/server/redirects.d.ts +15 -0
  93. package/dist/server/redirects.js +15 -0
  94. package/dist/server/redirects.js.map +1 -0
  95. package/dist/styles.css +110 -0
  96. package/dist/version.js +1 -1
  97. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import * as z from "zod/mini";
2
2
  import { SeoSchema } from "./seo.js";
3
- const NavItemSchema = z.object({
3
+ const NavLeafSchema = z.object({
4
4
  label: z.string().check(z.minLength(1)).check(z.maxLength(80)),
5
5
  href: z.string().check(z.minLength(1)).check(z.maxLength(2e3)),
6
6
  /**
@@ -9,9 +9,42 @@ const NavItemSchema = z.object({
9
9
  */
10
10
  target: z.optional(z.enum(["_self", "_blank", "_embed"]))
11
11
  });
12
+ const NavItemSchema = z.object({
13
+ ...NavLeafSchema.shape,
14
+ /**
15
+ * Submenu (7.24): één niveau diep, bewust niet recursief. "Diensten ▾ Tuinaanleg / Onderhoud"
16
+ * dekt vrijwel elke bedrijfssite; een tweede niveau is op mobiel niet meer te bedienen en op
17
+ * desktop een hover-doolhof. Een item mét kinderen blijft zelf klikbaar (href = de
18
+ * overzichtspagina), zodat het menu ook zonder JavaScript en met het toetsenbord werkt.
19
+ */
20
+ children: z.optional(z.array(NavLeafSchema).check(z.maxLength(20)))
21
+ });
12
22
  function isExterneLink(href) {
13
23
  return /^https?:\/\//i.test(href);
14
24
  }
25
+ const RedirectSchema = z.object({
26
+ from: z.string().check(z.minLength(1)).check(z.maxLength(400)).check(z.regex(/^\/[^\s?#]*$/, "een pad begint met / en bevat geen spaties")),
27
+ to: z.string().check(z.minLength(1)).check(z.maxLength(400)).check(z.regex(/^\/[^\s]*$/, "een pad begint met /"))
28
+ });
29
+ function normaliseerPad(pad) {
30
+ const p = pad.split("?")[0].split("#")[0].replace(/\/+$/, "");
31
+ return p === "" ? "/" : p.startsWith("/") ? p : `/${p}`;
32
+ }
33
+ function zoekRedirect(pad, redirects) {
34
+ if (!redirects?.length) return null;
35
+ const kaart = new Map(redirects.map((r) => [normaliseerPad(r.from), normaliseerPad(r.to)]));
36
+ let huidig = normaliseerPad(pad);
37
+ const gezien = /* @__PURE__ */ new Set([huidig]);
38
+ let doel = null;
39
+ for (let i = 0; i < 5; i++) {
40
+ const volgende = kaart.get(huidig);
41
+ if (!volgende || gezien.has(volgende)) break;
42
+ gezien.add(volgende);
43
+ doel = volgende;
44
+ huidig = volgende;
45
+ }
46
+ return doel;
47
+ }
15
48
  const EMBED_PAD = "/extern";
16
49
  function navHref(item) {
17
50
  if (item.target === "_embed" && isExterneLink(item.href)) {
@@ -130,7 +163,9 @@ const SiteSettingsSchema = z.object({
130
163
  dashboardLinks: z.optional(z.array(z.object({
131
164
  label: z.string().check(z.minLength(1)).check(z.maxLength(80)),
132
165
  href: z.string().check(z.minLength(1)).check(z.maxLength(2e3))
133
- })).check(z.maxLength(12)))
166
+ })).check(z.maxLength(12))),
167
+ /** Doorverwijzingen van oude naar nieuwe paden (7.24). Publiek: de catch-all leest ze. */
168
+ redirects: z.optional(z.array(RedirectSchema).check(z.maxLength(200)))
134
169
  });
135
170
  const PublicSiteSettingsSchema = z.object({
136
171
  name: z.optional(z.string().check(z.maxLength(160))),
@@ -138,7 +173,8 @@ const PublicSiteSettingsSchema = z.object({
138
173
  theme: z.optional(ThemeSchema),
139
174
  seoDefaults: z.optional(SeoSchema),
140
175
  footer: z.optional(FooterSettingsSchema),
141
- modules: z.optional(ModulesSettingsSchema)
176
+ modules: z.optional(ModulesSettingsSchema),
177
+ redirects: z.optional(z.array(RedirectSchema).check(z.maxLength(200)))
142
178
  });
143
179
  function toPublicSettings(s) {
144
180
  return {
@@ -147,7 +183,8 @@ function toPublicSettings(s) {
147
183
  theme: s.theme,
148
184
  seoDefaults: s.seoDefaults,
149
185
  footer: s.footer,
150
- modules: s.modules
186
+ modules: s.modules,
187
+ redirects: s.redirects
151
188
  };
152
189
  }
153
190
  export {
@@ -158,6 +195,7 @@ export {
158
195
  ModulesSettingsSchema,
159
196
  NavItemSchema,
160
197
  PublicSiteSettingsSchema,
198
+ RedirectSchema,
161
199
  SiteSettingsSchema,
162
200
  SocialLinkSchema,
163
201
  ThemeSchema,
@@ -165,6 +203,8 @@ export {
165
203
  isToegestaneEmbed,
166
204
  mergeThemeTokens,
167
205
  navHref,
168
- toPublicSettings
206
+ normaliseerPad,
207
+ toPublicSettings,
208
+ zoekRedirect
169
209
  };
170
210
  //# sourceMappingURL=settings.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/schema/settings.ts"],"sourcesContent":["import * as z from \"zod/mini\";\nimport { SeoSchema } from \"./seo\";\n\n/**\n * Eén navigatie-item.\n *\n * `target` is OPTIONEEL en dat is opzet: elk bestaand menu in Git en in Firestore mist dit veld, en\n * moet blijven valideren. Ontbreekt het, dan opent de link in hetzelfde venster — precies wat een\n * gewone `<a href>` altijd al deed.\n *\n * Er staat bewust geen `type: \"page\" | \"extern\"` in. Dat onderscheid is uit de href zelf af te\n * lezen (begint hij met `/` of met `http`), en een apart veld zou een tweede waarheid introduceren\n * die uit de pas kan lopen met de href. Het beheerscherm mag de gebruiker wél die keuze voorleggen;\n * wat er wordt opgeslagen is één ondubbelzinnig adres.\n */\nexport const NavItemSchema = z.object({\n label: z.string().check(z.minLength(1)).check(z.maxLength(80)),\n href: z.string().check(z.minLength(1)).check(z.maxLength(2000)),\n /**\n * `_embed` toont een externe pagina in een kader BINNEN de site, onder de logo/menubalk, alsof\n * hij erbij hoort. Alleen zinvol bij een externe href.\n */\n target: z.optional(z.enum([\"_self\", \"_blank\", \"_embed\"])),\n});\nexport type NavItem = z.infer<typeof NavItemSchema>;\n\n/** Verwijst dit item naar buiten? Afgeleid uit de href, zodat er geen tweede waarheid ontstaat. */\nexport function isExterneLink(href: string): boolean {\n return /^https?:\\/\\//i.test(href);\n}\n\n/** Het pad waarop een ingekaderde externe pagina wordt getoond. */\nexport const EMBED_PAD = \"/extern\";\n\n/**\n * De href waar dit menu-item feitelijk heen linkt.\n *\n * Bij `_embed` is dat niet de externe URL zelf maar de eigen `/extern`-route, die hem in een kader\n * binnen de site toont. De externe URL gaat als parameter mee; de route controleert vervolgens of\n * dat adres écht in het menu staat vóór hij het inlaadt. Zonder die controle zou iedereen een\n * willekeurige pagina in jouw huisstijl kunnen laten verschijnen door de URL te bewerken.\n */\nexport function navHref(item: NavItem): string {\n if (item.target === \"_embed\" && isExterneLink(item.href)) {\n return `${EMBED_PAD}?url=${encodeURIComponent(item.href)}`;\n }\n return item.href;\n}\n\n/** Staat dit adres als ingekaderd menu-item in de navigatie? Zo niet, dan niet tonen. */\nexport function isToegestaneEmbed(url: string, nav: NavItem[] | undefined): boolean {\n return (nav ?? []).some((i) => i.target === \"_embed\" && i.href === url);\n}\n\n/** Huisstijl-tokens (worden CSS custom properties). Alle uitbreidingen zijn optioneel/update-veilig. */\nexport const ThemeSchema = z.object({\n primary: z.string().check(z.maxLength(40)),\n accent: z.optional(z.string().check(z.maxLength(40))),\n /** Logo-URL (absoluut, https) — gebruikt in de mail-header en op de factuur-PDF. */\n logo: z.optional(z.string().check(z.maxLength(500))),\n background: z.optional(z.string().check(z.maxLength(40))),\n foreground: z.optional(z.string().check(z.maxLength(40))),\n fontFamily: z.optional(z.string().check(z.maxLength(200))),\n /** Optioneel apart lettertype voor koppen (display/serif), voor een elegantere typografie. */\n headingFont: z.optional(z.string().check(z.maxLength(200))),\n /** Optionele stylesheet-URL voor het lettertype (bv. een Google Fonts CSS-link). */\n fontUrl: z.optional(z.string().check(z.maxLength(500))),\n /** Afrondingsschaal (sm/md/lg). */\n radius: z.optional(z.object({ sm: z.optional(z.string().check(z.maxLength(20))), md: z.optional(z.string().check(z.maxLength(20))), lg: z.optional(z.string().check(z.maxLength(20))) })),\n /** Ruimteschaal voor block-padding (xs..xl). */\n spacing: z.optional(z.object({\n xs: z.optional(z.string().check(z.maxLength(20))),\n sm: z.optional(z.string().check(z.maxLength(20))),\n md: z.optional(z.string().check(z.maxLength(20))),\n lg: z.optional(z.string().check(z.maxLength(20))),\n xl: z.optional(z.string().check(z.maxLength(20))),\n })),\n /** Maximale container-breedte + horizontale padding. */\n container: z.optional(z.object({ maxWidth: z.optional(z.string().check(z.maxLength(20))), padding: z.optional(z.string().check(z.maxLength(20))) })),\n /** Donkere modus: uit, altijd aan, of meebewegen met het systeem van de bezoeker. */\n dark: z.optional(z.object({\n /**\n * \"auto\" — volgt het besturingssysteem van de bezoeker (light-dark()).\n * \"dark\" — de site is altijd donker, ongeacht de bezoeker.\n * \"light\" — de site is altijd licht (gelijk aan geen donkere modus).\n *\n * Vervangt de oude `auto`-boolean, die alleen \"volg het systeem\" of \"niets\" kon zeggen —\n * \"deze site is gewoon donker\" was daarmee niet uit te drukken. `auto` blijft gelezen worden\n * zodat bestaande site-configs blijven werken (zie themeMode in XiCMSProvider).\n */\n mode: z.optional(z.enum([\"light\", \"dark\", \"auto\"])),\n /**\n * @deprecated Gebruik `mode`. Blijft werken: true → \"auto\", afwezig/false → \"light\".\n * OPTIONEEL (niet `_default(false)`): met een verplichte default draagt elk gemerged\n * dark-object een `auto`, en dan botst een partiële update uit /beheer op het type.\n */\n auto: z.optional(z.boolean()),\n background: z.optional(z.string().check(z.maxLength(40))),\n foreground: z.optional(z.string().check(z.maxLength(40))),\n primary: z.optional(z.string().check(z.maxLength(40))),\n })),\n});\nexport type ThemeTokens = z.infer<typeof ThemeSchema>;\n\n/**\n * Voegt live thema-instellingen (uit `settings/public.theme`, /beheer) per token samen met het\n * statische SiteConfig-thema. Firestore wint per veld waar gezet; geneste schalen (radius/spacing/\n * container/dark) mergen per subveld, zodat een klant die alleen kleuren wijzigt de fonts/radius\n * uit de repo behoudt. Puur (geen React) — gedeeld door provider en `useMergedSiteConfig`.\n */\nexport function mergeThemeTokens(base: ThemeTokens, override?: ThemeTokens): ThemeTokens {\n if (!override) return base;\n const merged: ThemeTokens = { ...base, ...override };\n for (const key of [\"radius\", \"spacing\", \"container\", \"dark\"] as const) {\n if (base[key] && override[key]) {\n merged[key] = { ...base[key], ...override[key] } as never;\n } else if (base[key] && !override[key]) {\n merged[key] = base[key] as never;\n }\n }\n return merged;\n}\n\n/**\n * Afzender-instelling voor formulier-e-mail. Een provider mag alleen versturen vanaf een\n * geverifieerd domein, dus dit is per site instelbaar:\n * - mode \"xicms\": from = verifiedSender (XiCMS-domein), replyTo = inzender/klant. Werkt direct.\n * - mode \"client\": from = het geverifieerde eigen domein van de klant.\n */\nexport const FormsSenderSchema = z.object({\n mode: z._default(z.enum([\"xicms\", \"client\"]), \"xicms\"),\n /** Geverifieerd from-adres (verplicht ongeacht mode; bij \"client\" het klant-domein). */\n fromEmail: z.optional(z.email()),\n fromName: z.optional(z.string().check(z.maxLength(120))),\n /** Waar notificaties heen gaan (meestal het klant-mailadres). */\n notifyTo: z.optional(z.email()),\n});\nexport type FormsSender = z.infer<typeof FormsSenderSchema>;\n\n/** Eén social-link (platform + url). */\nexport const SocialLinkSchema = z.object({\n platform: z.string().check(z.minLength(1)).check(z.maxLength(40)),\n href: z.string().check(z.minLength(1)).check(z.maxLength(2000)),\n});\nexport type SocialLink = z.infer<typeof SocialLinkSchema>;\n\n/** Eén footer-kolom: kop + links. */\nexport const FooterColumnSchema = z.object({\n heading: z.optional(z.string().check(z.maxLength(120))),\n links: z._default(z.array(NavItemSchema).check(z.maxLength(20)), []),\n});\n\n/** Bewerkbare footer (klant past dit aan in /beheer; core levert de unstyled `SiteFooter`). */\nexport const FooterSettingsSchema = z.object({\n columns: z.optional(z.array(FooterColumnSchema).check(z.maxLength(6))),\n social: z.optional(z.array(SocialLinkSchema).check(z.maxLength(12))),\n copyright: z.optional(z.string().check(z.maxLength(300))),\n /** Toon de \"Beheer\"-link in de footer (default: aan). */\n showAdminLink: z.optional(z.boolean()),\n});\nexport type FooterSettings = z.infer<typeof FooterSettingsSchema>;\n\n/** Per-module activatie/instellingen (spiegelt ModulesSiteConfig; permissief gehouden). */\nexport const ModulesSettingsSchema = z.record(\n z.string(),\n z.union([\n z.boolean(),\n z.object({ enabled: z.optional(z.boolean()), config: z.optional(z.record(z.string(), z.unknown())) }),\n ]),\n);\n\n/** Het `sites/{siteId}/settings/site`-document: alle bewerkbare site-instellingen. */\nexport const SiteSettingsSchema = z.object({\n name: z.optional(z.string().check(z.minLength(1)).check(z.maxLength(160))),\n // Zelfde hostname-regel als SiteConfigSchema.domain (geen protocol/pad).\n domain: z.union([z.optional(z.string().check(z.maxLength(255)).check(z.regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/i, \"alleen een hostname, bv. 'www.klant.nl' (zonder https:// of pad)\"))), z.literal(\"\")]),\n contactEmail: z.optional(z.email()),\n nav: z.optional(z.array(NavItemSchema).check(z.maxLength(50))),\n theme: z.optional(ThemeSchema),\n seoDefaults: z.optional(SeoSchema),\n forms: z.optional(FormsSenderSchema),\n footer: z.optional(FooterSettingsSchema),\n modules: z.optional(ModulesSettingsSchema),\n /**\n * Snelkoppelingen op de beheer-startpagina (7.16). Beheer-intern: bewust NIET in de publieke\n * subset. Opslaan loopt via de bestaande saveSiteSettings-callable — die is passthrough met\n * merge, dus dit veld vergt geen functions-wijziging.\n */\n dashboardLinks: z.optional(z.array(z.object({\n label: z.string().check(z.minLength(1)).check(z.maxLength(80)),\n href: z.string().check(z.minLength(1)).check(z.maxLength(2000)),\n })).check(z.maxLength(12))),\n});\nexport type SiteSettings = z.infer<typeof SiteSettingsSchema>;\n\n/**\n * Publiek-veilige presentatie-instellingen (`sites/{siteId}/settings/public`): wordt door de live\n * site gelezen (footer, thema, nav, modules) zónder de gevoelige velden (e-mailafzender, contact)\n * uit `settings/site` bloot te geven.\n */\nexport const PublicSiteSettingsSchema = z.object({\n name: z.optional(z.string().check(z.maxLength(160))),\n nav: z.optional(z.array(NavItemSchema).check(z.maxLength(50))),\n theme: z.optional(ThemeSchema),\n seoDefaults: z.optional(SeoSchema),\n footer: z.optional(FooterSettingsSchema),\n modules: z.optional(ModulesSettingsSchema),\n});\nexport type PublicSiteSettings = z.infer<typeof PublicSiteSettingsSchema>;\n\n/** Haalt de publiek-veilige subset uit de volledige settings (voor het `settings/public`-doc). */\nexport function toPublicSettings(s: SiteSettings): PublicSiteSettings {\n return {\n name: s.name,\n nav: s.nav,\n theme: s.theme,\n seoDefaults: s.seoDefaults,\n footer: s.footer,\n modules: s.modules,\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/schema/settings.ts"],"sourcesContent":["import * as z from \"zod/mini\";\nimport { SeoSchema } from \"./seo\";\n\n/**\n * Eén navigatie-item.\n *\n * `target` is OPTIONEEL en dat is opzet: elk bestaand menu in Git en in Firestore mist dit veld, en\n * moet blijven valideren. Ontbreekt het, dan opent de link in hetzelfde venster — precies wat een\n * gewone `<a href>` altijd al deed.\n *\n * Er staat bewust geen `type: \"page\" | \"extern\"` in. Dat onderscheid is uit de href zelf af te\n * lezen (begint hij met `/` of met `http`), en een apart veld zou een tweede waarheid introduceren\n * die uit de pas kan lopen met de href. Het beheerscherm mag de gebruiker wél die keuze voorleggen;\n * wat er wordt opgeslagen is één ondubbelzinnig adres.\n */\nconst NavLeafSchema = z.object({\n label: z.string().check(z.minLength(1)).check(z.maxLength(80)),\n href: z.string().check(z.minLength(1)).check(z.maxLength(2000)),\n /**\n * `_embed` toont een externe pagina in een kader BINNEN de site, onder de logo/menubalk, alsof\n * hij erbij hoort. Alleen zinvol bij een externe href.\n */\n target: z.optional(z.enum([\"_self\", \"_blank\", \"_embed\"])),\n});\nexport const NavItemSchema = z.object({\n ...NavLeafSchema.shape,\n /**\n * Submenu (7.24): één niveau diep, bewust niet recursief. \"Diensten ▾ Tuinaanleg / Onderhoud\"\n * dekt vrijwel elke bedrijfssite; een tweede niveau is op mobiel niet meer te bedienen en op\n * desktop een hover-doolhof. Een item mét kinderen blijft zelf klikbaar (href = de\n * overzichtspagina), zodat het menu ook zonder JavaScript en met het toetsenbord werkt.\n */\n children: z.optional(z.array(NavLeafSchema).check(z.maxLength(20))),\n});\nexport type NavItem = z.infer<typeof NavItemSchema>;\nexport type NavLeaf = z.infer<typeof NavLeafSchema>;\n\n/** Verwijst dit item naar buiten? Afgeleid uit de href, zodat er geen tweede waarheid ontstaat. */\nexport function isExterneLink(href: string): boolean {\n return /^https?:\\/\\//i.test(href);\n}\n\n/**\n * Eén doorverwijzing (7.24): een oud pad dat permanent naar een nieuw pad wijst.\n *\n * Ontstaat vanzelf wanneer een klant het pad van een pagina wijzigt — de oude URL staat in\n * zoekmachines, in mails en op visitekaartjes, en zonder redirect wordt dat een 404. Alleen\n * interne paden (beginnen met `/`): een open redirect naar buiten is een phishing-hulpmiddel.\n */\nexport const RedirectSchema = z.object({\n from: z.string().check(z.minLength(1)).check(z.maxLength(400)).check(z.regex(/^\\/[^\\s?#]*$/, \"een pad begint met / en bevat geen spaties\")),\n to: z.string().check(z.minLength(1)).check(z.maxLength(400)).check(z.regex(/^\\/[^\\s]*$/, \"een pad begint met /\")),\n});\nexport type Redirect = z.infer<typeof RedirectSchema>;\n\n/**\n * Normaliseert een pad zoals de catch-all dat doet (\"/over-ons/\" → \"/over-ons\", \"\" → \"/\"), zodat\n * een redirect matcht ongeacht een afsluitende slash.\n */\nexport function normaliseerPad(pad: string): string {\n const p = pad.split(\"?\")[0].split(\"#\")[0].replace(/\\/+$/, \"\");\n return p === \"\" ? \"/\" : p.startsWith(\"/\") ? p : `/${p}`;\n}\n\n/**\n * Zoekt de bestemming voor een pad in de redirect-lijst. Volgt ketens (a→b, b→c) tot een diepte\n * van vijf en weigert lussen: een klant die eerst /a naar /b hernoemt en later terug, mag daar\n * geen eindeloze redirect aan overhouden. `null` = geen doorverwijzing.\n */\nexport function zoekRedirect(pad: string, redirects: Redirect[] | undefined): string | null {\n if (!redirects?.length) return null;\n const kaart = new Map(redirects.map((r) => [normaliseerPad(r.from), normaliseerPad(r.to)]));\n let huidig = normaliseerPad(pad);\n const gezien = new Set<string>([huidig]);\n let doel: string | null = null;\n for (let i = 0; i < 5; i++) {\n const volgende = kaart.get(huidig);\n if (!volgende || gezien.has(volgende)) break;\n gezien.add(volgende);\n doel = volgende;\n huidig = volgende;\n }\n return doel;\n}\n\n/** Het pad waarop een ingekaderde externe pagina wordt getoond. */\nexport const EMBED_PAD = \"/extern\";\n\n/**\n * De href waar dit menu-item feitelijk heen linkt.\n *\n * Bij `_embed` is dat niet de externe URL zelf maar de eigen `/extern`-route, die hem in een kader\n * binnen de site toont. De externe URL gaat als parameter mee; de route controleert vervolgens of\n * dat adres écht in het menu staat vóór hij het inlaadt. Zonder die controle zou iedereen een\n * willekeurige pagina in jouw huisstijl kunnen laten verschijnen door de URL te bewerken.\n */\nexport function navHref(item: NavItem): string {\n if (item.target === \"_embed\" && isExterneLink(item.href)) {\n return `${EMBED_PAD}?url=${encodeURIComponent(item.href)}`;\n }\n return item.href;\n}\n\n/** Staat dit adres als ingekaderd menu-item in de navigatie? Zo niet, dan niet tonen. */\nexport function isToegestaneEmbed(url: string, nav: NavItem[] | undefined): boolean {\n return (nav ?? []).some((i) => i.target === \"_embed\" && i.href === url);\n}\n\n/** Huisstijl-tokens (worden CSS custom properties). Alle uitbreidingen zijn optioneel/update-veilig. */\nexport const ThemeSchema = z.object({\n primary: z.string().check(z.maxLength(40)),\n accent: z.optional(z.string().check(z.maxLength(40))),\n /** Logo-URL (absoluut, https) — gebruikt in de mail-header en op de factuur-PDF. */\n logo: z.optional(z.string().check(z.maxLength(500))),\n background: z.optional(z.string().check(z.maxLength(40))),\n foreground: z.optional(z.string().check(z.maxLength(40))),\n fontFamily: z.optional(z.string().check(z.maxLength(200))),\n /** Optioneel apart lettertype voor koppen (display/serif), voor een elegantere typografie. */\n headingFont: z.optional(z.string().check(z.maxLength(200))),\n /** Optionele stylesheet-URL voor het lettertype (bv. een Google Fonts CSS-link). */\n fontUrl: z.optional(z.string().check(z.maxLength(500))),\n /** Afrondingsschaal (sm/md/lg). */\n radius: z.optional(z.object({ sm: z.optional(z.string().check(z.maxLength(20))), md: z.optional(z.string().check(z.maxLength(20))), lg: z.optional(z.string().check(z.maxLength(20))) })),\n /** Ruimteschaal voor block-padding (xs..xl). */\n spacing: z.optional(z.object({\n xs: z.optional(z.string().check(z.maxLength(20))),\n sm: z.optional(z.string().check(z.maxLength(20))),\n md: z.optional(z.string().check(z.maxLength(20))),\n lg: z.optional(z.string().check(z.maxLength(20))),\n xl: z.optional(z.string().check(z.maxLength(20))),\n })),\n /** Maximale container-breedte + horizontale padding. */\n container: z.optional(z.object({ maxWidth: z.optional(z.string().check(z.maxLength(20))), padding: z.optional(z.string().check(z.maxLength(20))) })),\n /** Donkere modus: uit, altijd aan, of meebewegen met het systeem van de bezoeker. */\n dark: z.optional(z.object({\n /**\n * \"auto\" — volgt het besturingssysteem van de bezoeker (light-dark()).\n * \"dark\" — de site is altijd donker, ongeacht de bezoeker.\n * \"light\" — de site is altijd licht (gelijk aan geen donkere modus).\n *\n * Vervangt de oude `auto`-boolean, die alleen \"volg het systeem\" of \"niets\" kon zeggen —\n * \"deze site is gewoon donker\" was daarmee niet uit te drukken. `auto` blijft gelezen worden\n * zodat bestaande site-configs blijven werken (zie themeMode in XiCMSProvider).\n */\n mode: z.optional(z.enum([\"light\", \"dark\", \"auto\"])),\n /**\n * @deprecated Gebruik `mode`. Blijft werken: true → \"auto\", afwezig/false → \"light\".\n * OPTIONEEL (niet `_default(false)`): met een verplichte default draagt elk gemerged\n * dark-object een `auto`, en dan botst een partiële update uit /beheer op het type.\n */\n auto: z.optional(z.boolean()),\n background: z.optional(z.string().check(z.maxLength(40))),\n foreground: z.optional(z.string().check(z.maxLength(40))),\n primary: z.optional(z.string().check(z.maxLength(40))),\n })),\n});\nexport type ThemeTokens = z.infer<typeof ThemeSchema>;\n\n/**\n * Voegt live thema-instellingen (uit `settings/public.theme`, /beheer) per token samen met het\n * statische SiteConfig-thema. Firestore wint per veld waar gezet; geneste schalen (radius/spacing/\n * container/dark) mergen per subveld, zodat een klant die alleen kleuren wijzigt de fonts/radius\n * uit de repo behoudt. Puur (geen React) — gedeeld door provider en `useMergedSiteConfig`.\n */\nexport function mergeThemeTokens(base: ThemeTokens, override?: ThemeTokens): ThemeTokens {\n if (!override) return base;\n const merged: ThemeTokens = { ...base, ...override };\n for (const key of [\"radius\", \"spacing\", \"container\", \"dark\"] as const) {\n if (base[key] && override[key]) {\n merged[key] = { ...base[key], ...override[key] } as never;\n } else if (base[key] && !override[key]) {\n merged[key] = base[key] as never;\n }\n }\n return merged;\n}\n\n/**\n * Afzender-instelling voor formulier-e-mail. Een provider mag alleen versturen vanaf een\n * geverifieerd domein, dus dit is per site instelbaar:\n * - mode \"xicms\": from = verifiedSender (XiCMS-domein), replyTo = inzender/klant. Werkt direct.\n * - mode \"client\": from = het geverifieerde eigen domein van de klant.\n */\nexport const FormsSenderSchema = z.object({\n mode: z._default(z.enum([\"xicms\", \"client\"]), \"xicms\"),\n /** Geverifieerd from-adres (verplicht ongeacht mode; bij \"client\" het klant-domein). */\n fromEmail: z.optional(z.email()),\n fromName: z.optional(z.string().check(z.maxLength(120))),\n /** Waar notificaties heen gaan (meestal het klant-mailadres). */\n notifyTo: z.optional(z.email()),\n});\nexport type FormsSender = z.infer<typeof FormsSenderSchema>;\n\n/** Eén social-link (platform + url). */\nexport const SocialLinkSchema = z.object({\n platform: z.string().check(z.minLength(1)).check(z.maxLength(40)),\n href: z.string().check(z.minLength(1)).check(z.maxLength(2000)),\n});\nexport type SocialLink = z.infer<typeof SocialLinkSchema>;\n\n/** Eén footer-kolom: kop + links. */\nexport const FooterColumnSchema = z.object({\n heading: z.optional(z.string().check(z.maxLength(120))),\n links: z._default(z.array(NavItemSchema).check(z.maxLength(20)), []),\n});\n\n/** Bewerkbare footer (klant past dit aan in /beheer; core levert de unstyled `SiteFooter`). */\nexport const FooterSettingsSchema = z.object({\n columns: z.optional(z.array(FooterColumnSchema).check(z.maxLength(6))),\n social: z.optional(z.array(SocialLinkSchema).check(z.maxLength(12))),\n copyright: z.optional(z.string().check(z.maxLength(300))),\n /** Toon de \"Beheer\"-link in de footer (default: aan). */\n showAdminLink: z.optional(z.boolean()),\n});\nexport type FooterSettings = z.infer<typeof FooterSettingsSchema>;\n\n/** Per-module activatie/instellingen (spiegelt ModulesSiteConfig; permissief gehouden). */\nexport const ModulesSettingsSchema = z.record(\n z.string(),\n z.union([\n z.boolean(),\n z.object({ enabled: z.optional(z.boolean()), config: z.optional(z.record(z.string(), z.unknown())) }),\n ]),\n);\n\n/** Het `sites/{siteId}/settings/site`-document: alle bewerkbare site-instellingen. */\nexport const SiteSettingsSchema = z.object({\n name: z.optional(z.string().check(z.minLength(1)).check(z.maxLength(160))),\n // Zelfde hostname-regel als SiteConfigSchema.domain (geen protocol/pad).\n domain: z.union([z.optional(z.string().check(z.maxLength(255)).check(z.regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/i, \"alleen een hostname, bv. 'www.klant.nl' (zonder https:// of pad)\"))), z.literal(\"\")]),\n contactEmail: z.optional(z.email()),\n nav: z.optional(z.array(NavItemSchema).check(z.maxLength(50))),\n theme: z.optional(ThemeSchema),\n seoDefaults: z.optional(SeoSchema),\n forms: z.optional(FormsSenderSchema),\n footer: z.optional(FooterSettingsSchema),\n modules: z.optional(ModulesSettingsSchema),\n /**\n * Snelkoppelingen op de beheer-startpagina (7.16). Beheer-intern: bewust NIET in de publieke\n * subset. Opslaan loopt via de bestaande saveSiteSettings-callable — die is passthrough met\n * merge, dus dit veld vergt geen functions-wijziging.\n */\n dashboardLinks: z.optional(z.array(z.object({\n label: z.string().check(z.minLength(1)).check(z.maxLength(80)),\n href: z.string().check(z.minLength(1)).check(z.maxLength(2000)),\n })).check(z.maxLength(12))),\n /** Doorverwijzingen van oude naar nieuwe paden (7.24). Publiek: de catch-all leest ze. */\n redirects: z.optional(z.array(RedirectSchema).check(z.maxLength(200))),\n});\nexport type SiteSettings = z.infer<typeof SiteSettingsSchema>;\n\n/**\n * Publiek-veilige presentatie-instellingen (`sites/{siteId}/settings/public`): wordt door de live\n * site gelezen (footer, thema, nav, modules) zónder de gevoelige velden (e-mailafzender, contact)\n * uit `settings/site` bloot te geven.\n */\nexport const PublicSiteSettingsSchema = z.object({\n name: z.optional(z.string().check(z.maxLength(160))),\n nav: z.optional(z.array(NavItemSchema).check(z.maxLength(50))),\n theme: z.optional(ThemeSchema),\n seoDefaults: z.optional(SeoSchema),\n footer: z.optional(FooterSettingsSchema),\n modules: z.optional(ModulesSettingsSchema),\n redirects: z.optional(z.array(RedirectSchema).check(z.maxLength(200))),\n});\nexport type PublicSiteSettings = z.infer<typeof PublicSiteSettingsSchema>;\n\n/** Haalt de publiek-veilige subset uit de volledige settings (voor het `settings/public`-doc). */\nexport function toPublicSettings(s: SiteSettings): PublicSiteSettings {\n return {\n name: s.name,\n nav: s.nav,\n theme: s.theme,\n seoDefaults: s.seoDefaults,\n footer: s.footer,\n modules: s.modules,\n redirects: s.redirects,\n };\n}\n"],"mappings":"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","names":[]}
@@ -123,6 +123,15 @@ export declare const SiteConfigSchema: z.ZodMiniObject<{
123
123
  _embed: "_embed";
124
124
  _self: "_self";
125
125
  }>>;
126
+ children: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniObject<{
127
+ label: z.ZodMiniString<string>;
128
+ href: z.ZodMiniString<string>;
129
+ target: z.ZodMiniOptional<z.ZodMiniEnum<{
130
+ _blank: "_blank";
131
+ _embed: "_embed";
132
+ _self: "_self";
133
+ }>>;
134
+ }, z.core.$strip>>>;
126
135
  }, z.core.$strip>>>;
127
136
  contactEmail: z.ZodMiniOptional<z.ZodMiniEmail>;
128
137
  seoDefaults: z.ZodMiniOptional<z.ZodMiniObject<{
@@ -72,8 +72,12 @@ async function getPageBlockData(siteId, page, opts) {
72
72
  for (const m of alle) {
73
73
  const url = typeof m.url === "string" ? m.url : "";
74
74
  const varianten = m.variants;
75
- if (!url || !varianten?.sizes?.length || m.status === "processing") continue;
76
- if (blokTekst.includes(url)) map[url] = { variants: m.variants, width: m.width, height: m.height };
75
+ const focal = m.focal;
76
+ const heeftFocal = typeof focal?.x === "number" && typeof focal?.y === "number";
77
+ if (!url || m.status === "processing" || !varianten?.sizes?.length && !heeftFocal) continue;
78
+ if (blokTekst.includes(url)) {
79
+ map[url] = { variants: m.variants, width: m.width, height: m.height, ...heeftFocal ? { focal } : {} };
80
+ }
77
81
  }
78
82
  if (Object.keys(map).length > 0) data.media = map;
79
83
  }),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/server/blockData.ts"],"sourcesContent":["import \"server-only\";\n\nimport type { Page } from \"../schema/pages\";\nimport { listPublishedPages, listPublishedNews, listFormDefinitions, listSiteMedia } from \"./pages\";\nimport { blocksToText, type SearchEntry } from \"../blocks/searchText\";\n\n/**\n * SSR-seeds voor blokken die data buiten de pagina zelf nodig hebben (7.12).\n *\n * Het duurste patroon in XiCMS was niet een gemiste servercache maar de BROWSER: blokken als de\n * productgrid, het zoekblok en het formulier haalden hun data client-side uit Firestore — per\n * bezoeker, buiten elke cache om. Eén winkelpagina kostte zo honderden reads per bezoeker, en het\n * zoekblok las de complete pagina-collectie. De server-readers bestonden (server/shop.ts), maar\n * elke site moest er zelf aan denken ze aan te roepen — en geen enkele deed dat. SITE-AUDIT noemt\n * ontbrekende SSR-seeds \"de #1 prestatieval\"; dit bestand maakt die val structureel onmogelijk.\n *\n * `getPageBlockData` kijkt naar de blóktypes van de pagina en verzamelt precies de data die deze\n * pagina nodig heeft — niets meer. Een pagina zonder shop-blokken doet nul product-reads; een\n * pagina zonder zoekblok bouwt geen index. Alle onderliggende readers zitten in de tagged\n * servercache (server/cached.ts), dus dit kost Firestore éénmaal per interval, niet per bezoeker.\n *\n * Gebruik in de catch-all van de klantsite (ná symbol-expansie, zodat blokken ín symbolen meetellen):\n *\n * const page = await getPublishedPageByPathExpanded(siteId, path, locale);\n * const blockData = page ? await getPageBlockData(siteId, page, { searchParams }) : undefined;\n * <PublishedPage page={page} siteId={siteId} blockData={blockData} />\n *\n * De blokken zelf houden hun client-fallback: een site die dit (nog) niet doorgeeft blijft werken,\n * alleen duurder en trager — precies het oude gedrag.\n */\n\n/** Bloktypes die de shop-seed nodig hebben (productDetail leest daarnaast `?p=` voor het product). */\nconst SHOP_TYPES = new Set([\"productGrid\", \"shopCategories\", \"productDetail\", \"cart\", \"checkout\", \"donation\", \"orderStatus\"]);\n/** Bloktypes die de nieuws-seed nodig hebben. */\nconst NEWS_TYPES = new Set([\"newsList\", \"newsWidget\"]);\n\n/** Verzamel alle `_type`-waarden, recursief — kolommen/tabs/secties kunnen blokken bevatten. */\nfunction collectTypes(v: unknown, out: Set<string>, depth: number): void {\n if (depth > 6 || v == null || typeof v !== \"object\") return;\n if (Array.isArray(v)) {\n for (const item of v) collectTypes(item, out, depth + 1);\n return;\n }\n const o = v as Record<string, unknown>;\n if (typeof o._type === \"string\") out.add(o._type);\n for (const val of Object.values(o)) collectTypes(val, out, depth + 1);\n}\n\n/** Verzamel de `formId`s van alle formulierblokken (ook genest). */\nfunction collectFormIds(v: unknown, out: Set<string>, depth: number): void {\n if (depth > 6 || v == null || typeof v !== \"object\") return;\n if (Array.isArray(v)) {\n for (const item of v) collectFormIds(item, out, depth + 1);\n return;\n }\n const o = v as Record<string, unknown>;\n if (o._type === \"form\" && typeof o.formId === \"string\" && o.formId) out.add(o.formId);\n for (const val of Object.values(o)) collectFormIds(val, out, depth + 1);\n}\n\n/** Wil één van de zoekblokken op de pagina ook nieuws doorzoeken? */\nfunction searchWantsNews(v: unknown, depth: number): boolean {\n if (depth > 6 || v == null || typeof v !== \"object\") return false;\n if (Array.isArray(v)) return v.some((item) => searchWantsNews(item, depth + 1));\n const o = v as Record<string, unknown>;\n if (o._type === \"search\" && o.includeNews === true) return true;\n return Object.values(o).some((val) => searchWantsNews(val, depth + 1));\n}\n\nexport interface PageBlockDataOptions {\n /** `searchParams` van de route — alleen nodig voor productdetailpagina's (`?p=<slug>`). */\n searchParams?: { p?: string | string[]; [k: string]: unknown };\n /** De taal van de render — de zoekindex indexeert dan de vertaalde paden. (Nog ongebruikt.) */\n locale?: string;\n}\n\n/**\n * Server-side blockData voor één pagina, op basis van zijn bloktypes.\n * Geef het resultaat door als `<PublishedPage blockData={...}>`. `undefined`-namespaces blijven\n * weg zodat de blokken hun eigen fallback houden waar deze functie niets voor verzamelde.\n */\nexport async function getPageBlockData(\n siteId: string,\n page: Pick<Page, \"blocks\">,\n opts?: PageBlockDataOptions,\n): Promise<Record<string, unknown> | undefined> {\n const types = new Set<string>();\n collectTypes(page.blocks ?? [], types, 0);\n\n const wilShop = [...SHOP_TYPES].some((t) => types.has(t));\n const wilNews = [...NEWS_TYPES].some((t) => types.has(t));\n const wilSearch = types.has(\"search\");\n const formIds = new Set<string>();\n collectFormIds(page.blocks ?? [], formIds, 0);\n\n if (!wilShop && !wilNews && !wilSearch && formIds.size === 0) return undefined;\n\n const data: Record<string, unknown> = {};\n\n // Elke seed faalt ZACHT: bij een fout blijft die namespace weg en neemt de client-fallback van\n // het blok het over — hetzelfde gedrag als een site die (nog) geen seeds doorgeeft. De readers\n // gooien bewust bij een hik (uitkomsten worden gecached, fouten niet — zie server/cached.ts),\n // dus hier opvangen is wat \"één mislukte read\" scheidt van \"één mislukte pagina\".\n const zacht = async (naam: string, fn: () => Promise<void>) => {\n try {\n await fn();\n } catch (e) {\n console.warn(`[xicms] blockData-seed \"${naam}\" overgeslagen:`, (e as Error)?.message ?? e);\n }\n };\n\n // Parallel: elke seed leest uit de tagged servercache, dus dit zijn meestal nul echte reads.\n await Promise.all([\n zacht(\"shop\", async () => {\n if (!wilShop) return;\n // Dynamische import: shop-servercode alleen in de graph van pagina's/sites die hem gebruiken.\n const { getShopPageData } = await import(\"./shop\");\n data.shop = await getShopPageData(siteId, opts?.searchParams);\n }),\n zacht(\"news\", async () => {\n if (!wilNews) return;\n data.news = await listPublishedNews(siteId);\n }),\n zacht(\"forms\", async () => {\n if (formIds.size === 0) return;\n const alle = await listFormDefinitions(siteId);\n const map: Record<string, unknown> = {};\n for (const f of alle) if (formIds.has(f.id)) map[f.id] = f;\n data.forms = map;\n }),\n zacht(\"media\", async () => {\n // Beeld-ladder-seed: alleen de media-docs waarvan de URL werkelijk op deze pagina staat,\n // en alleen als de pipeline varianten heeft gemaakt. Eén stringify-scan is goedkoper en\n // robuuster dan per bloktype weten welke velden afbeeldingen zijn.\n const blokTekst = JSON.stringify(page.blocks ?? []);\n if (!blokTekst.includes(\"firebasestorage\") && !blokTekst.includes(\"storage.googleapis\")) return;\n const alle = await listSiteMedia(siteId);\n const map: Record<string, unknown> = {};\n for (const m of alle) {\n const url = typeof m.url === \"string\" ? m.url : \"\";\n const varianten = m.variants as { sizes?: unknown[] } | undefined;\n if (!url || !varianten?.sizes?.length || m.status === \"processing\") continue;\n if (blokTekst.includes(url)) map[url] = { variants: m.variants, width: m.width, height: m.height };\n }\n if (Object.keys(map).length > 0) data.media = map;\n }),\n zacht(\"search\", async () => {\n if (!wilSearch) return;\n const [pages, news] = await Promise.all([\n listPublishedPages(siteId),\n searchWantsNews(page.blocks ?? [], 0) ? listPublishedNews(siteId) : Promise.resolve([]),\n ]);\n const index: SearchEntry[] = pages.map((p) => ({\n title: String(p.title ?? \"\"),\n path: String(p.path ?? \"/\"),\n text: blocksToText(p.blocks),\n }));\n for (const n of news) {\n index.push({\n title: n.title,\n path: `/nieuws/${n.slug || n.id}`,\n text: `${n.excerpt ?? \"\"} ${(n.bodyHtml ?? n.body ?? \"\").replace(/<[^>]*>/g, \" \")}`,\n });\n }\n data.searchIndex = index;\n }),\n ]);\n\n return data;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/server/blockData.ts"],"sourcesContent":["import \"server-only\";\n\nimport type { Page } from \"../schema/pages\";\nimport { listPublishedPages, listPublishedNews, listFormDefinitions, listSiteMedia } from \"./pages\";\nimport { blocksToText, type SearchEntry } from \"../blocks/searchText\";\n\n/**\n * SSR-seeds voor blokken die data buiten de pagina zelf nodig hebben (7.12).\n *\n * Het duurste patroon in XiCMS was niet een gemiste servercache maar de BROWSER: blokken als de\n * productgrid, het zoekblok en het formulier haalden hun data client-side uit Firestore — per\n * bezoeker, buiten elke cache om. Eén winkelpagina kostte zo honderden reads per bezoeker, en het\n * zoekblok las de complete pagina-collectie. De server-readers bestonden (server/shop.ts), maar\n * elke site moest er zelf aan denken ze aan te roepen — en geen enkele deed dat. SITE-AUDIT noemt\n * ontbrekende SSR-seeds \"de #1 prestatieval\"; dit bestand maakt die val structureel onmogelijk.\n *\n * `getPageBlockData` kijkt naar de blóktypes van de pagina en verzamelt precies de data die deze\n * pagina nodig heeft — niets meer. Een pagina zonder shop-blokken doet nul product-reads; een\n * pagina zonder zoekblok bouwt geen index. Alle onderliggende readers zitten in de tagged\n * servercache (server/cached.ts), dus dit kost Firestore éénmaal per interval, niet per bezoeker.\n *\n * Gebruik in de catch-all van de klantsite (ná symbol-expansie, zodat blokken ín symbolen meetellen):\n *\n * const page = await getPublishedPageByPathExpanded(siteId, path, locale);\n * const blockData = page ? await getPageBlockData(siteId, page, { searchParams }) : undefined;\n * <PublishedPage page={page} siteId={siteId} blockData={blockData} />\n *\n * De blokken zelf houden hun client-fallback: een site die dit (nog) niet doorgeeft blijft werken,\n * alleen duurder en trager — precies het oude gedrag.\n */\n\n/** Bloktypes die de shop-seed nodig hebben (productDetail leest daarnaast `?p=` voor het product). */\nconst SHOP_TYPES = new Set([\"productGrid\", \"shopCategories\", \"productDetail\", \"cart\", \"checkout\", \"donation\", \"orderStatus\"]);\n/** Bloktypes die de nieuws-seed nodig hebben. */\nconst NEWS_TYPES = new Set([\"newsList\", \"newsWidget\"]);\n\n/** Verzamel alle `_type`-waarden, recursief — kolommen/tabs/secties kunnen blokken bevatten. */\nfunction collectTypes(v: unknown, out: Set<string>, depth: number): void {\n if (depth > 6 || v == null || typeof v !== \"object\") return;\n if (Array.isArray(v)) {\n for (const item of v) collectTypes(item, out, depth + 1);\n return;\n }\n const o = v as Record<string, unknown>;\n if (typeof o._type === \"string\") out.add(o._type);\n for (const val of Object.values(o)) collectTypes(val, out, depth + 1);\n}\n\n/** Verzamel de `formId`s van alle formulierblokken (ook genest). */\nfunction collectFormIds(v: unknown, out: Set<string>, depth: number): void {\n if (depth > 6 || v == null || typeof v !== \"object\") return;\n if (Array.isArray(v)) {\n for (const item of v) collectFormIds(item, out, depth + 1);\n return;\n }\n const o = v as Record<string, unknown>;\n if (o._type === \"form\" && typeof o.formId === \"string\" && o.formId) out.add(o.formId);\n for (const val of Object.values(o)) collectFormIds(val, out, depth + 1);\n}\n\n/** Wil één van de zoekblokken op de pagina ook nieuws doorzoeken? */\nfunction searchWantsNews(v: unknown, depth: number): boolean {\n if (depth > 6 || v == null || typeof v !== \"object\") return false;\n if (Array.isArray(v)) return v.some((item) => searchWantsNews(item, depth + 1));\n const o = v as Record<string, unknown>;\n if (o._type === \"search\" && o.includeNews === true) return true;\n return Object.values(o).some((val) => searchWantsNews(val, depth + 1));\n}\n\nexport interface PageBlockDataOptions {\n /** `searchParams` van de route — alleen nodig voor productdetailpagina's (`?p=<slug>`). */\n searchParams?: { p?: string | string[]; [k: string]: unknown };\n /** De taal van de render — de zoekindex indexeert dan de vertaalde paden. (Nog ongebruikt.) */\n locale?: string;\n}\n\n/**\n * Server-side blockData voor één pagina, op basis van zijn bloktypes.\n * Geef het resultaat door als `<PublishedPage blockData={...}>`. `undefined`-namespaces blijven\n * weg zodat de blokken hun eigen fallback houden waar deze functie niets voor verzamelde.\n */\nexport async function getPageBlockData(\n siteId: string,\n page: Pick<Page, \"blocks\">,\n opts?: PageBlockDataOptions,\n): Promise<Record<string, unknown> | undefined> {\n const types = new Set<string>();\n collectTypes(page.blocks ?? [], types, 0);\n\n const wilShop = [...SHOP_TYPES].some((t) => types.has(t));\n const wilNews = [...NEWS_TYPES].some((t) => types.has(t));\n const wilSearch = types.has(\"search\");\n const formIds = new Set<string>();\n collectFormIds(page.blocks ?? [], formIds, 0);\n\n if (!wilShop && !wilNews && !wilSearch && formIds.size === 0) return undefined;\n\n const data: Record<string, unknown> = {};\n\n // Elke seed faalt ZACHT: bij een fout blijft die namespace weg en neemt de client-fallback van\n // het blok het over — hetzelfde gedrag als een site die (nog) geen seeds doorgeeft. De readers\n // gooien bewust bij een hik (uitkomsten worden gecached, fouten niet — zie server/cached.ts),\n // dus hier opvangen is wat \"één mislukte read\" scheidt van \"één mislukte pagina\".\n const zacht = async (naam: string, fn: () => Promise<void>) => {\n try {\n await fn();\n } catch (e) {\n console.warn(`[xicms] blockData-seed \"${naam}\" overgeslagen:`, (e as Error)?.message ?? e);\n }\n };\n\n // Parallel: elke seed leest uit de tagged servercache, dus dit zijn meestal nul echte reads.\n await Promise.all([\n zacht(\"shop\", async () => {\n if (!wilShop) return;\n // Dynamische import: shop-servercode alleen in de graph van pagina's/sites die hem gebruiken.\n const { getShopPageData } = await import(\"./shop\");\n data.shop = await getShopPageData(siteId, opts?.searchParams);\n }),\n zacht(\"news\", async () => {\n if (!wilNews) return;\n data.news = await listPublishedNews(siteId);\n }),\n zacht(\"forms\", async () => {\n if (formIds.size === 0) return;\n const alle = await listFormDefinitions(siteId);\n const map: Record<string, unknown> = {};\n for (const f of alle) if (formIds.has(f.id)) map[f.id] = f;\n data.forms = map;\n }),\n zacht(\"media\", async () => {\n // Beeld-ladder-seed: alleen de media-docs waarvan de URL werkelijk op deze pagina staat,\n // en alleen als de pipeline varianten heeft gemaakt. Eén stringify-scan is goedkoper en\n // robuuster dan per bloktype weten welke velden afbeeldingen zijn.\n const blokTekst = JSON.stringify(page.blocks ?? []);\n if (!blokTekst.includes(\"firebasestorage\") && !blokTekst.includes(\"storage.googleapis\")) return;\n const alle = await listSiteMedia(siteId);\n const map: Record<string, unknown> = {};\n for (const m of alle) {\n const url = typeof m.url === \"string\" ? m.url : \"\";\n const varianten = m.variants as { sizes?: unknown[] } | undefined;\n // 7.24: ook een foto zónder ladder maar mét focuspunt gaat mee — anders zou een focuspunt\n // op een oudere upload (van vóór de pipeline) nooit aankomen.\n const focal = m.focal as { x?: unknown; y?: unknown } | undefined;\n const heeftFocal = typeof focal?.x === \"number\" && typeof focal?.y === \"number\";\n if (!url || m.status === \"processing\" || (!varianten?.sizes?.length && !heeftFocal)) continue;\n if (blokTekst.includes(url)) {\n map[url] = { variants: m.variants, width: m.width, height: m.height, ...(heeftFocal ? { focal } : {}) };\n }\n }\n if (Object.keys(map).length > 0) data.media = map;\n }),\n zacht(\"search\", async () => {\n if (!wilSearch) return;\n const [pages, news] = await Promise.all([\n listPublishedPages(siteId),\n searchWantsNews(page.blocks ?? [], 0) ? listPublishedNews(siteId) : Promise.resolve([]),\n ]);\n const index: SearchEntry[] = pages.map((p) => ({\n title: String(p.title ?? \"\"),\n path: String(p.path ?? \"/\"),\n text: blocksToText(p.blocks),\n }));\n for (const n of news) {\n index.push({\n title: n.title,\n path: `/nieuws/${n.slug || n.id}`,\n text: `${n.excerpt ?? \"\"} ${(n.bodyHtml ?? n.body ?? \"\").replace(/<[^>]*>/g, \" \")}`,\n });\n }\n data.searchIndex = index;\n }),\n ]);\n\n return data;\n}\n"],"mappings":"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","names":[]}
@@ -14,3 +14,14 @@
14
14
  * afsluiten, verifieer dan hier een Firebase-ID-token vóór `enable()`.
15
15
  */
16
16
  export declare function draftPOST(req: Request): Promise<Response>;
17
+ /**
18
+ * Deelbare preview-link (7.24): `GET /api/draft?token=…`.
19
+ *
20
+ * Een editor maakt in de visuele editor een link (callable `createPreviewLink`, ondertekend met
21
+ * het revalidate-geheim); wie hem opent krijgt Draft Mode aan en wordt doorgestuurd naar het pad
22
+ * uit het token. Zo kan een collega of klant zonder account meekijken vóór publiceren.
23
+ *
24
+ * `siteId` komt uit de site zelf (niet uit de URL): een token van site A werkt nooit op site B.
25
+ * Zonder geheim op deze deployment staat de route uit (404) — precies zoals /api/revalidate.
26
+ */
27
+ export declare function maakDraftGET(siteId: string): (req: Request) => Promise<Response>;
@@ -1,4 +1,5 @@
1
1
  import { draftMode } from "next/headers";
2
+ import { verifyPreviewToken } from "./previewToken.js";
2
3
  async function draftPOST(req) {
3
4
  const aan = new URL(req.url).searchParams.get("aan") !== "0";
4
5
  const dm = await draftMode();
@@ -6,7 +7,25 @@ async function draftPOST(req) {
6
7
  else dm.disable();
7
8
  return new Response(null, { status: 204 });
8
9
  }
10
+ function maakDraftGET(siteId) {
11
+ return async (req) => {
12
+ const secret = process.env.XICMS_REVALIDATE_SECRET;
13
+ if (!secret) return new Response("preview-links staan uit op deze site", { status: 404 });
14
+ const url = new URL(req.url);
15
+ const token = url.searchParams.get("token") ?? "";
16
+ const claims = await verifyPreviewToken(token, secret, siteId);
17
+ if (!claims) {
18
+ return new Response("Deze preview-link is ongeldig of verlopen. Vraag een nieuwe aan de beheerder.", {
19
+ status: 403,
20
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
21
+ });
22
+ }
23
+ (await draftMode()).enable();
24
+ return Response.redirect(new URL(claims.path, url.origin).toString(), 303);
25
+ };
26
+ }
9
27
  export {
10
- draftPOST
28
+ draftPOST,
29
+ maakDraftGET
11
30
  };
12
31
  //# sourceMappingURL=draft.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/server/draft.ts"],"sourcesContent":["import { draftMode } from \"next/headers\";\n\n/**\n * Zet Next's Draft Mode aan of uit voor de huidige browser. Aangeroepen door `PreviewDraftSync`\n * (XiCMSProvider) zodra een editor in- of uitlogt: `POST /api/draft?aan=1` respectievelijk `?aan=0`.\n *\n * WAAROM DIT MOET: Draft Mode werkt via de ondertekende `__prerender_bypass`-cookie, en die kan\n * alleen de server zetten — `document.cookie` op de client komt er niet aan. Precies dat maakt het\n * beter dan de eigen preview-cookie die hiervoor gebruikt werd: Next controleert deze cookie VÓÓR\n * de cache, buiten de route om, dus de pagina-route hoeft niets dynamisch te lezen en blijft voor\n * publiek gewoon statisch geprerenderd.\n *\n * Draagt geen rechten: preview toont nooit meer dan de al-gepubliceerde content, dus wie deze route\n * zelf aanroept ziet hoogstens een verse versie van iets dat toch al openbaar is. Wat het wél kost\n * is een ongecachte render (en dus een Firestore-read) per request van die browser. Wil je dat\n * afsluiten, verifieer dan hier een Firebase-ID-token vóór `enable()`.\n */\nexport async function draftPOST(req: Request): Promise<Response> {\n const aan = new URL(req.url).searchParams.get(\"aan\") !== \"0\";\n const dm = await draftMode();\n if (aan) dm.enable();\n else dm.disable();\n return new Response(null, { status: 204 });\n}\n"],"mappings":"AAAA,SAAS,iBAAiB;AAiB1B,eAAsB,UAAU,KAAiC;AAC/D,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG,EAAE,aAAa,IAAI,KAAK,MAAM;AACzD,QAAM,KAAK,MAAM,UAAU;AAC3B,MAAI,IAAK,IAAG,OAAO;AAAA,MACd,IAAG,QAAQ;AAChB,SAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAI,CAAC;AAC3C;","names":[]}
1
+ {"version":3,"sources":["../../src/server/draft.ts"],"sourcesContent":["import { draftMode } from \"next/headers\";\nimport { verifyPreviewToken } from \"./previewToken\";\n\n/**\n * Zet Next's Draft Mode aan of uit voor de huidige browser. Aangeroepen door `PreviewDraftSync`\n * (XiCMSProvider) zodra een editor in- of uitlogt: `POST /api/draft?aan=1` respectievelijk `?aan=0`.\n *\n * WAAROM DIT MOET: Draft Mode werkt via de ondertekende `__prerender_bypass`-cookie, en die kan\n * alleen de server zetten — `document.cookie` op de client komt er niet aan. Precies dat maakt het\n * beter dan de eigen preview-cookie die hiervoor gebruikt werd: Next controleert deze cookie VÓÓR\n * de cache, buiten de route om, dus de pagina-route hoeft niets dynamisch te lezen en blijft voor\n * publiek gewoon statisch geprerenderd.\n *\n * Draagt geen rechten: preview toont nooit meer dan de al-gepubliceerde content, dus wie deze route\n * zelf aanroept ziet hoogstens een verse versie van iets dat toch al openbaar is. Wat het wél kost\n * is een ongecachte render (en dus een Firestore-read) per request van die browser. Wil je dat\n * afsluiten, verifieer dan hier een Firebase-ID-token vóór `enable()`.\n */\nexport async function draftPOST(req: Request): Promise<Response> {\n const aan = new URL(req.url).searchParams.get(\"aan\") !== \"0\";\n const dm = await draftMode();\n if (aan) dm.enable();\n else dm.disable();\n return new Response(null, { status: 204 });\n}\n\n/**\n * Deelbare preview-link (7.24): `GET /api/draft?token=…`.\n *\n * Een editor maakt in de visuele editor een link (callable `createPreviewLink`, ondertekend met\n * het revalidate-geheim); wie hem opent krijgt Draft Mode aan en wordt doorgestuurd naar het pad\n * uit het token. Zo kan een collega of klant zonder account meekijken vóór publiceren.\n *\n * `siteId` komt uit de site zelf (niet uit de URL): een token van site A werkt nooit op site B.\n * Zonder geheim op deze deployment staat de route uit (404) — precies zoals /api/revalidate.\n */\nexport function maakDraftGET(siteId: string): (req: Request) => Promise<Response> {\n return async (req) => {\n const secret = process.env.XICMS_REVALIDATE_SECRET;\n if (!secret) return new Response(\"preview-links staan uit op deze site\", { status: 404 });\n const url = new URL(req.url);\n const token = url.searchParams.get(\"token\") ?? \"\";\n const claims = await verifyPreviewToken(token, secret, siteId);\n if (!claims) {\n return new Response(\"Deze preview-link is ongeldig of verlopen. Vraag een nieuwe aan de beheerder.\", {\n status: 403,\n headers: { \"Content-Type\": \"text/plain; charset=utf-8\" },\n });\n }\n (await draftMode()).enable();\n return Response.redirect(new URL(claims.path, url.origin).toString(), 303);\n };\n}\n"],"mappings":"AAAA,SAAS,iBAAiB;AAC1B,SAAS,0BAA0B;AAiBnC,eAAsB,UAAU,KAAiC;AAC/D,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG,EAAE,aAAa,IAAI,KAAK,MAAM;AACzD,QAAM,KAAK,MAAM,UAAU;AAC3B,MAAI,IAAK,IAAG,OAAO;AAAA,MACd,IAAG,QAAQ;AAChB,SAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAI,CAAC;AAC3C;AAYO,SAAS,aAAa,QAAqD;AAChF,SAAO,OAAO,QAAQ;AACpB,UAAM,SAAS,QAAQ,IAAI;AAC3B,QAAI,CAAC,OAAQ,QAAO,IAAI,SAAS,wCAAwC,EAAE,QAAQ,IAAI,CAAC;AACxF,UAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,UAAM,QAAQ,IAAI,aAAa,IAAI,OAAO,KAAK;AAC/C,UAAM,SAAS,MAAM,mBAAmB,OAAO,QAAQ,MAAM;AAC7D,QAAI,CAAC,QAAQ;AACX,aAAO,IAAI,SAAS,iFAAiF;AAAA,QACnG,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,4BAA4B;AAAA,MACzD,CAAC;AAAA,IACH;AACA,KAAC,MAAM,UAAU,GAAG,OAAO;AAC3B,WAAO,SAAS,SAAS,IAAI,IAAI,OAAO,MAAM,IAAI,MAAM,EAAE,SAAS,GAAG,GAAG;AAAA,EAC3E;AACF;","names":[]}
@@ -11,7 +11,9 @@ export { validateEnv, assertEnv, type EnvReport } from "./env.js";
11
11
  export { getPageBlockData, type PageBlockDataOptions } from "./blockData.js";
12
12
  export { listFormDefinitions, listSiteMedia } from "./pages.js";
13
13
  export { revalidatePOST } from "./revalidate.js";
14
- export { draftPOST } from "./draft.js";
14
+ export { draftPOST, maakDraftGET } from "./draft.js";
15
+ export { signPreviewToken, verifyPreviewToken, type PreviewClaims } from "./previewToken.js";
16
+ export { resolveRedirect } from "./redirects.js";
15
17
  export { versionGET } from "./version.js";
16
18
  export { pathFromSlugs, listPublishedPages, listPublishedNews, getPublishedPageByPath, getPublishedPageByPathExpanded, getSiteSymbols, expandSymbols, getSiteCustomBlocks, getPublicSiteSettings, getDraftPageById, getDraftPageByPath, type StoredPage, } from "./pages.js";
17
19
  export { getBundle, getBundlePage, getBundleNews, getNewsPageData, type PublishedBundle, type BundleItem, } from "./bundle.js";
@@ -20,7 +20,9 @@ import { validateEnv, assertEnv } from "./env.js";
20
20
  import { getPageBlockData } from "./blockData.js";
21
21
  import { listFormDefinitions, listSiteMedia } from "./pages.js";
22
22
  import { revalidatePOST } from "./revalidate.js";
23
- import { draftPOST } from "./draft.js";
23
+ import { draftPOST, maakDraftGET } from "./draft.js";
24
+ import { signPreviewToken, verifyPreviewToken } from "./previewToken.js";
25
+ import { resolveRedirect } from "./redirects.js";
24
26
  import { versionGET } from "./version.js";
25
27
  import {
26
28
  pathFromSlugs,
@@ -85,16 +87,20 @@ export {
85
87
  localizeBlocks,
86
88
  localizePage,
87
89
  localizedPath,
90
+ maakDraftGET,
88
91
  openingHoursJsonLd,
89
92
  parseLocaleSlugs,
90
93
  pathFromSlugs,
91
94
  productJsonLd,
92
95
  pruneTranslations,
96
+ resolveRedirect,
93
97
  revalidatePOST,
94
98
  sanitizeRichText,
99
+ signPreviewToken,
95
100
  siteLocales,
96
101
  siteOrigin,
97
102
  validateEnv,
103
+ verifyPreviewToken,
98
104
  versionGET,
99
105
  videoObjectJsonLd
100
106
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/server/index.ts"],"sourcesContent":["/**\n * @xiwebdesign/xicms-core/server — server-only helpers (firebase-admin, sanitization, SEO,\n * pagina-reads). NOOIT importeren in client-componenten.\n */\nimport \"server-only\";\n\n// `getAdminAuth` zit BEWUST niet hier maar achter de subpath `@xiwebdesign/xicms-core/server/auth`\n// (zie ./auth.ts): firebase-admin/auth sloopt via jwks-rsa/jose élke server-render op Vercel, en\n// hoort dus niet in de bundel van sites die alleen pagina's lezen.\nexport { getAdminApp, getAdminDb } from \"../firebase/admin\";\nexport { sanitizeRichText } from \"./sanitize\";\n// Veilige JSON-LD-serialisatie voor <script type=\"application/ld+json\"> in consumer-routes\n// (escapet `<` zodat klant-tekst met </script> de pagina niet kan breken).\nexport { jsonLdHtml } from \"../blocks/builtins/jsonld\";\nexport {\n buildMetadata,\n buildJsonLd,\n buildSitemap,\n buildRobots,\n siteOrigin,\n businessJsonLd,\n breadcrumbListJsonLd,\n articleJsonLd,\n productJsonLd,\n openingHoursJsonLd,\n galleryJsonLd,\n videoObjectJsonLd,\n type BusinessInfo,\n} from \"./seo\";\nexport { validateEnv, assertEnv, type EnvReport } from \"./env\";\n// On-demand revalidation (6.5): POST-handler voor `app/api/revalidate/route.ts` in de klantsite.\n// De backend roept die route na een publicatie aan → wijziging binnen seconden live, zonder rebuild.\n// SSR-seeds (7.12): verzamelt server-side de blockData die de blokken van déze pagina nodig\n// hebben (shop, nieuws, formulieren, zoekindex) — één aanroep in de catch-all, en geen enkel blok\n// hoeft meer per bezoeker uit Firestore te lezen.\nexport { getPageBlockData, type PageBlockDataOptions } from \"./blockData\";\nexport { listFormDefinitions, listSiteMedia } from \"./pages\";\nexport { revalidatePOST } from \"./revalidate\";\n// Preview (Draft Mode): POST-handler voor `app/api/draft/route.ts` in de klantsite. Zet de\n// bypass-cookie zodat een ingelogde editor de content-bundel + ISR-cache overslaat, terwijl de\n// pagina-route voor publiek statisch blijft.\nexport { draftPOST } from \"./draft\";\n// Versie-endpoint: GET-handler voor `app/api/version/route.ts`, zodat het superbeheer kan zien op\n// welke engine een DEPLOYMENT draait (en niet alleen wanneer er voor het laatst is ingelogd).\nexport { versionGET } from \"./version\";\n// Shop-SSR (6.0): opt-in via subpath `@xiwebdesign/xicms-core/server/shop` (readers + product-SEO).\n// Niet meer onvoorwaardelijk her-geëxporteerd, zodat shop-servercode niet in elke site landt.\nexport {\n pathFromSlugs,\n listPublishedPages,\n listPublishedNews,\n getPublishedPageByPath,\n getPublishedPageByPathExpanded,\n getSiteSymbols,\n expandSymbols,\n getSiteCustomBlocks,\n getPublicSiteSettings,\n getDraftPageById,\n getDraftPageByPath,\n type StoredPage,\n} from \"./pages\";\n// Content-bundle (5.7): build-time reader van `content/published.bundle.json`. De pagina-/settings-/\n// symbol-/customBlock-readers hierboven zijn al bundle-first; deze exports zijn voor directe\n// consumptie (bv. nieuws-blockData in de klant-routes).\nexport {\n getBundle,\n getBundlePage,\n getBundleNews,\n getNewsPageData,\n type PublishedBundle,\n type BundleItem,\n} from \"./bundle\";\n// Localisatie (5.0) — server-safe overlay-helpers, ook bruikbaar in route-handlers/layouts.\nexport {\n siteLocales,\n parseLocaleSlugs,\n localizedPath,\n localizePage,\n localizeBlocks,\n pruneTranslations,\n TranslationsSchema,\n type Translations,\n type LocaleConfig,\n} from \"../i18n/overlay\";\n"],"mappings":"AAIA,OAAO;AAKP,SAAS,aAAa,kBAAkB;AACxC,SAAS,wBAAwB;AAGjC,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,aAAa,iBAAiC;AAMvD,SAAS,wBAAmD;AAC5D,SAAS,qBAAqB,qBAAqB;AACnD,SAAS,sBAAsB;AAI/B,SAAS,iBAAiB;AAG1B,SAAS,kBAAkB;AAG3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAIP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;","names":[]}
1
+ {"version":3,"sources":["../../src/server/index.ts"],"sourcesContent":["/**\n * @xiwebdesign/xicms-core/server — server-only helpers (firebase-admin, sanitization, SEO,\n * pagina-reads). NOOIT importeren in client-componenten.\n */\nimport \"server-only\";\n\n// `getAdminAuth` zit BEWUST niet hier maar achter de subpath `@xiwebdesign/xicms-core/server/auth`\n// (zie ./auth.ts): firebase-admin/auth sloopt via jwks-rsa/jose élke server-render op Vercel, en\n// hoort dus niet in de bundel van sites die alleen pagina's lezen.\nexport { getAdminApp, getAdminDb } from \"../firebase/admin\";\nexport { sanitizeRichText } from \"./sanitize\";\n// Veilige JSON-LD-serialisatie voor <script type=\"application/ld+json\"> in consumer-routes\n// (escapet `<` zodat klant-tekst met </script> de pagina niet kan breken).\nexport { jsonLdHtml } from \"../blocks/builtins/jsonld\";\nexport {\n buildMetadata,\n buildJsonLd,\n buildSitemap,\n buildRobots,\n siteOrigin,\n businessJsonLd,\n breadcrumbListJsonLd,\n articleJsonLd,\n productJsonLd,\n openingHoursJsonLd,\n galleryJsonLd,\n videoObjectJsonLd,\n type BusinessInfo,\n} from \"./seo\";\nexport { validateEnv, assertEnv, type EnvReport } from \"./env\";\n// On-demand revalidation (6.5): POST-handler voor `app/api/revalidate/route.ts` in de klantsite.\n// De backend roept die route na een publicatie aan → wijziging binnen seconden live, zonder rebuild.\n// SSR-seeds (7.12): verzamelt server-side de blockData die de blokken van déze pagina nodig\n// hebben (shop, nieuws, formulieren, zoekindex) — één aanroep in de catch-all, en geen enkel blok\n// hoeft meer per bezoeker uit Firestore te lezen.\nexport { getPageBlockData, type PageBlockDataOptions } from \"./blockData\";\nexport { listFormDefinitions, listSiteMedia } from \"./pages\";\nexport { revalidatePOST } from \"./revalidate\";\n// Preview (Draft Mode): POST-handler voor `app/api/draft/route.ts` in de klantsite. Zet de\n// bypass-cookie zodat een ingelogde editor de content-bundel + ISR-cache overslaat, terwijl de\n// pagina-route voor publiek statisch blijft.\nexport { draftPOST, maakDraftGET } from \"./draft\";\nexport { signPreviewToken, verifyPreviewToken, type PreviewClaims } from \"./previewToken\";\n// Doorverwijzingen (7.24): `resolveRedirect(siteId, path)` in de catch-all vlak vóór notFound().\nexport { resolveRedirect } from \"./redirects\";\n// Versie-endpoint: GET-handler voor `app/api/version/route.ts`, zodat het superbeheer kan zien op\n// welke engine een DEPLOYMENT draait (en niet alleen wanneer er voor het laatst is ingelogd).\nexport { versionGET } from \"./version\";\n// Shop-SSR (6.0): opt-in via subpath `@xiwebdesign/xicms-core/server/shop` (readers + product-SEO).\n// Niet meer onvoorwaardelijk her-geëxporteerd, zodat shop-servercode niet in elke site landt.\nexport {\n pathFromSlugs,\n listPublishedPages,\n listPublishedNews,\n getPublishedPageByPath,\n getPublishedPageByPathExpanded,\n getSiteSymbols,\n expandSymbols,\n getSiteCustomBlocks,\n getPublicSiteSettings,\n getDraftPageById,\n getDraftPageByPath,\n type StoredPage,\n} from \"./pages\";\n// Content-bundle (5.7): build-time reader van `content/published.bundle.json`. De pagina-/settings-/\n// symbol-/customBlock-readers hierboven zijn al bundle-first; deze exports zijn voor directe\n// consumptie (bv. nieuws-blockData in de klant-routes).\nexport {\n getBundle,\n getBundlePage,\n getBundleNews,\n getNewsPageData,\n type PublishedBundle,\n type BundleItem,\n} from \"./bundle\";\n// Localisatie (5.0) — server-safe overlay-helpers, ook bruikbaar in route-handlers/layouts.\nexport {\n siteLocales,\n parseLocaleSlugs,\n localizedPath,\n localizePage,\n localizeBlocks,\n pruneTranslations,\n TranslationsSchema,\n type Translations,\n type LocaleConfig,\n} from \"../i18n/overlay\";\n"],"mappings":"AAIA,OAAO;AAKP,SAAS,aAAa,kBAAkB;AACxC,SAAS,wBAAwB;AAGjC,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,aAAa,iBAAiC;AAMvD,SAAS,wBAAmD;AAC5D,SAAS,qBAAqB,qBAAqB;AACnD,SAAS,sBAAsB;AAI/B,SAAS,WAAW,oBAAoB;AACxC,SAAS,kBAAkB,0BAA8C;AAEzE,SAAS,uBAAuB;AAGhC,SAAS,kBAAkB;AAG3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAIP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;","names":[]}
@@ -272,7 +272,10 @@ async function readCollection(siteId, collectionId) {
272
272
  }
273
273
  }
274
274
  function isPublishedNews(data) {
275
- return data.published === true || data.status === "published";
275
+ const gepubliceerd = data.published === true || data.status === "published";
276
+ if (!gepubliceerd) return false;
277
+ const datum = typeof data.date === "string" ? data.date.slice(0, 10) : "";
278
+ return !datum || datum <= (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
276
279
  }
277
280
  function plain(v) {
278
281
  if (v == null || typeof v !== "object") return v;