@xiwebdesign/xicms-core 5.3.0 → 5.4.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 (69) hide show
  1. package/dist/XiCMSProvider.d.ts +11 -2
  2. package/dist/XiCMSProvider.js +10 -5
  3. package/dist/XiCMSProvider.js.map +1 -1
  4. package/dist/admin/blockEditors/GenericEditor.js +18 -8
  5. package/dist/admin/blockEditors/GenericEditor.js.map +1 -1
  6. package/dist/admin/customBlocks/CustomBlocksPanel.js +12 -2
  7. package/dist/admin/customBlocks/CustomBlocksPanel.js.map +1 -1
  8. package/dist/admin/lib/pagesClient.js +4 -1
  9. package/dist/admin/lib/pagesClient.js.map +1 -1
  10. package/dist/admin/lib/upload.js +14 -7
  11. package/dist/admin/lib/upload.js.map +1 -1
  12. package/dist/admin/media/MediaManager.js +60 -28
  13. package/dist/admin/media/MediaManager.js.map +1 -1
  14. package/dist/admin/media/MediaPicker.js +25 -15
  15. package/dist/admin/media/MediaPicker.js.map +1 -1
  16. package/dist/admin/pages/PageEditor.d.ts +2 -1
  17. package/dist/admin/pages/PageEditor.js +16 -2
  18. package/dist/admin/pages/PageEditor.js.map +1 -1
  19. package/dist/admin/pages/PagesPanel.js +9 -5
  20. package/dist/admin/pages/PagesPanel.js.map +1 -1
  21. package/dist/admin/visual/VisualEditor.js +11 -2
  22. package/dist/admin/visual/VisualEditor.js.map +1 -1
  23. package/dist/agentFiles/files.d.ts +15 -3
  24. package/dist/agentFiles/files.js +57 -0
  25. package/dist/agentFiles/files.js.map +1 -1
  26. package/dist/auth/AuthProvider.js +3 -3
  27. package/dist/auth/AuthProvider.js.map +1 -1
  28. package/dist/blocks/builtins/Columns.js +1 -1
  29. package/dist/blocks/builtins/Columns.js.map +1 -1
  30. package/dist/blocks/builtins/Gallery.js +2 -2
  31. package/dist/blocks/builtins/Gallery.js.map +1 -1
  32. package/dist/blocks/builtins/Hero.js +1 -1
  33. package/dist/blocks/builtins/Hero.js.map +1 -1
  34. package/dist/blocks/builtins/SymbolRef.js.map +1 -1
  35. package/dist/components/PageView.js +4 -3
  36. package/dist/components/PageView.js.map +1 -1
  37. package/dist/demo/index.d.ts +7 -0
  38. package/dist/demo/index.js +23 -0
  39. package/dist/demo/index.js.map +1 -0
  40. package/dist/index.d.ts +2 -4
  41. package/dist/index.js +0 -20
  42. package/dist/index.js.map +1 -1
  43. package/dist/modules/registry.d.ts +3 -4
  44. package/dist/modules/registry.js +2 -3
  45. package/dist/modules/registry.js.map +1 -1
  46. package/dist/schema/blocks.d.ts +27 -12
  47. package/dist/schema/blocks.js +3 -0
  48. package/dist/schema/blocks.js.map +1 -1
  49. package/dist/schema/settings.d.ts +1 -34
  50. package/dist/schema/settings.js +2 -8
  51. package/dist/schema/settings.js.map +1 -1
  52. package/dist/schema/siteConfig.d.ts +3 -71
  53. package/dist/schema/siteConfig.js +3 -8
  54. package/dist/schema/siteConfig.js.map +1 -1
  55. package/dist/schema/symbols.d.ts +2 -2
  56. package/dist/schema/symbols.js.map +1 -1
  57. package/dist/server/index.d.ts +1 -1
  58. package/dist/server/index.js +2 -0
  59. package/dist/server/index.js.map +1 -1
  60. package/dist/server/pages.d.ts +23 -3
  61. package/dist/server/pages.js +37 -3
  62. package/dist/server/pages.js.map +1 -1
  63. package/dist/server/seo.js +5 -3
  64. package/dist/server/seo.js.map +1 -1
  65. package/dist/settings/useSiteSettings.d.ts +15 -3
  66. package/dist/settings/useSiteSettings.js +26 -5
  67. package/dist/settings/useSiteSettings.js.map +1 -1
  68. package/dist/styles.css +14 -0
  69. package/package.json +6 -2
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/schema/settings.ts"],"sourcesContent":["import { z } from \"zod\";\nimport { SeoSchema } from \"./seo\";\n\n/** Eén navigatie-item. */\nexport const NavItemSchema = z.object({\n label: z.string().min(1).max(80),\n href: z.string().min(1).max(2000),\n});\nexport type NavItem = z.infer<typeof NavItemSchema>;\n\n/** Huisstijl-tokens (worden CSS custom properties). Alle uitbreidingen zijn optioneel/update-veilig. */\nexport const ThemeSchema = z.object({\n primary: z.string().max(40),\n accent: z.string().max(40).optional(),\n background: z.string().max(40).optional(),\n foreground: z.string().max(40).optional(),\n fontFamily: z.string().max(200).optional(),\n /** Optioneel apart lettertype voor koppen (display/serif), voor een elegantere typografie. */\n headingFont: z.string().max(200).optional(),\n /** Optionele stylesheet-URL voor het lettertype (bv. een Google Fonts CSS-link). */\n fontUrl: z.string().max(500).optional(),\n /** Afrondingsschaal (sm/md/lg). */\n radius: z.object({ sm: z.string().max(20).optional(), md: z.string().max(20).optional(), lg: z.string().max(20).optional() }).optional(),\n /** Ruimteschaal voor block-padding (xs..xl). */\n spacing: z\n .object({\n xs: z.string().max(20).optional(),\n sm: z.string().max(20).optional(),\n md: z.string().max(20).optional(),\n lg: z.string().max(20).optional(),\n xl: z.string().max(20).optional(),\n })\n .optional(),\n /** Maximale container-breedte + horizontale padding. */\n container: z.object({ maxWidth: z.string().max(20).optional(), padding: z.string().max(20).optional() }).optional(),\n /** Optionele donkere modus (volgt het systeem als `auto`). */\n dark: z\n .object({\n auto: z.boolean().default(false),\n background: z.string().max(40).optional(),\n foreground: z.string().max(40).optional(),\n primary: z.string().max(40).optional(),\n })\n .optional(),\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.enum([\"xicms\", \"client\"]).default(\"xicms\"),\n /** Geverifieerd from-adres (verplicht ongeacht mode; bij \"client\" het klant-domein). */\n fromEmail: z.string().email().optional(),\n fromName: z.string().max(120).optional(),\n /** Waar notificaties heen gaan (meestal het klant-mailadres). */\n notifyTo: z.string().email().optional(),\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().min(1).max(40),\n href: z.string().min(1).max(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.string().max(120).optional(),\n links: z.array(NavItemSchema).max(20).default([]),\n});\n\n/** Bewerkbare footer (klant past dit aan in /beheer; core levert de unstyled `SiteFooter`). */\nexport const FooterSettingsSchema = z.object({\n columns: z.array(FooterColumnSchema).max(6).optional(),\n social: z.array(SocialLinkSchema).max(12).optional(),\n copyright: z.string().max(300).optional(),\n /** Toon de \"Beheer\"-link in de footer (default: aan). */\n showAdminLink: z.boolean().optional(),\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.boolean().optional(), config: z.record(z.string(), z.unknown()).optional() }),\n ]),\n);\n\n/** Het `sites/{siteId}/settings/site`-document: alle bewerkbare site-instellingen. */\nexport const SiteSettingsSchema = z.object({\n name: z.string().min(1).max(160).optional(),\n domain: z.string().max(255).optional(),\n contactEmail: z.string().email().optional(),\n nav: z.array(NavItemSchema).max(50).optional(),\n theme: ThemeSchema.optional(),\n seoDefaults: SeoSchema.optional(),\n forms: FormsSenderSchema.optional(),\n features: z\n .object({\n pages: z.boolean().optional(),\n news: z.boolean().optional(),\n media: z.boolean().optional(),\n forms: z.boolean().optional(),\n stats: z.boolean().optional(),\n })\n .optional(),\n footer: FooterSettingsSchema.optional(),\n modules: ModulesSettingsSchema.optional(),\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.string().max(160).optional(),\n nav: z.array(NavItemSchema).max(50).optional(),\n theme: ThemeSchema.optional(),\n seoDefaults: SeoSchema.optional(),\n footer: FooterSettingsSchema.optional(),\n modules: ModulesSettingsSchema.optional(),\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":"AAAA,SAAS,SAAS;AAClB,SAAS,iBAAiB;AAGnB,MAAM,gBAAgB,EAAE,OAAO;AAAA,EACpC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE;AAAA,EAC/B,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAI;AAClC,CAAC;AAIM,MAAM,cAAc,EAAE,OAAO;AAAA,EAClC,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE;AAAA,EAC1B,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,EACpC,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,EACxC,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,EACxC,YAAY,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA;AAAA,EAEzC,aAAa,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA;AAAA,EAE1C,SAAS,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA;AAAA,EAEtC,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,SAAS,GAAG,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,SAAS,GAAG,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS;AAAA;AAAA,EAEvI,SAAS,EACN,OAAO;AAAA,IACN,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,IAChC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,IAChC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,IAChC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,IAChC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,EAClC,CAAC,EACA,SAAS;AAAA;AAAA,EAEZ,WAAW,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,SAAS,GAAG,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS;AAAA;AAAA,EAElH,MAAM,EACH,OAAO;AAAA,IACN,MAAM,EAAE,QAAQ,EAAE,QAAQ,KAAK;AAAA,IAC/B,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,IACxC,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,IACxC,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,EACvC,CAAC,EACA,SAAS;AACd,CAAC;AASM,SAAS,iBAAiB,MAAmB,UAAqC;AACvF,MAAI,CAAC,SAAU,QAAO;AACtB,QAAM,SAAsB,EAAE,GAAG,MAAM,GAAG,SAAS;AACnD,aAAW,OAAO,CAAC,UAAU,WAAW,aAAa,MAAM,GAAY;AACrE,QAAI,KAAK,GAAG,KAAK,SAAS,GAAG,GAAG;AAC9B,aAAO,GAAG,IAAI,EAAE,GAAG,KAAK,GAAG,GAAG,GAAG,SAAS,GAAG,EAAE;AAAA,IACjD,WAAW,KAAK,GAAG,KAAK,CAAC,SAAS,GAAG,GAAG;AACtC,aAAO,GAAG,IAAI,KAAK,GAAG;AAAA,IACxB;AAAA,EACF;AACA,SAAO;AACT;AAQO,MAAM,oBAAoB,EAAE,OAAO;AAAA,EACxC,MAAM,EAAE,KAAK,CAAC,SAAS,QAAQ,CAAC,EAAE,QAAQ,OAAO;AAAA;AAAA,EAEjD,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS;AAAA,EACvC,UAAU,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA;AAAA,EAEvC,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS;AACxC,CAAC;AAIM,MAAM,mBAAmB,EAAE,OAAO;AAAA,EACvC,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE;AAAA,EAClC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAI;AAClC,CAAC;AAIM,MAAM,qBAAqB,EAAE,OAAO;AAAA,EACzC,SAAS,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EACtC,OAAO,EAAE,MAAM,aAAa,EAAE,IAAI,EAAE,EAAE,QAAQ,CAAC,CAAC;AAClD,CAAC;AAGM,MAAM,uBAAuB,EAAE,OAAO;AAAA,EAC3C,SAAS,EAAE,MAAM,kBAAkB,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,EACrD,QAAQ,EAAE,MAAM,gBAAgB,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,EACnD,WAAW,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA;AAAA,EAExC,eAAe,EAAE,QAAQ,EAAE,SAAS;AACtC,CAAC;AAIM,MAAM,wBAAwB,EAAE;AAAA,EACrC,EAAE,OAAO;AAAA,EACT,EAAE,MAAM;AAAA,IACN,EAAE,QAAQ;AAAA,IACV,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,GAAG,QAAQ,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,SAAS,EAAE,CAAC;AAAA,EACpG,CAAC;AACH;AAGO,MAAM,qBAAqB,EAAE,OAAO;AAAA,EACzC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EAC1C,QAAQ,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EACrC,cAAc,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS;AAAA,EAC1C,KAAK,EAAE,MAAM,aAAa,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,EAC7C,OAAO,YAAY,SAAS;AAAA,EAC5B,aAAa,UAAU,SAAS;AAAA,EAChC,OAAO,kBAAkB,SAAS;AAAA,EAClC,UAAU,EACP,OAAO;AAAA,IACN,OAAO,EAAE,QAAQ,EAAE,SAAS;AAAA,IAC5B,MAAM,EAAE,QAAQ,EAAE,SAAS;AAAA,IAC3B,OAAO,EAAE,QAAQ,EAAE,SAAS;AAAA,IAC5B,OAAO,EAAE,QAAQ,EAAE,SAAS;AAAA,IAC5B,OAAO,EAAE,QAAQ,EAAE,SAAS;AAAA,EAC9B,CAAC,EACA,SAAS;AAAA,EACZ,QAAQ,qBAAqB,SAAS;AAAA,EACtC,SAAS,sBAAsB,SAAS;AAC1C,CAAC;AAQM,MAAM,2BAA2B,EAAE,OAAO;AAAA,EAC/C,MAAM,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EACnC,KAAK,EAAE,MAAM,aAAa,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,EAC7C,OAAO,YAAY,SAAS;AAAA,EAC5B,aAAa,UAAU,SAAS;AAAA,EAChC,QAAQ,qBAAqB,SAAS;AAAA,EACtC,SAAS,sBAAsB,SAAS;AAC1C,CAAC;AAIM,SAAS,iBAAiB,GAAqC;AACpE,SAAO;AAAA,IACL,MAAM,EAAE;AAAA,IACR,KAAK,EAAE;AAAA,IACP,OAAO,EAAE;AAAA,IACT,aAAa,EAAE;AAAA,IACf,QAAQ,EAAE;AAAA,IACV,SAAS,EAAE;AAAA,EACb;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/schema/settings.ts"],"sourcesContent":["import { z } from \"zod\";\nimport { SeoSchema } from \"./seo\";\n\n/** Eén navigatie-item. */\nexport const NavItemSchema = z.object({\n label: z.string().min(1).max(80),\n href: z.string().min(1).max(2000),\n});\nexport type NavItem = z.infer<typeof NavItemSchema>;\n\n/** Huisstijl-tokens (worden CSS custom properties). Alle uitbreidingen zijn optioneel/update-veilig. */\nexport const ThemeSchema = z.object({\n primary: z.string().max(40),\n accent: z.string().max(40).optional(),\n background: z.string().max(40).optional(),\n foreground: z.string().max(40).optional(),\n fontFamily: z.string().max(200).optional(),\n /** Optioneel apart lettertype voor koppen (display/serif), voor een elegantere typografie. */\n headingFont: z.string().max(200).optional(),\n /** Optionele stylesheet-URL voor het lettertype (bv. een Google Fonts CSS-link). */\n fontUrl: z.string().max(500).optional(),\n /** Afrondingsschaal (sm/md/lg). */\n radius: z.object({ sm: z.string().max(20).optional(), md: z.string().max(20).optional(), lg: z.string().max(20).optional() }).optional(),\n /** Ruimteschaal voor block-padding (xs..xl). */\n spacing: z\n .object({\n xs: z.string().max(20).optional(),\n sm: z.string().max(20).optional(),\n md: z.string().max(20).optional(),\n lg: z.string().max(20).optional(),\n xl: z.string().max(20).optional(),\n })\n .optional(),\n /** Maximale container-breedte + horizontale padding. */\n container: z.object({ maxWidth: z.string().max(20).optional(), padding: z.string().max(20).optional() }).optional(),\n /** Optionele donkere modus (volgt het systeem als `auto`). */\n dark: z\n .object({\n auto: z.boolean().default(false),\n background: z.string().max(40).optional(),\n foreground: z.string().max(40).optional(),\n primary: z.string().max(40).optional(),\n })\n .optional(),\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.enum([\"xicms\", \"client\"]).default(\"xicms\"),\n /** Geverifieerd from-adres (verplicht ongeacht mode; bij \"client\" het klant-domein). */\n fromEmail: z.string().email().optional(),\n fromName: z.string().max(120).optional(),\n /** Waar notificaties heen gaan (meestal het klant-mailadres). */\n notifyTo: z.string().email().optional(),\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().min(1).max(40),\n href: z.string().min(1).max(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.string().max(120).optional(),\n links: z.array(NavItemSchema).max(20).default([]),\n});\n\n/** Bewerkbare footer (klant past dit aan in /beheer; core levert de unstyled `SiteFooter`). */\nexport const FooterSettingsSchema = z.object({\n columns: z.array(FooterColumnSchema).max(6).optional(),\n social: z.array(SocialLinkSchema).max(12).optional(),\n copyright: z.string().max(300).optional(),\n /** Toon de \"Beheer\"-link in de footer (default: aan). */\n showAdminLink: z.boolean().optional(),\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.boolean().optional(), config: z.record(z.string(), z.unknown()).optional() }),\n ]),\n);\n\n/** Het `sites/{siteId}/settings/site`-document: alle bewerkbare site-instellingen. */\nexport const SiteSettingsSchema = z.object({\n name: z.string().min(1).max(160).optional(),\n // Zelfde hostname-regel als SiteConfigSchema.domain (geen protocol/pad).\n domain: z\n .string()\n .max(255)\n .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)\")\n .optional()\n .or(z.literal(\"\")),\n contactEmail: z.string().email().optional(),\n nav: z.array(NavItemSchema).max(50).optional(),\n theme: ThemeSchema.optional(),\n seoDefaults: SeoSchema.optional(),\n forms: FormsSenderSchema.optional(),\n footer: FooterSettingsSchema.optional(),\n modules: ModulesSettingsSchema.optional(),\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.string().max(160).optional(),\n nav: z.array(NavItemSchema).max(50).optional(),\n theme: ThemeSchema.optional(),\n seoDefaults: SeoSchema.optional(),\n footer: FooterSettingsSchema.optional(),\n modules: ModulesSettingsSchema.optional(),\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":[]}
@@ -45,17 +45,6 @@ export interface SiteConfig {
45
45
  seoDefaults?: Seo;
46
46
  /** Afzender-instelling voor formulier-e-mail (fallback voor settings/site). */
47
47
  forms?: FormsSender;
48
- /**
49
- * @deprecated Gebruik `modules` (XiCMS 3.0). Blijft werken als legacy-brug: `features[key] ===
50
- * false` zet de bijbehorende module uit. Aan/uit zetten van optionele beheer-panelen.
51
- */
52
- features?: {
53
- pages?: boolean;
54
- news?: boolean;
55
- media?: boolean;
56
- forms?: boolean;
57
- stats?: boolean;
58
- };
59
48
  /**
60
49
  * Modules per site aan/uit zetten en configureren (XiCMS 3.0). Een uitgezette module verbergt
61
50
  * zowel zijn blocks als zijn beheer-paneel. Per module kun je ook `config` meegeven (gevalideerd
@@ -80,7 +69,7 @@ export declare function defineSiteConfig(config: SiteConfig): SiteConfig;
80
69
  export declare const SiteConfigSchema: z.ZodObject<{
81
70
  siteId: z.ZodString;
82
71
  name: z.ZodString;
83
- domain: z.ZodOptional<z.ZodString>;
72
+ domain: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
84
73
  mode: z.ZodEnum<["demo", "live"]>;
85
74
  demoEditing: z.ZodOptional<z.ZodBoolean>;
86
75
  lang: z.ZodOptional<z.ZodString>;
@@ -262,25 +251,6 @@ export declare const SiteConfigSchema: z.ZodObject<{
262
251
  fromName?: string | undefined;
263
252
  notifyTo?: string | undefined;
264
253
  }>>;
265
- features: z.ZodOptional<z.ZodObject<{
266
- pages: z.ZodOptional<z.ZodBoolean>;
267
- news: z.ZodOptional<z.ZodBoolean>;
268
- media: z.ZodOptional<z.ZodBoolean>;
269
- forms: z.ZodOptional<z.ZodBoolean>;
270
- stats: z.ZodOptional<z.ZodBoolean>;
271
- }, "strip", z.ZodTypeAny, {
272
- forms?: boolean | undefined;
273
- pages?: boolean | undefined;
274
- news?: boolean | undefined;
275
- media?: boolean | undefined;
276
- stats?: boolean | undefined;
277
- }, {
278
- forms?: boolean | undefined;
279
- pages?: boolean | undefined;
280
- news?: boolean | undefined;
281
- media?: boolean | undefined;
282
- stats?: boolean | undefined;
283
- }>>;
284
254
  modules: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodBoolean, z.ZodObject<{
285
255
  enabled: z.ZodOptional<z.ZodBoolean>;
286
256
  config: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
@@ -294,7 +264,7 @@ export declare const SiteConfigSchema: z.ZodObject<{
294
264
  }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
295
265
  siteId: z.ZodString;
296
266
  name: z.ZodString;
297
- domain: z.ZodOptional<z.ZodString>;
267
+ domain: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
298
268
  mode: z.ZodEnum<["demo", "live"]>;
299
269
  demoEditing: z.ZodOptional<z.ZodBoolean>;
300
270
  lang: z.ZodOptional<z.ZodString>;
@@ -476,25 +446,6 @@ export declare const SiteConfigSchema: z.ZodObject<{
476
446
  fromName?: string | undefined;
477
447
  notifyTo?: string | undefined;
478
448
  }>>;
479
- features: z.ZodOptional<z.ZodObject<{
480
- pages: z.ZodOptional<z.ZodBoolean>;
481
- news: z.ZodOptional<z.ZodBoolean>;
482
- media: z.ZodOptional<z.ZodBoolean>;
483
- forms: z.ZodOptional<z.ZodBoolean>;
484
- stats: z.ZodOptional<z.ZodBoolean>;
485
- }, "strip", z.ZodTypeAny, {
486
- forms?: boolean | undefined;
487
- pages?: boolean | undefined;
488
- news?: boolean | undefined;
489
- media?: boolean | undefined;
490
- stats?: boolean | undefined;
491
- }, {
492
- forms?: boolean | undefined;
493
- pages?: boolean | undefined;
494
- news?: boolean | undefined;
495
- media?: boolean | undefined;
496
- stats?: boolean | undefined;
497
- }>>;
498
449
  modules: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodBoolean, z.ZodObject<{
499
450
  enabled: z.ZodOptional<z.ZodBoolean>;
500
451
  config: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
@@ -508,7 +459,7 @@ export declare const SiteConfigSchema: z.ZodObject<{
508
459
  }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
509
460
  siteId: z.ZodString;
510
461
  name: z.ZodString;
511
- domain: z.ZodOptional<z.ZodString>;
462
+ domain: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
512
463
  mode: z.ZodEnum<["demo", "live"]>;
513
464
  demoEditing: z.ZodOptional<z.ZodBoolean>;
514
465
  lang: z.ZodOptional<z.ZodString>;
@@ -690,25 +641,6 @@ export declare const SiteConfigSchema: z.ZodObject<{
690
641
  fromName?: string | undefined;
691
642
  notifyTo?: string | undefined;
692
643
  }>>;
693
- features: z.ZodOptional<z.ZodObject<{
694
- pages: z.ZodOptional<z.ZodBoolean>;
695
- news: z.ZodOptional<z.ZodBoolean>;
696
- media: z.ZodOptional<z.ZodBoolean>;
697
- forms: z.ZodOptional<z.ZodBoolean>;
698
- stats: z.ZodOptional<z.ZodBoolean>;
699
- }, "strip", z.ZodTypeAny, {
700
- forms?: boolean | undefined;
701
- pages?: boolean | undefined;
702
- news?: boolean | undefined;
703
- media?: boolean | undefined;
704
- stats?: boolean | undefined;
705
- }, {
706
- forms?: boolean | undefined;
707
- pages?: boolean | undefined;
708
- news?: boolean | undefined;
709
- media?: boolean | undefined;
710
- stats?: boolean | undefined;
711
- }>>;
712
644
  modules: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodBoolean, z.ZodObject<{
713
645
  enabled: z.ZodOptional<z.ZodBoolean>;
714
646
  config: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
@@ -7,7 +7,9 @@ function defineSiteConfig(config) {
7
7
  const SiteConfigSchema = z.object({
8
8
  siteId: z.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*$/, "gebruik kleine letters/cijfers/koppeltekens (bv. 'bakkerij-jansen')"),
9
9
  name: z.string().min(1).max(160),
10
- domain: z.string().max(255).optional(),
10
+ // Alleen een hostname (www.klant.nl) — géén protocol/pad: "https://…/x" glipte er eerst
11
+ // doorheen en werd maar half genormaliseerd in siteOrigin/buildMetadata.
12
+ domain: z.string().max(255).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)").optional().or(z.literal("")),
11
13
  mode: z.enum(["demo", "live"]),
12
14
  demoEditing: z.boolean().optional(),
13
15
  lang: z.string().max(10).optional(),
@@ -18,13 +20,6 @@ const SiteConfigSchema = z.object({
18
20
  contactEmail: z.string().email().optional(),
19
21
  seoDefaults: SeoSchema.optional(),
20
22
  forms: FormsSenderSchema.optional(),
21
- features: z.object({
22
- pages: z.boolean().optional(),
23
- news: z.boolean().optional(),
24
- media: z.boolean().optional(),
25
- forms: z.boolean().optional(),
26
- stats: z.boolean().optional()
27
- }).optional(),
28
23
  modules: ModulesSettingsSchema.optional()
29
24
  }).passthrough();
30
25
  function validateSiteConfig(config) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/schema/siteConfig.ts"],"sourcesContent":["/**\n * SiteConfig — de statische basisconfig in een klantrepo (`content/site.config.ts`).\n * Dit is de fallback/identiteit van de site; bewerkbare instellingen kunnen in Firebase\n * (`sites/{siteId}/settings/site`) staan en deze waarden overschrijven.\n *\n * `@xiwebdesign/xicms-core` bevat hier nooit klantgegevens — die staan in de klantrepo.\n */\nimport { z } from \"zod\";\nimport { NavItemSchema, ThemeSchema, FormsSenderSchema, ModulesSettingsSchema, type NavItem, type ThemeTokens, type FormsSender } from \"./settings\";\nimport { SeoSchema, type Seo } from \"./seo\";\nimport type { ModulesSiteConfig } from \"../modules/types\";\n\nexport type SiteMode = \"demo\" | \"live\";\n\nexport interface SiteConfig {\n /** Uniek tenant-id; bepaalt het pad `sites/{siteId}/...` in Firebase. */\n siteId: string;\n /** Weergavenaam van het bedrijf. */\n name: string;\n /** Productiedomein, bv. \"www.bakkerijjansen.nl\". */\n domain?: string;\n /** \"demo\" toont de lock/CTA; \"live\" is de productiestand. */\n mode: SiteMode;\n /**\n * Demo-bewerkmodus: toont een pseudo-login zodat bezoekers het beheer (teksten + blokken) kunnen\n * uitproberen. Alle \"bewerkingen\" blijven UITSLUITEND lokaal in de browser (localStorage) — er gaat\n * niets naar Firebase. Alleen bedoeld voor een demo-site; laat dit `false`/weg op echte klantsites.\n */\n demoEditing?: boolean;\n /** Taalcode voor `<html lang>` en a11y (default \"nl\"). */\n lang?: string;\n /**\n * Extra talen naast de default (5.0), bv. `[\"de\", \"en\"]`. Vertalingen leven als overlay op\n * pagina's/symbolen; niet-default talen krijgen een URL-prefix (`/de/over-ons`). Weglaten =\n * ééntalige site (geen overhead).\n */\n locales?: string[];\n /** Default-taal van de canonieke content (default: `lang` ?? \"nl\"). */\n defaultLocale?: string;\n /** Huisstijl. */\n theme: ThemeTokens;\n /** Hoofdnavigatie. */\n nav?: NavItem[];\n /** Contact-e-mailadres voor de demo-CTA en formulieren. */\n contactEmail?: string;\n /** Standaard-SEO als een pagina niets opgeeft. */\n seoDefaults?: Seo;\n /** Afzender-instelling voor formulier-e-mail (fallback voor settings/site). */\n forms?: FormsSender;\n /**\n * @deprecated Gebruik `modules` (XiCMS 3.0). Blijft werken als legacy-brug: `features[key] ===\n * false` zet de bijbehorende module uit. Aan/uit zetten van optionele beheer-panelen.\n */\n features?: {\n pages?: boolean;\n news?: boolean;\n media?: boolean;\n forms?: boolean;\n stats?: boolean;\n };\n /**\n * Modules per site aan/uit zetten en configureren (XiCMS 3.0). Een uitgezette module verbergt\n * zowel zijn blocks als zijn beheer-paneel. Per module kun je ook `config` meegeven (gevalideerd\n * tegen het `settingsSchema` van de module) zodat de agent gedrag instelt zonder core te forken:\n *\n * modules: {\n * gallery: { enabled: true, config: { lightbox: true, columns: 3 } },\n * stats: false,\n * }\n *\n * Afwezig = de standaard van de module (`defaultEnabled`, doorgaans aan).\n */\n modules?: ModulesSiteConfig;\n}\n\n/** Helper met type-inferentie voor `site.config.ts`. */\nexport function defineSiteConfig(config: SiteConfig): SiteConfig {\n return config;\n}\n\n/**\n * Runtime-spiegel van de `SiteConfig`-interface. TypeScript vangt fouten bij compileren, maar een\n * (agent-)geschreven `site.config.ts` kan alsnog ongeldige waarden bevatten (verkeerd siteId-formaat,\n * te lange strings, kapotte nav-items). `.passthrough()` zodat extra klant-velden nooit een fout zijn.\n */\nexport const SiteConfigSchema = z\n .object({\n siteId: z\n .string()\n .min(1)\n .max(64)\n .regex(/^[a-z0-9][a-z0-9-]*$/, \"gebruik kleine letters/cijfers/koppeltekens (bv. 'bakkerij-jansen')\"),\n name: z.string().min(1).max(160),\n domain: z.string().max(255).optional(),\n mode: z.enum([\"demo\", \"live\"]),\n demoEditing: z.boolean().optional(),\n lang: z.string().max(10).optional(),\n locales: z.array(z.string().min(2).max(10)).max(10).optional(),\n defaultLocale: z.string().min(2).max(10).optional(),\n theme: ThemeSchema,\n nav: z.array(NavItemSchema).max(50).optional(),\n contactEmail: z.string().email().optional(),\n seoDefaults: SeoSchema.optional(),\n forms: FormsSenderSchema.optional(),\n features: z\n .object({\n pages: z.boolean().optional(),\n news: z.boolean().optional(),\n media: z.boolean().optional(),\n forms: z.boolean().optional(),\n stats: z.boolean().optional(),\n })\n .optional(),\n modules: ModulesSettingsSchema.optional(),\n })\n .passthrough();\n\n/**\n * Valideert een SiteConfig en geeft leesbare issues terug (leeg = geldig). Wordt in development\n * door de provider aangeroepen zodat een agent config-fouten direct in de console ziet, in plaats\n * van een subtiel kapotte site. Gooit bewust niet — een issue mag een site nooit platleggen.\n */\nexport function validateSiteConfig(config: unknown): string[] {\n const res = SiteConfigSchema.safeParse(config);\n if (res.success) return [];\n return res.error.issues.map((i) => `${i.path.join(\".\") || \"(root)\"}: ${i.message}`);\n}\n"],"mappings":"AAOA,SAAS,SAAS;AAClB,SAAS,eAAe,aAAa,mBAAmB,6BAA+E;AACvI,SAAS,iBAA2B;AAmE7B,SAAS,iBAAiB,QAAgC;AAC/D,SAAO;AACT;AAOO,MAAM,mBAAmB,EAC7B,OAAO;AAAA,EACN,QAAQ,EACL,OAAO,EACP,IAAI,CAAC,EACL,IAAI,EAAE,EACN,MAAM,wBAAwB,qEAAqE;AAAA,EACtG,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EAC/B,QAAQ,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EACrC,MAAM,EAAE,KAAK,CAAC,QAAQ,MAAM,CAAC;AAAA,EAC7B,aAAa,EAAE,QAAQ,EAAE,SAAS;AAAA,EAClC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,EAClC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,EAC7D,eAAe,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,EAClD,OAAO;AAAA,EACP,KAAK,EAAE,MAAM,aAAa,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,EAC7C,cAAc,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS;AAAA,EAC1C,aAAa,UAAU,SAAS;AAAA,EAChC,OAAO,kBAAkB,SAAS;AAAA,EAClC,UAAU,EACP,OAAO;AAAA,IACN,OAAO,EAAE,QAAQ,EAAE,SAAS;AAAA,IAC5B,MAAM,EAAE,QAAQ,EAAE,SAAS;AAAA,IAC3B,OAAO,EAAE,QAAQ,EAAE,SAAS;AAAA,IAC5B,OAAO,EAAE,QAAQ,EAAE,SAAS;AAAA,IAC5B,OAAO,EAAE,QAAQ,EAAE,SAAS;AAAA,EAC9B,CAAC,EACA,SAAS;AAAA,EACZ,SAAS,sBAAsB,SAAS;AAC1C,CAAC,EACA,YAAY;AAOR,SAAS,mBAAmB,QAA2B;AAC5D,QAAM,MAAM,iBAAiB,UAAU,MAAM;AAC7C,MAAI,IAAI,QAAS,QAAO,CAAC;AACzB,SAAO,IAAI,MAAM,OAAO,IAAI,CAAC,MAAM,GAAG,EAAE,KAAK,KAAK,GAAG,KAAK,QAAQ,KAAK,EAAE,OAAO,EAAE;AACpF;","names":[]}
