@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.
- package/dist/XiCMSProvider.d.ts +11 -2
- package/dist/XiCMSProvider.js +10 -5
- package/dist/XiCMSProvider.js.map +1 -1
- package/dist/admin/blockEditors/GenericEditor.js +18 -8
- package/dist/admin/blockEditors/GenericEditor.js.map +1 -1
- package/dist/admin/customBlocks/CustomBlocksPanel.js +12 -2
- package/dist/admin/customBlocks/CustomBlocksPanel.js.map +1 -1
- package/dist/admin/lib/pagesClient.js +4 -1
- package/dist/admin/lib/pagesClient.js.map +1 -1
- package/dist/admin/lib/upload.js +14 -7
- package/dist/admin/lib/upload.js.map +1 -1
- package/dist/admin/media/MediaManager.js +60 -28
- package/dist/admin/media/MediaManager.js.map +1 -1
- package/dist/admin/media/MediaPicker.js +25 -15
- package/dist/admin/media/MediaPicker.js.map +1 -1
- package/dist/admin/pages/PageEditor.d.ts +2 -1
- package/dist/admin/pages/PageEditor.js +16 -2
- package/dist/admin/pages/PageEditor.js.map +1 -1
- package/dist/admin/pages/PagesPanel.js +9 -5
- package/dist/admin/pages/PagesPanel.js.map +1 -1
- package/dist/admin/visual/VisualEditor.js +11 -2
- package/dist/admin/visual/VisualEditor.js.map +1 -1
- package/dist/agentFiles/files.d.ts +15 -3
- package/dist/agentFiles/files.js +57 -0
- package/dist/agentFiles/files.js.map +1 -1
- package/dist/auth/AuthProvider.js +3 -3
- package/dist/auth/AuthProvider.js.map +1 -1
- package/dist/blocks/builtins/Columns.js +1 -1
- package/dist/blocks/builtins/Columns.js.map +1 -1
- package/dist/blocks/builtins/Gallery.js +2 -2
- package/dist/blocks/builtins/Gallery.js.map +1 -1
- package/dist/blocks/builtins/Hero.js +1 -1
- package/dist/blocks/builtins/Hero.js.map +1 -1
- package/dist/blocks/builtins/SymbolRef.js.map +1 -1
- package/dist/components/PageView.js +4 -3
- package/dist/components/PageView.js.map +1 -1
- package/dist/demo/index.d.ts +7 -0
- package/dist/demo/index.js +23 -0
- package/dist/demo/index.js.map +1 -0
- package/dist/index.d.ts +2 -4
- package/dist/index.js +0 -20
- package/dist/index.js.map +1 -1
- package/dist/modules/registry.d.ts +3 -4
- package/dist/modules/registry.js +2 -3
- package/dist/modules/registry.js.map +1 -1
- package/dist/schema/blocks.d.ts +27 -12
- package/dist/schema/blocks.js +3 -0
- package/dist/schema/blocks.js.map +1 -1
- package/dist/schema/settings.d.ts +1 -34
- package/dist/schema/settings.js +2 -8
- package/dist/schema/settings.js.map +1 -1
- package/dist/schema/siteConfig.d.ts +3 -71
- package/dist/schema/siteConfig.js +3 -8
- package/dist/schema/siteConfig.js.map +1 -1
- package/dist/schema/symbols.d.ts +2 -2
- package/dist/schema/symbols.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/pages.d.ts +23 -3
- package/dist/server/pages.js +37 -3
- package/dist/server/pages.js.map +1 -1
- package/dist/server/seo.js +5 -3
- package/dist/server/seo.js.map +1 -1
- package/dist/settings/useSiteSettings.d.ts +15 -3
- package/dist/settings/useSiteSettings.js +26 -5
- package/dist/settings/useSiteSettings.js.map +1 -1
- package/dist/styles.css +14 -0
- 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":"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","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
|
-
|
|
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 *
|
|
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":[]}
|
package/dist/schema/symbols.d.ts
CHANGED
|
@@ -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
|
|
203
|
-
export type StoredSymbol =
|
|
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
|
|
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":[]}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/server/index.js
CHANGED
|
@@ -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,
|
package/dist/server/index.js.map
CHANGED
|
@@ -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":[]}
|
package/dist/server/pages.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
+
declare function getSiteCustomBlocksUncached(siteId: string): Promise<CustomBlock[]>;
|
|
56
|
+
export {};
|
package/dist/server/pages.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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,
|
package/dist/server/pages.js.map
CHANGED
|
@@ -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":[]}
|
package/dist/server/seo.js
CHANGED
|
@@ -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:
|
|
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
|
}
|