1
+ {"version":3,"sources":["../../src/schema/siteConfig.ts"],"sourcesContent":["/**\n * SiteConfig — de statische basisconfig in een klantrepo (`content/site.config.ts`).\n * Dit is de fallback/identiteit van de site; bewerkbare instellingen kunnen in Firebase\n * (`sites/{siteId}/settings/site`) staan en deze waarden overschrijven.\n *\n * `@xiwebdesign/xicms-core` bevat hier nooit klantgegevens — die staan in de klantrepo.\n */\nimport { z } from \"zod\";\nimport { NavItemSchema, ThemeSchema, FormsSenderSchema, ModulesSettingsSchema, type NavItem, type ThemeTokens, type FormsSender } from \"./settings\";\nimport { SeoSchema, type Seo } from \"./seo\";\nimport type { ModulesSiteConfig } from \"../modules/types\";\n\nexport type SiteMode = \"demo\" | \"live\";\n\nexport interface SiteConfig {\n /** Uniek tenant-id; bepaalt het pad `sites/{siteId}/...` in Firebase. */\n siteId: string;\n /** Weergavenaam van het bedrijf. */\n name: string;\n /** Productiedomein, bv. \"www.bakkerijjansen.nl\". */\n domain?: string;\n /** \"demo\" toont de lock/CTA; \"live\" is de productiestand. */\n mode: SiteMode;\n /**\n * Demo-bewerkmodus: toont een pseudo-login zodat bezoekers het beheer (teksten + blokken) kunnen\n * uitproberen. Alle \"bewerkingen\" blijven UITSLUITEND lokaal in de browser (localStorage) — er gaat\n * niets naar Firebase. Alleen bedoeld voor een demo-site; laat dit `false`/weg op echte klantsites.\n */\n demoEditing?: boolean;\n /** Taalcode voor `<html lang>` en a11y (default \"nl\"). */\n lang?: string;\n /**\n * Extra talen naast de default (5.0), bv. `[\"de\", \"en\"]`. Vertalingen leven als overlay op\n * pagina's/symbolen; niet-default talen krijgen een URL-prefix (`/de/over-ons`). Weglaten =\n * ééntalige site (geen overhead).\n */\n locales?: string[];\n /** Default-taal van de canonieke content (default: `lang` ?? \"nl\"). */\n defaultLocale?: string;\n /** Huisstijl. */\n theme: ThemeTokens;\n /** Hoofdnavigatie. */\n nav?: NavItem[];\n /** Contact-e-mailadres voor de demo-CTA en formulieren. */\n contactEmail?: string;\n /** Standaard-SEO als een pagina niets opgeeft. */\n seoDefaults?: Seo;\n /** Afzender-instelling voor formulier-e-mail (fallback voor settings/site). */\n forms?: FormsSender;\n /**\n * Modules per site aan/uit zetten en configureren (XiCMS 3.0). Een uitgezette module verbergt\n * zowel zijn blocks als zijn beheer-paneel. Per module kun je ook `config` meegeven (gevalideerd\n * tegen het `settingsSchema` van de module) zodat de agent gedrag instelt zonder core te forken:\n *\n * modules: {\n * gallery: { enabled: true, config: { lightbox: true, columns: 3 } },\n * stats: false,\n * }\n *\n * Afwezig = de standaard van de module (`defaultEnabled`, doorgaans aan).\n */\n modules?: ModulesSiteConfig;\n}\n\n/** Helper met type-inferentie voor `site.config.ts`. */\nexport function defineSiteConfig(config: SiteConfig): SiteConfig {\n return config;\n}\n\n/**\n * Runtime-spiegel van de `SiteConfig`-interface. TypeScript vangt fouten bij compileren, maar een\n * (agent-)geschreven `site.config.ts` kan alsnog ongeldige waarden bevatten (verkeerd siteId-formaat,\n * te lange strings, kapotte nav-items). `.passthrough()` zodat extra klant-velden nooit een fout zijn.\n */\nexport const SiteConfigSchema = z\n .object({\n siteId: z\n .string()\n .min(1)\n .max(64)\n .regex(/^[a-z0-9][a-z0-9-]*$/, \"gebruik kleine letters/cijfers/koppeltekens (bv. 'bakkerij-jansen')\"),\n name: z.string().min(1).max(160),\n // Alleen een hostname (www.klant.nl) — géén protocol/pad: \"https://…/x\" glipte er eerst\n // doorheen en werd maar half genormaliseerd in siteOrigin/buildMetadata.\n domain: z\n .string()\n .max(255)\n .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)\")\n .optional()\n .or(z.literal(\"\")),\n mode: z.enum([\"demo\", \"live\"]),\n demoEditing: z.boolean().optional(),\n lang: z.string().max(10).optional(),\n locales: z.array(z.string().min(2).max(10)).max(10).optional(),\n defaultLocale: z.string().min(2).max(10).optional(),\n theme: ThemeSchema,\n nav: z.array(NavItemSchema).max(50).optional(),\n contactEmail: z.string().email().optional(),\n seoDefaults: SeoSchema.optional(),\n forms: FormsSenderSchema.optional(),\n modules: ModulesSettingsSchema.optional(),\n })\n .passthrough();\n\n/**\n * Valideert een SiteConfig en geeft leesbare issues terug (leeg = geldig). Wordt in development\n * door de provider aangeroepen zodat een agent config-fouten direct in de console ziet, in plaats\n * van een subtiel kapotte site. Gooit bewust niet — een issue mag een site nooit platleggen.\n */\nexport function validateSiteConfig(config: unknown): string[] {\n const res = SiteConfigSchema.safeParse(config);\n if (res.success) return [];\n return res.error.issues.map((i) => `${i.path.join(\".\") || \"(root)\"}: ${i.message}`);\n}\n"],"mappings":"AAOA,SAAS,SAAS;AAClB,SAAS,eAAe,aAAa,mBAAmB,6BAA+E;AACvI,SAAS,iBAA2B;AAwD7B,SAAS,iBAAiB,QAAgC;AAC/D,SAAO;AACT;AAOO,MAAM,mBAAmB,EAC7B,OAAO;AAAA,EACN,QAAQ,EACL,OAAO,EACP,IAAI,CAAC,EACL,IAAI,EAAE,EACN,MAAM,wBAAwB,qEAAqE;AAAA,EACtG,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA;AAAA;AAAA,EAG/B,QAAQ,EACL,OAAO,EACP,IAAI,GAAG,EACP,MAAM,sEAAsE,kEAAkE,EAC9I,SAAS,EACT,GAAG,EAAE,QAAQ,EAAE,CAAC;AAAA,EACnB,MAAM,EAAE,KAAK,CAAC,QAAQ,MAAM,CAAC;AAAA,EAC7B,aAAa,EAAE,QAAQ,EAAE,SAAS;AAAA,EAClC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,EAClC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,EAC7D,eAAe,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,EAClD,OAAO;AAAA,EACP,KAAK,EAAE,MAAM,aAAa,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,EAC7C,cAAc,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS;AAAA,EAC1C,aAAa,UAAU,SAAS;AAAA,EAChC,OAAO,kBAAkB,SAAS;AAAA,EAClC,SAAS,sBAAsB,SAAS;AAC1C,CAAC,EACA,YAAY;AAOR,SAAS,mBAAmB,QAA2B;AAC5D,QAAM,MAAM,iBAAiB,UAAU,MAAM;AAC7C,MAAI,IAAI,QAAS,QAAO,CAAC;AACzB,SAAO,IAAI,MAAM,OAAO,IAAI,CAAC,MAAM,GAAG,EAAE,KAAK,KAAK,GAAG,KAAK,QAAQ,KAAK,EAAE,OAAO,EAAE;AACpF;","names":[]}
@@ -199,7 +199,7 @@ export declare const SymbolSchema: z.ZodObject<{
199
199
  }, z.ZodTypeAny, "passthrough">[] | undefined;
200
200
  translations?: Record<string, Record<string, string>> | undefined;
201
201
  }>;
202
- export type Symbol = z.infer<typeof SymbolSchema>;
203
- export type StoredSymbol = Symbol & {
202
+ export type SymbolDefinition = z.infer<typeof SymbolSchema>;
203
+ export type StoredSymbol = SymbolDefinition & {
204
204
  id: string;
205
205
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/schema/symbols.ts"],"sourcesContent":["/**\n * Symbols (5.0, primo-concept): een symbol is een benoemde, gedeelde sectie — een compositie van\n * gewone blokken — die op meerdere pagina's als **referentie** wordt ingevoegd (`symbolRef`-blok).\n * Wijzig het symbol → alle pagina's die ernaar verwijzen bewegen mee (i.t.t. patterns, die bij\n * invoegen worden gekloond). \"Loskoppelen\" in de editor zet een referentie om in een kloon.\n *\n * Opslag: `sites/{siteId}/symbols/{symbolId}` — publiek leesbaar (de live site leest ze bij het\n * renderen), schrijven alleen via de `saveSymbol`-callable (sanitize + validatie + audit).\n * Een symbol mag GEEN symbolRef bevatten (geen recursie) — afgedwongen in schema én callable.\n */\nimport { z } from \"zod\";\nimport { BlockBaseSchema } from \"./blocks\";\nimport { TranslationsSchema } from \"../i18n/overlay\";\n\nexport const SymbolSchema = z.object({\n name: z.string().min(1).max(120).describe(\"Naam\"),\n /** De blok-compositie van het symbol (zelfde vorm als page.blocks). */\n blocks: z\n .array(BlockBaseSchema)\n .max(50)\n .default([])\n .superRefine((blocks, ctx) => {\n if (blocks.some((b) => b._type === \"symbolRef\")) {\n ctx.addIssue({ code: z.ZodIssueCode.custom, message: \"Een symbool kan geen ander symbool bevatten.\" });\n }\n }),\n /** Taal-overlay (5.0): per locale gepunte veldpaden → vertaalde tekst (zie i18n/overlay.ts). */\n translations: TranslationsSchema.optional(),\n});\nexport type Symbol = z.infer<typeof SymbolSchema>;\nexport type StoredSymbol = Symbol & { id: string };\n"],"mappings":"AAUA,SAAS,SAAS;AAClB,SAAS,uBAAuB;AAChC,SAAS,0BAA0B;AAE5B,MAAM,eAAe,EAAE,OAAO;AAAA,EACnC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS,MAAM;AAAA;AAAA,EAEhD,QAAQ,EACL,MAAM,eAAe,EACrB,IAAI,EAAE,EACN,QAAQ,CAAC,CAAC,EACV,YAAY,CAAC,QAAQ,QAAQ;AAC5B,QAAI,OAAO,KAAK,CAAC,MAAM,EAAE,UAAU,WAAW,GAAG;AAC/C,UAAI,SAAS,EAAE,MAAM,EAAE,aAAa,QAAQ,SAAS,+CAA+C,CAAC;AAAA,IACvG;AAAA,EACF,CAAC;AAAA;AAAA,EAEH,cAAc,mBAAmB,SAAS;AAC5C,CAAC;","names":[]}
1
+ {"version":3,"sources":["../../src/schema/symbols.ts"],"sourcesContent":["/**\n * Symbols (5.0, primo-concept): een symbol is een benoemde, gedeelde sectie — een compositie van\n * gewone blokken — die op meerdere pagina's als **referentie** wordt ingevoegd (`symbolRef`-blok).\n * Wijzig het symbol → alle pagina's die ernaar verwijzen bewegen mee (i.t.t. patterns, die bij\n * invoegen worden gekloond). \"Loskoppelen\" in de editor zet een referentie om in een kloon.\n *\n * Opslag: `sites/{siteId}/symbols/{symbolId}` — publiek leesbaar (de live site leest ze bij het\n * renderen), schrijven alleen via de `saveSymbol`-callable (sanitize + validatie + audit).\n * Een symbol mag GEEN symbolRef bevatten (geen recursie) — afgedwongen in schema én callable.\n */\nimport { z } from \"zod\";\nimport { BlockBaseSchema } from \"./blocks\";\nimport { TranslationsSchema } from \"../i18n/overlay\";\n\nexport const SymbolSchema = z.object({\n name: z.string().min(1).max(120).describe(\"Naam\"),\n /** De blok-compositie van het symbol (zelfde vorm als page.blocks). */\n blocks: z\n .array(BlockBaseSchema)\n .max(50)\n .default([])\n .superRefine((blocks, ctx) => {\n if (blocks.some((b) => b._type === \"symbolRef\")) {\n ctx.addIssue({ code: z.ZodIssueCode.custom, message: \"Een symbool kan geen ander symbool bevatten.\" });\n }\n }),\n /** Taal-overlay (5.0): per locale gepunte veldpaden → vertaalde tekst (zie i18n/overlay.ts). */\n translations: TranslationsSchema.optional(),\n});\n// \"SymbolDefinition\", niet \"Symbol\": dat schaduwde de JS-global Symbol in elke consumer-import.\nexport type SymbolDefinition = z.infer<typeof SymbolSchema>;\nexport type StoredSymbol = SymbolDefinition & { id: string };\n"],"mappings":"AAUA,SAAS,SAAS;AAClB,SAAS,uBAAuB;AAChC,SAAS,0BAA0B;AAE5B,MAAM,eAAe,EAAE,OAAO;AAAA,EACnC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS,MAAM;AAAA;AAAA,EAEhD,QAAQ,EACL,MAAM,eAAe,EACrB,IAAI,EAAE,EACN,QAAQ,CAAC,CAAC,EACV,YAAY,CAAC,QAAQ,QAAQ;AAC5B,QAAI,OAAO,KAAK,CAAC,MAAM,EAAE,UAAU,WAAW,GAAG;AAC/C,UAAI,SAAS,EAAE,MAAM,EAAE,aAAa,QAAQ,SAAS,+CAA+C,CAAC;AAAA,IACvG;AAAA,EACF,CAAC;AAAA;AAAA,EAEH,cAAc,mBAAmB,SAAS;AAC5C,CAAC;","names":[]}
@@ -8,5 +8,5 @@ export { sanitizeRichText } from "./sanitize";
8
8
  export { jsonLdHtml } from "../blocks/builtins/jsonld";
9
9
  export { buildMetadata, buildJsonLd, buildSitemap, buildRobots, siteOrigin, businessJsonLd, breadcrumbListJsonLd, articleJsonLd, productJsonLd, openingHoursJsonLd, galleryJsonLd, videoObjectJsonLd, type BusinessInfo, } from "./seo";
10
10
  export { validateEnv, assertEnv, type EnvReport } from "./env";
11
- export { pathFromSlugs, listPublishedPages, getPublishedPageByPath, getPublishedPageByPathExpanded, getSiteSymbols, expandSymbols, getSiteCustomBlocks, getDraftPageById, getDraftPageByPath, type StoredPage, } from "./pages";
11
+ export { pathFromSlugs, listPublishedPages, getPublishedPageByPath, getPublishedPageByPathExpanded, getSiteSymbols, expandSymbols, getSiteCustomBlocks, getPublicSiteSettings, getDraftPageById, getDraftPageByPath, type StoredPage, } from "./pages";
12
12
  export { siteLocales, parseLocaleSlugs, localizedPath, localizePage, localizeBlocks, pruneTranslations, TranslationsSchema, type Translations, type LocaleConfig, } from "../i18n/overlay";
@@ -25,6 +25,7 @@ import {
25
25
  getSiteSymbols,
26
26
  expandSymbols,
27
27
  getSiteCustomBlocks,
28
+ getPublicSiteSettings,
28
29
  getDraftPageById,
29
30
  getDraftPageByPath
30
31
  } from "./pages";
@@ -54,6 +55,7 @@ export {
54
55
  getAdminDb,
55
56
  getDraftPageById,
56
57
  getDraftPageByPath,
58
+ getPublicSiteSettings,
57
59
  getPublishedPageByPath,
58
60
  getPublishedPageByPathExpanded,
59
61
  getSiteCustomBlocks,
@@ -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\nexport { getAdminApp, getAdminAuth, 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\";\nexport {\n pathFromSlugs,\n listPublishedPages,\n getPublishedPageByPath,\n getPublishedPageByPathExpanded,\n getSiteSymbols,\n expandSymbols,\n getSiteCustomBlocks,\n getDraftPageById,\n getDraftPageByPath,\n type StoredPage,\n} from \"./pages\";\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;AAEP,SAAS,aAAa,cAAc,kBAAkB;AACtD,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;AACvD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;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\nexport { getAdminApp, getAdminAuth, 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\";\nexport {\n pathFromSlugs,\n listPublishedPages,\n getPublishedPageByPath,\n getPublishedPageByPathExpanded,\n getSiteSymbols,\n expandSymbols,\n getSiteCustomBlocks,\n getPublicSiteSettings,\n getDraftPageById,\n getDraftPageByPath,\n type StoredPage,\n} from \"./pages\";\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;AAEP,SAAS,aAAa,cAAc,kBAAkB;AACtD,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;AACvD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;","names":[]}
@@ -2,6 +2,7 @@ import "server-only";
2
2
  import { type Page } from "../schema/pages";
3
3
  import { type StoredSymbol } from "../schema/symbols";
4
4
  import { type CustomBlock } from "../schema/customBlocks";
5
+ import { type PublicSiteSettings } from "../schema/settings";
5
6
  export type StoredPage = Page & {
6
7
  id: string;
7
8
  };
@@ -9,14 +10,24 @@ export type StoredPage = Page & {
9
10
  export declare function pathFromSlugs(slugs: string[] | undefined): string;
10
11
  /** Alle gepubliceerde pagina's (sitemap / generateStaticParams). Keyless via REST als geen Admin. */
11
12
  export declare function listPublishedPages(siteId: string): Promise<StoredPage[]>;
13
+ /**
14
+ * Per-request-dedup via React cache(): metadata (generateMetadata) én de render lezen dezelfde
15
+ * pagina — zonder cache() waren dat twee identieke Firestore-reads per request. NB: de
16
+ * `next:{revalidate}`-hint op het REST-pad dekt dit niet (POST-fetches cachen niet); route-level
17
+ * ISR (revalidate=300 in de consumer) blijft de echte kosten-demper tussen requests.
18
+ */
19
+ export declare const getPublishedPageByPath: typeof getPublishedPageByPathUncached;
20
+ export declare const getSiteSymbols: typeof getSiteSymbolsUncached;
21
+ export declare const getSiteCustomBlocks: typeof getSiteCustomBlocksUncached;
22
+ export declare const getPublicSiteSettings: typeof getPublicSiteSettingsUncached;
12
23
  /** Eén gepubliceerde pagina op pad. Keyless via REST als geen Admin. */
13
- export declare function getPublishedPageByPath(siteId: string, path: string): Promise<StoredPage | null>;
24
+ declare function getPublishedPageByPathUncached(siteId: string, path: string): Promise<StoredPage | null>;
14
25
  /** Draft-versie (voor preview achter auth). */
15
26
  export declare function getDraftPageById(siteId: string, pageId: string): Promise<StoredPage | null>;
16
27
  /** Draft op pad (preview). */
17
28
  export declare function getDraftPageByPath(siteId: string, path: string): Promise<StoredPage | null>;
18
29
  /** Alle symbolen van een site (gevalideerd; ongeldige overgeslagen met waarschuwing). */
19
- export declare function getSiteSymbols(siteId: string): Promise<StoredSymbol[]>;
30
+ declare function getSiteSymbolsUncached(siteId: string): Promise<StoredSymbol[]>;
20
31
  /**
21
32
  * Vervangt symbolRef-blokken door de blokken van hun symbool (één niveau — symbolen kunnen zelf
22
33
  * geen symbolRef bevatten). `_key`s van de ingevoegde blokken worden geprefixt met de ref-key
@@ -32,5 +43,14 @@ export declare function expandSymbols<T extends {
32
43
  * én op de ingevoegde symbolen (elk symbool draagt zijn eigen `translations`).
33
44
  */
34
45
  export declare function getPublishedPageByPathExpanded(siteId: string, path: string, locale?: string): Promise<StoredPage | null>;
46
+ /**
47
+ * Publieke site-instellingen (`sites/{siteId}/settings/public`) server-side — voor de
48
+ * `settingsData`-prop van XiCMSProvider. Zo zit het klant-thema/nav/footer al in de server-HTML
49
+ * (geen theme-flits bij eerste paint) én vervalt de per-bezoeker `getDoc` in de browser: de
50
+ * layout-route is ISR-gecached, dus dit kost ~1 read per revalidate i.p.v. per bezoeker.
51
+ * Fouten leveren `{}` op — de site valt dan terug op de statische SiteConfig.
52
+ */
53
+ declare function getPublicSiteSettingsUncached(siteId: string): Promise<PublicSiteSettings>;
35
54
  /** Alle custom blocks van een site (gevalideerd) — voor SSR-registratie via XiCMSProvider. */
36
- export declare function getSiteCustomBlocks(siteId: string): Promise<CustomBlock[]>;
55
+ declare function getSiteCustomBlocksUncached(siteId: string): Promise<CustomBlock[]>;
56
+ export {};
@@ -1,8 +1,10 @@
1
1
  import "server-only";
2
+ import { cache } from "react";
2
3
  import { getAdminDb } from "../firebase/admin";
3
4
  import { PageSchema } from "../schema/pages";
4
5
  import { SymbolSchema } from "../schema/symbols";
5
6
  import { CustomBlockSchema } from "../schema/customBlocks";
7
+ import { PublicSiteSettingsSchema } from "../schema/settings";
6
8
  import { localizePage, localizeBlocks } from "../i18n/overlay";
7
9
  function toPath(slugs) {
8
10
  const clean = (slugs ?? []).filter(Boolean);
@@ -82,7 +84,11 @@ async function listPublishedPages(siteId) {
82
84
  }
83
85
  return restQuery(siteId, [eq("status", "published")]);
84
86
  }
85
- async function getPublishedPageByPath(siteId, path) {
87
+ const getPublishedPageByPath = cache(getPublishedPageByPathUncached);
88
+ const getSiteSymbols = cache(getSiteSymbolsUncached);
89
+ const getSiteCustomBlocks = cache(getSiteCustomBlocksUncached);
90
+ const getPublicSiteSettings = cache(getPublicSiteSettingsUncached);
91
+ async function getPublishedPageByPathUncached(siteId, path) {
86
92
  const adb = getAdminDb();
87
93
  if (adb) {
88
94
  return safeAdminRead(null, `getPublishedPageByPath ${siteId}${path}`, async () => {
@@ -138,7 +144,7 @@ async function readCollection(siteId, collectionId) {
138
144
  return [];
139
145
  }
140
146
  }
141
- async function getSiteSymbols(siteId) {
147
+ async function getSiteSymbolsUncached(siteId) {
142
148
  const rows = await readCollection(siteId, "symbols");
143
149
  const out = [];
144
150
  for (const r of rows) {
@@ -173,7 +179,34 @@ async function getPublishedPageByPathExpanded(siteId, path, locale) {
173
179
  }
174
180
  return expandSymbols(page, symbols);
175
181
  }
176
- async function getSiteCustomBlocks(siteId) {
182
+ async function getPublicSiteSettingsUncached(siteId) {
183
+ const adb = getAdminDb();
184
+ let data = {};
185
+ if (adb) {
186
+ data = await safeAdminRead({}, `getPublicSiteSettings ${siteId}`, async () => {
187
+ const d = await adb.doc(`sites/${siteId}/settings/public`).get();
188
+ return d.exists ? d.data() : {};
189
+ });
190
+ } else {
191
+ const apiKey = process.env.NEXT_PUBLIC_FIREBASE_API_KEY;
192
+ const projectId = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID;
193
+ if (apiKey && projectId) {
194
+ const url = `https://firestore.googleapis.com/v1/projects/${projectId}/databases/(default)/documents/sites/${siteId}/settings/public?key=${apiKey}`;
195
+ try {
196
+ const res = await fetch(url, { next: { revalidate: 300 } });
197
+ if (res.ok) {
198
+ const doc = await res.json();
199
+ data = decodeFields(doc.fields ?? {});
200
+ }
201
+ } catch (err) {
202
+ console.error("[xicms] Firestore REST-read van settings/public mislukt:", err);
203
+ }
204
+ }
205
+ }
206
+ const parsed = PublicSiteSettingsSchema.safeParse(data ?? {});
207
+ return parsed.success ? parsed.data : {};
208
+ }
209
+ async function getSiteCustomBlocksUncached(siteId) {
177
210
  const rows = await readCollection(siteId, "customBlocks");
178
211
  const out = [];
179
212
  for (const r of rows) {
@@ -187,6 +220,7 @@ export {
187
220
  expandSymbols,
188
221
  getDraftPageById,
189
222
  getDraftPageByPath,
223
+ getPublicSiteSettings,
190
224
  getPublishedPageByPath,
191
225
  getPublishedPageByPathExpanded,
192
226
  getSiteCustomBlocks,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/server/pages.ts"],"sourcesContent":["import \"server-only\";\n\nimport { getAdminDb } from \"../firebase/admin\";\nimport { PageSchema, type Page } from \"../schema/pages\";\nimport { SymbolSchema, type StoredSymbol } from \"../schema/symbols\";\nimport { CustomBlockSchema, type CustomBlock } from \"../schema/customBlocks\";\nimport { localizePage, localizeBlocks } from \"../i18n/overlay\";\n\nexport type StoredPage = Page & { id: string };\n\nfunction toPath(slugs: string[] | undefined): string {\n const clean = (slugs ?? []).filter(Boolean);\n return \"/\" + clean.join(\"/\");\n}\n\n/** Pad uit Next catch-all params (`[[...slug]]`). \"/\" voor de root. */\nexport function pathFromSlugs(slugs: string[] | undefined): string {\n const p = toPath(slugs);\n return p === \"/\" ? \"/\" : p.replace(/\\/$/, \"\");\n}\n\nfunction parse(id: string, data: unknown): StoredPage | null {\n const res = PageSchema.safeParse(data);\n if (!res.success) {\n // Niet stil 404'en: een gepubliceerde pagina die het schema niet haalt is een diagnose-signaal\n // (schema-evolutie zonder default, of corrupte write) — zelfde patroon als symbols/customBlocks.\n console.warn(`[xicms] pagina \"${id}\" faalt schema-validatie en wordt overgeslagen:`, res.error.issues[0]);\n return null;\n }\n return { id, ...res.data };\n}\n\n/**\n * Admin-SDK-reads mogen een PUBLIEKE pagina nooit laten 500'en — zelfde belofte als het REST-pad\n * (dat al overal try/catch → lege fallback heeft). Een Firestore-hikje wordt gelogd en levert de\n * fallback op; de route rendert dan 404/fallback i.p.v. een servererror.\n */\nasync function safeAdminRead<T>(fallback: T, what: string, fn: () => Promise<T>): Promise<T> {\n try {\n return await fn();\n } catch (e) {\n console.warn(`[xicms] Admin-read mislukt (${what}) — fallback gebruikt:`, (e as Error)?.message ?? e);\n return fallback;\n }\n}\n\n/* ---------------- keyless public reads via Firestore REST (alleen apiKey) ----------------\n * Gepubliceerde pagina's zijn publiek leesbaar (rules). De Firestore REST-API honoreert die\n * rules met enkel de web-apiKey (geen Admin-credentials, geen client-SDK), dus dit werkt\n * server-side op keyless deployments (org-policy zonder service-account-keys).\n */\ntype FsValue = Record<string, unknown>;\nfunction decode(v: FsValue): unknown {\n if (v == null) return null;\n if (\"stringValue\" in v) return v.stringValue;\n if (\"integerValue\" in v) return Number(v.integerValue);\n if (\"doubleValue\" in v) return v.doubleValue;\n if (\"booleanValue\" in v) return v.booleanValue;\n if (\"nullValue\" in v) return null;\n if (\"timestampValue\" in v) return v.timestampValue;\n if (\"referenceValue\" in v) return v.referenceValue;\n if (\"geoPointValue\" in v) return v.geoPointValue;\n if (\"arrayValue\" in v) return (((v.arrayValue as FsValue)?.values as FsValue[]) ?? []).map(decode);\n if (\"mapValue\" in v) return decodeFields(((v.mapValue as FsValue)?.fields as Record<string, FsValue>) ?? {});\n return null;\n}\nfunction decodeFields(fields: Record<string, FsValue>): Record<string, unknown> {\n return Object.fromEntries(Object.entries(fields).map(([k, x]) => [k, decode(x)]));\n}\n\ninterface Filter { fieldFilter: { field: { fieldPath: string }; op: \"EQUAL\"; value: { stringValue: string } } }\nfunction eq(fieldPath: string, value: string): Filter {\n return { fieldFilter: { field: { fieldPath }, op: \"EQUAL\", value: { stringValue: value } } };\n}\n\nasync function restQuery(siteId: string, filters: Filter[], limit?: number): Promise<StoredPage[]> {\n const apiKey = process.env.NEXT_PUBLIC_FIREBASE_API_KEY;\n const projectId = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID;\n if (!apiKey || !projectId) return [];\n const url = `https://firestore.googleapis.com/v1/projects/${projectId}/databases/(default)/documents/sites/${siteId}:runQuery?key=${apiKey}`;\n const where = filters.length === 1 ? filters[0] : { compositeFilter: { op: \"AND\", filters } };\n const structuredQuery: Record<string, unknown> = { from: [{ collectionId: \"pages\" }], where };\n if (limit) structuredQuery.limit = limit;\n // Netwerk-/parsefouten mogen een publieke pagina nooit laten crashen (500) — dan liever een\n // lege lijst (Next toont dan een 404 of de fallback, en de volgende revalidate probeert opnieuw).\n try {\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ structuredQuery }),\n // Pagina's zijn ISR-gecached; next-cache mag dit hergebruiken.\n next: { revalidate: 300 },\n });\n if (!res.ok) return [];\n const rows = (await res.json()) as { document?: { name: string; fields: Record<string, FsValue> } }[];\n return rows\n .filter((r) => r.document)\n .map((r) => parse(r.document!.name.split(\"/\").pop()!, decodeFields(r.document!.fields)))\n .filter((p): p is StoredPage => p !== null);\n } catch (err) {\n console.error(\"[xicms] Firestore REST-query mislukt:\", err);\n return [];\n }\n}\n\n/** Alle gepubliceerde pagina's (sitemap / generateStaticParams). Keyless via REST als geen Admin. */\nexport async function listPublishedPages(siteId: string): Promise<StoredPage[]> {\n const adb = getAdminDb();\n if (adb) {\n return safeAdminRead([], `listPublishedPages ${siteId}`, async () => {\n const snap = await adb.collection(`sites/${siteId}/pages`).where(\"status\", \"==\", \"published\").get();\n return snap.docs.map((d) => parse(d.id, d.data())).filter((p): p is StoredPage => p !== null);\n });\n }\n return restQuery(siteId, [eq(\"status\", \"published\")]);\n}\n\n/** Eén gepubliceerde pagina op pad. Keyless via REST als geen Admin. */\nexport async function getPublishedPageByPath(siteId: string, path: string): Promise<StoredPage | null> {\n const adb = getAdminDb();\n if (adb) {\n return safeAdminRead(null, `getPublishedPageByPath ${siteId}${path}`, async () => {\n const snap = await adb\n .collection(`sites/${siteId}/pages`)\n .where(\"status\", \"==\", \"published\")\n .where(\"path\", \"==\", path)\n .limit(1)\n .get();\n const d = snap.docs[0];\n return d ? parse(d.id, d.data()) : null;\n });\n }\n const rows = await restQuery(siteId, [eq(\"status\", \"published\"), eq(\"path\", path)], 1);\n return rows[0] ?? null;\n}\n\n/** Draft-versie (voor preview achter auth). */\nexport async function getDraftPageById(siteId: string, pageId: string): Promise<StoredPage | null> {\n const db = getAdminDb();\n if (!db) return null;\n return safeAdminRead(null, `getDraftPageById ${siteId}/${pageId}`, async () => {\n const d = await db.doc(`sites/${siteId}/drafts/${pageId}`).get();\n return d.exists ? parse(d.id, d.data()) : null;\n });\n}\n\n/** Draft op pad (preview). */\nexport async function getDraftPageByPath(siteId: string, path: string): Promise<StoredPage | null> {\n const db = getAdminDb();\n if (!db) return null;\n return safeAdminRead(null, `getDraftPageByPath ${siteId}${path}`, async () => {\n const snap = await db.collection(`sites/${siteId}/drafts`).where(\"path\", \"==\", path).limit(1).get();\n const d = snap.docs[0];\n return d ? parse(d.id, d.data()) : null;\n });\n}\n\n/* ---------------- symbols & custom blocks (5.0) ---------------- */\n\n/** Generieke publieke collectie-read onder sites/{siteId}/ — Admin als beschikbaar, anders REST. */\nasync function readCollection(siteId: string, collectionId: string): Promise<{ id: string; data: Record<string, unknown> }[]> {\n const adb = getAdminDb();\n if (adb) {\n return safeAdminRead([], `readCollection ${siteId}/${collectionId}`, async () => {\n const snap = await adb.collection(`sites/${siteId}/${collectionId}`).get();\n return snap.docs.map((d) => ({ id: d.id, data: d.data() as Record<string, unknown> }));\n });\n }\n const apiKey = process.env.NEXT_PUBLIC_FIREBASE_API_KEY;\n const projectId = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID;\n if (!apiKey || !projectId) return [];\n const url = `https://firestore.googleapis.com/v1/projects/${projectId}/databases/(default)/documents/sites/${siteId}:runQuery?key=${apiKey}`;\n try {\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ structuredQuery: { from: [{ collectionId }] } }),\n next: { revalidate: 300 },\n });\n if (!res.ok) return [];\n const rows = (await res.json()) as { document?: { name: string; fields: Record<string, FsValue> } }[];\n return rows\n .filter((r) => r.document)\n .map((r) => ({ id: r.document!.name.split(\"/\").pop()!, data: decodeFields(r.document!.fields ?? {}) }));\n } catch (err) {\n console.error(`[xicms] Firestore REST-read van ${collectionId} mislukt:`, err);\n return [];\n }\n}\n\n/** Alle symbolen van een site (gevalideerd; ongeldige overgeslagen met waarschuwing). */\nexport async function getSiteSymbols(siteId: string): Promise<StoredSymbol[]> {\n const rows = await readCollection(siteId, \"symbols\");\n const out: StoredSymbol[] = [];\n for (const r of rows) {\n const res = SymbolSchema.safeParse(r.data);\n if (res.success) out.push({ id: r.id, ...res.data });\n else console.warn(`[xicms] symbool \"${r.id}\" ongeldig — overgeslagen.`);\n }\n return out;\n}\n\n/**\n * Vervangt symbolRef-blokken door de blokken van hun symbool (één niveau — symbolen kunnen zelf\n * geen symbolRef bevatten). `_key`s van de ingevoegde blokken worden geprefixt met de ref-key\n * zodat ze uniek blijven wanneer hetzelfde symbool vaker op een pagina staat. Verweesde refs\n * verdwijnen stil (de pagina mag nooit breken op een verwijderd symbool).\n */\nexport function expandSymbols<T extends { blocks?: unknown[] }>(page: T, symbols: StoredSymbol[]): T {\n const byId = new Map(symbols.map((s) => [s.id, s]));\n const blocks = (page.blocks ?? []).flatMap((b) => {\n const blk = b as { _type?: string; _key?: string; symbolId?: string };\n if (blk?._type !== \"symbolRef\") return [b];\n const sym = blk.symbolId ? byId.get(blk.symbolId) : undefined;\n if (!sym) return [];\n return sym.blocks.map((sb, i) => ({ ...(sb as Record<string, unknown>), _key: `${blk._key ?? \"sym\"}-${(sb as { _key?: string })._key ?? i}` }));\n });\n return { ...page, blocks };\n}\n\n/**\n * Gepubliceerde pagina op pad mét symbolRef-expansie (SEO: symboolinhoud zit in de SSR-HTML).\n * Geef `locale` mee (een NIET-default taal) om de vertaal-overlay toe te passen — op de pagina\n * én op de ingevoegde symbolen (elk symbool draagt zijn eigen `translations`).\n */\nexport async function getPublishedPageByPathExpanded(\n siteId: string,\n path: string,\n locale?: string,\n): Promise<StoredPage | null> {\n let page = await getPublishedPageByPath(siteId, path);\n if (!page) return null;\n // De caller geeft alléén een niet-default taal door; \"\" als default matcht dus nooit.\n if (locale) page = localizePage(page, locale, \"\");\n if (!(page.blocks ?? []).some((b) => (b as { _type?: string })._type === \"symbolRef\")) return page;\n let symbols = await getSiteSymbols(siteId);\n if (locale) {\n symbols = symbols.map((s) => ({\n ...s,\n blocks: localizeBlocks(s.blocks as Array<{ _key?: string }>, s.translations?.[locale]) as typeof s.blocks,\n }));\n }\n return expandSymbols(page, symbols);\n}\n\n/** Alle custom blocks van een site (gevalideerd) — voor SSR-registratie via XiCMSProvider. */\nexport async function getSiteCustomBlocks(siteId: string): Promise<CustomBlock[]> {\n const rows = await readCollection(siteId, \"customBlocks\");\n const out: CustomBlock[] = [];\n for (const r of rows) {\n const res = CustomBlockSchema.safeParse({ ...r.data, type: r.data.type ?? r.id });\n if (res.success) out.push(res.data);\n else console.warn(`[xicms] custom block \"${r.id}\" ongeldig — overgeslagen.`);\n }\n return out;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/server/pages.ts"],"sourcesContent":["import \"server-only\";\n\nimport { cache } from \"react\";\n\nimport { getAdminDb } from \"../firebase/admin\";\nimport { PageSchema, type Page } from \"../schema/pages\";\nimport { SymbolSchema, type StoredSymbol } from \"../schema/symbols\";\nimport { CustomBlockSchema, type CustomBlock } from \"../schema/customBlocks\";\nimport { PublicSiteSettingsSchema, type PublicSiteSettings } from \"../schema/settings\";\nimport { localizePage, localizeBlocks } from \"../i18n/overlay\";\n\nexport type StoredPage = Page & { id: string };\n\nfunction toPath(slugs: string[] | undefined): string {\n const clean = (slugs ?? []).filter(Boolean);\n return \"/\" + clean.join(\"/\");\n}\n\n/** Pad uit Next catch-all params (`[[...slug]]`). \"/\" voor de root. */\nexport function pathFromSlugs(slugs: string[] | undefined): string {\n const p = toPath(slugs);\n return p === \"/\" ? \"/\" : p.replace(/\\/$/, \"\");\n}\n\nfunction parse(id: string, data: unknown): StoredPage | null {\n const res = PageSchema.safeParse(data);\n if (!res.success) {\n // Niet stil 404'en: een gepubliceerde pagina die het schema niet haalt is een diagnose-signaal\n // (schema-evolutie zonder default, of corrupte write) — zelfde patroon als symbols/customBlocks.\n console.warn(`[xicms] pagina \"${id}\" faalt schema-validatie en wordt overgeslagen:`, res.error.issues[0]);\n return null;\n }\n return { id, ...res.data };\n}\n\n/**\n * Admin-SDK-reads mogen een PUBLIEKE pagina nooit laten 500'en — zelfde belofte als het REST-pad\n * (dat al overal try/catch → lege fallback heeft). Een Firestore-hikje wordt gelogd en levert de\n * fallback op; de route rendert dan 404/fallback i.p.v. een servererror.\n */\nasync function safeAdminRead<T>(fallback: T, what: string, fn: () => Promise<T>): Promise<T> {\n try {\n return await fn();\n } catch (e) {\n console.warn(`[xicms] Admin-read mislukt (${what}) — fallback gebruikt:`, (e as Error)?.message ?? e);\n return fallback;\n }\n}\n\n/* ---------------- keyless public reads via Firestore REST (alleen apiKey) ----------------\n * Gepubliceerde pagina's zijn publiek leesbaar (rules). De Firestore REST-API honoreert die\n * rules met enkel de web-apiKey (geen Admin-credentials, geen client-SDK), dus dit werkt\n * server-side op keyless deployments (org-policy zonder service-account-keys).\n */\ntype FsValue = Record<string, unknown>;\nfunction decode(v: FsValue): unknown {\n if (v == null) return null;\n if (\"stringValue\" in v) return v.stringValue;\n if (\"integerValue\" in v) return Number(v.integerValue);\n if (\"doubleValue\" in v) return v.doubleValue;\n if (\"booleanValue\" in v) return v.booleanValue;\n if (\"nullValue\" in v) return null;\n if (\"timestampValue\" in v) return v.timestampValue;\n if (\"referenceValue\" in v) return v.referenceValue;\n if (\"geoPointValue\" in v) return v.geoPointValue;\n if (\"arrayValue\" in v) return (((v.arrayValue as FsValue)?.values as FsValue[]) ?? []).map(decode);\n if (\"mapValue\" in v) return decodeFields(((v.mapValue as FsValue)?.fields as Record<string, FsValue>) ?? {});\n return null;\n}\nfunction decodeFields(fields: Record<string, FsValue>): Record<string, unknown> {\n return Object.fromEntries(Object.entries(fields).map(([k, x]) => [k, decode(x)]));\n}\n\ninterface Filter { fieldFilter: { field: { fieldPath: string }; op: \"EQUAL\"; value: { stringValue: string } } }\nfunction eq(fieldPath: string, value: string): Filter {\n return { fieldFilter: { field: { fieldPath }, op: \"EQUAL\", value: { stringValue: value } } };\n}\n\nasync function restQuery(siteId: string, filters: Filter[], limit?: number): Promise<StoredPage[]> {\n const apiKey = process.env.NEXT_PUBLIC_FIREBASE_API_KEY;\n const projectId = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID;\n if (!apiKey || !projectId) return [];\n const url = `https://firestore.googleapis.com/v1/projects/${projectId}/databases/(default)/documents/sites/${siteId}:runQuery?key=${apiKey}`;\n const where = filters.length === 1 ? filters[0] : { compositeFilter: { op: \"AND\", filters } };\n const structuredQuery: Record<string, unknown> = { from: [{ collectionId: \"pages\" }], where };\n if (limit) structuredQuery.limit = limit;\n // Netwerk-/parsefouten mogen een publieke pagina nooit laten crashen (500) — dan liever een\n // lege lijst (Next toont dan een 404 of de fallback, en de volgende revalidate probeert opnieuw).\n try {\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ structuredQuery }),\n // Pagina's zijn ISR-gecached; next-cache mag dit hergebruiken.\n next: { revalidate: 300 },\n });\n if (!res.ok) return [];\n const rows = (await res.json()) as { document?: { name: string; fields: Record<string, FsValue> } }[];\n return rows\n .filter((r) => r.document)\n .map((r) => parse(r.document!.name.split(\"/\").pop()!, decodeFields(r.document!.fields)))\n .filter((p): p is StoredPage => p !== null);\n } catch (err) {\n console.error(\"[xicms] Firestore REST-query mislukt:\", err);\n return [];\n }\n}\n\n/** Alle gepubliceerde pagina's (sitemap / generateStaticParams). Keyless via REST als geen Admin. */\nexport async function listPublishedPages(siteId: string): Promise<StoredPage[]> {\n const adb = getAdminDb();\n if (adb) {\n return safeAdminRead([], `listPublishedPages ${siteId}`, async () => {\n const snap = await adb.collection(`sites/${siteId}/pages`).where(\"status\", \"==\", \"published\").get();\n return snap.docs.map((d) => parse(d.id, d.data())).filter((p): p is StoredPage => p !== null);\n });\n }\n return restQuery(siteId, [eq(\"status\", \"published\")]);\n}\n\n/**\n * Per-request-dedup via React cache(): metadata (generateMetadata) én de render lezen dezelfde\n * pagina — zonder cache() waren dat twee identieke Firestore-reads per request. NB: de\n * `next:{revalidate}`-hint op het REST-pad dekt dit niet (POST-fetches cachen niet); route-level\n * ISR (revalidate=300 in de consumer) blijft de echte kosten-demper tussen requests.\n */\nexport const getPublishedPageByPath = cache(getPublishedPageByPathUncached);\nexport const getSiteSymbols = cache(getSiteSymbolsUncached);\nexport const getSiteCustomBlocks = cache(getSiteCustomBlocksUncached);\nexport const getPublicSiteSettings = cache(getPublicSiteSettingsUncached);\n\n/** Eén gepubliceerde pagina op pad. Keyless via REST als geen Admin. */\nasync function getPublishedPageByPathUncached(siteId: string, path: string): Promise<StoredPage | null> {\n const adb = getAdminDb();\n if (adb) {\n return safeAdminRead(null, `getPublishedPageByPath ${siteId}${path}`, async () => {\n const snap = await adb\n .collection(`sites/${siteId}/pages`)\n .where(\"status\", \"==\", \"published\")\n .where(\"path\", \"==\", path)\n .limit(1)\n .get();\n const d = snap.docs[0];\n return d ? parse(d.id, d.data()) : null;\n });\n }\n const rows = await restQuery(siteId, [eq(\"status\", \"published\"), eq(\"path\", path)], 1);\n return rows[0] ?? null;\n}\n\n/** Draft-versie (voor preview achter auth). */\nexport async function getDraftPageById(siteId: string, pageId: string): Promise<StoredPage | null> {\n const db = getAdminDb();\n if (!db) return null;\n return safeAdminRead(null, `getDraftPageById ${siteId}/${pageId}`, async () => {\n const d = await db.doc(`sites/${siteId}/drafts/${pageId}`).get();\n return d.exists ? parse(d.id, d.data()) : null;\n });\n}\n\n/** Draft op pad (preview). */\nexport async function getDraftPageByPath(siteId: string, path: string): Promise<StoredPage | null> {\n const db = getAdminDb();\n if (!db) return null;\n return safeAdminRead(null, `getDraftPageByPath ${siteId}${path}`, async () => {\n const snap = await db.collection(`sites/${siteId}/drafts`).where(\"path\", \"==\", path).limit(1).get();\n const d = snap.docs[0];\n return d ? parse(d.id, d.data()) : null;\n });\n}\n\n/* ---------------- symbols & custom blocks (5.0) ---------------- */\n\n/** Generieke publieke collectie-read onder sites/{siteId}/ — Admin als beschikbaar, anders REST. */\nasync function readCollection(siteId: string, collectionId: string): Promise<{ id: string; data: Record<string, unknown> }[]> {\n const adb = getAdminDb();\n if (adb) {\n return safeAdminRead([], `readCollection ${siteId}/${collectionId}`, async () => {\n const snap = await adb.collection(`sites/${siteId}/${collectionId}`).get();\n return snap.docs.map((d) => ({ id: d.id, data: d.data() as Record<string, unknown> }));\n });\n }\n const apiKey = process.env.NEXT_PUBLIC_FIREBASE_API_KEY;\n const projectId = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID;\n if (!apiKey || !projectId) return [];\n const url = `https://firestore.googleapis.com/v1/projects/${projectId}/databases/(default)/documents/sites/${siteId}:runQuery?key=${apiKey}`;\n try {\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ structuredQuery: { from: [{ collectionId }] } }),\n next: { revalidate: 300 },\n });\n if (!res.ok) return [];\n const rows = (await res.json()) as { document?: { name: string; fields: Record<string, FsValue> } }[];\n return rows\n .filter((r) => r.document)\n .map((r) => ({ id: r.document!.name.split(\"/\").pop()!, data: decodeFields(r.document!.fields ?? {}) }));\n } catch (err) {\n console.error(`[xicms] Firestore REST-read van ${collectionId} mislukt:`, err);\n return [];\n }\n}\n\n/** Alle symbolen van een site (gevalideerd; ongeldige overgeslagen met waarschuwing). */\nasync function getSiteSymbolsUncached(siteId: string): Promise<StoredSymbol[]> {\n const rows = await readCollection(siteId, \"symbols\");\n const out: StoredSymbol[] = [];\n for (const r of rows) {\n const res = SymbolSchema.safeParse(r.data);\n if (res.success) out.push({ id: r.id, ...res.data });\n else console.warn(`[xicms] symbool \"${r.id}\" ongeldig — overgeslagen.`);\n }\n return out;\n}\n\n/**\n * Vervangt symbolRef-blokken door de blokken van hun symbool (één niveau — symbolen kunnen zelf\n * geen symbolRef bevatten). `_key`s van de ingevoegde blokken worden geprefixt met de ref-key\n * zodat ze uniek blijven wanneer hetzelfde symbool vaker op een pagina staat. Verweesde refs\n * verdwijnen stil (de pagina mag nooit breken op een verwijderd symbool).\n */\nexport function expandSymbols<T extends { blocks?: unknown[] }>(page: T, symbols: StoredSymbol[]): T {\n const byId = new Map(symbols.map((s) => [s.id, s]));\n const blocks = (page.blocks ?? []).flatMap((b) => {\n const blk = b as { _type?: string; _key?: string; symbolId?: string };\n if (blk?._type !== \"symbolRef\") return [b];\n const sym = blk.symbolId ? byId.get(blk.symbolId) : undefined;\n if (!sym) return [];\n return sym.blocks.map((sb, i) => ({ ...(sb as Record<string, unknown>), _key: `${blk._key ?? \"sym\"}-${(sb as { _key?: string })._key ?? i}` }));\n });\n return { ...page, blocks };\n}\n\n/**\n * Gepubliceerde pagina op pad mét symbolRef-expansie (SEO: symboolinhoud zit in de SSR-HTML).\n * Geef `locale` mee (een NIET-default taal) om de vertaal-overlay toe te passen — op de pagina\n * én op de ingevoegde symbolen (elk symbool draagt zijn eigen `translations`).\n */\nexport async function getPublishedPageByPathExpanded(\n siteId: string,\n path: string,\n locale?: string,\n): Promise<StoredPage | null> {\n let page = await getPublishedPageByPath(siteId, path);\n if (!page) return null;\n // De caller geeft alléén een niet-default taal door; \"\" als default matcht dus nooit.\n if (locale) page = localizePage(page, locale, \"\");\n if (!(page.blocks ?? []).some((b) => (b as { _type?: string })._type === \"symbolRef\")) return page;\n let symbols = await getSiteSymbols(siteId);\n if (locale) {\n symbols = symbols.map((s) => ({\n ...s,\n blocks: localizeBlocks(s.blocks as Array<{ _key?: string }>, s.translations?.[locale]) as typeof s.blocks,\n }));\n }\n return expandSymbols(page, symbols);\n}\n\n/**\n * Publieke site-instellingen (`sites/{siteId}/settings/public`) server-side — voor de\n * `settingsData`-prop van XiCMSProvider. Zo zit het klant-thema/nav/footer al in de server-HTML\n * (geen theme-flits bij eerste paint) én vervalt de per-bezoeker `getDoc` in de browser: de\n * layout-route is ISR-gecached, dus dit kost ~1 read per revalidate i.p.v. per bezoeker.\n * Fouten leveren `{}` op — de site valt dan terug op de statische SiteConfig.\n */\nasync function getPublicSiteSettingsUncached(siteId: string): Promise<PublicSiteSettings> {\n const adb = getAdminDb();\n let data: unknown = {};\n if (adb) {\n data = await safeAdminRead<unknown>({}, `getPublicSiteSettings ${siteId}`, async () => {\n const d = await adb.doc(`sites/${siteId}/settings/public`).get();\n return d.exists ? d.data() : {};\n });\n } else {\n const apiKey = process.env.NEXT_PUBLIC_FIREBASE_API_KEY;\n const projectId = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID;\n if (apiKey && projectId) {\n const url = `https://firestore.googleapis.com/v1/projects/${projectId}/databases/(default)/documents/sites/${siteId}/settings/public?key=${apiKey}`;\n try {\n // GET-fetches worden — anders dan de runQuery-POSTs — wél door de Next-datacache gedekt.\n const res = await fetch(url, { next: { revalidate: 300 } });\n if (res.ok) {\n const doc = (await res.json()) as { fields?: Record<string, FsValue> };\n data = decodeFields(doc.fields ?? {});\n }\n } catch (err) {\n console.error(\"[xicms] Firestore REST-read van settings/public mislukt:\", err);\n }\n }\n }\n const parsed = PublicSiteSettingsSchema.safeParse(data ?? {});\n return parsed.success ? parsed.data : {};\n}\n\n/** Alle custom blocks van een site (gevalideerd) — voor SSR-registratie via XiCMSProvider. */\nasync function getSiteCustomBlocksUncached(siteId: string): Promise<CustomBlock[]> {\n const rows = await readCollection(siteId, \"customBlocks\");\n const out: CustomBlock[] = [];\n for (const r of rows) {\n const res = CustomBlockSchema.safeParse({ ...r.data, type: r.data.type ?? r.id });\n if (res.success) out.push(res.data);\n else console.warn(`[xicms] custom block \"${r.id}\" ongeldig — overgeslagen.`);\n }\n return out;\n}\n"],"mappings":"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","names":[]}
@@ -35,7 +35,7 @@ function buildMetadata(page, site, i18n) {
35
35
  const description = seo.description ?? d.description;
36
36
  const origin = site.domain ? `https://${site.domain.replace(/^https?:\/\//, "").replace(/\/$/, "")}` : "";
37
37
  const rawPath = seo.canonical ?? page.path;
38
- const path = i18n && !/^https?:\/\//.test(rawPath) ? localizedPath(rawPath, i18n.locale, i18n.defaultLocale) : rawPath;
38
+ const path = i18n && !seo.canonical && !/^https?:\/\//.test(rawPath) ? localizedPath(rawPath, i18n.locale, i18n.defaultLocale) : rawPath;
39
39
  const canonical = /^https?:\/\//.test(path) ? path : `${origin}${path.startsWith("/") ? "" : "/"}${path}`;
40
40
  const languages = i18n && i18n.locales.length > 1 ? Object.fromEntries([
41
41
  ...i18n.locales.map((l) => [l, `${origin}${localizedPath(page.path, l, i18n.defaultLocale)}`]),
@@ -80,7 +80,7 @@ function businessJsonLd(info) {
80
80
  if (info.email) out.email = info.email;
81
81
  if (info.image) out.image = info.image;
82
82
  if (info.priceRange) out.priceRange = info.priceRange;
83
- if (info.address) {
83
+ if (info.address && (info.address.street || info.address.locality || info.address.postalCode)) {
84
84
  out.address = {
85
85
  "@type": "PostalAddress",
86
86
  streetAddress: info.address.street,
@@ -170,7 +170,9 @@ function buildJsonLd(page, site) {
170
170
  "@type": type,
171
171
  name: page.seo?.title ?? page.title,
172
172
  description: page.seo?.description ?? site.seoDefaults?.description,
173
- url: base + page.path,
173
+ // Alleen een url-node met een echte origin: een relatieve url ("/over-ons") is ongeldige
174
+ // structured data wanneer het domein (nog) niet geconfigureerd is.
175
+ ...base ? { url: base + page.path } : {},
174
176
  publisher: { "@type": "Organization", name: site.name }
175
177
  };
176
178
  }