@xiwebdesign/xicms-core 5.2.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/blockEditors/index.js +2 -2
- package/dist/admin/blockEditors/index.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/Booking.js +1 -1
- package/dist/blocks/builtins/Booking.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/Embed.js +2 -0
- package/dist/blocks/builtins/Embed.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 +46 -7
- package/dist/blocks/builtins/Hero.js.map +1 -1
- package/dist/blocks/builtins/ImageBlock.js +14 -3
- package/dist/blocks/builtins/ImageBlock.js.map +1 -1
- package/dist/blocks/builtins/Jobs.d.ts +2 -2
- package/dist/blocks/builtins/Jobs.js +1 -1
- package/dist/blocks/builtins/Jobs.js.map +1 -1
- package/dist/blocks/builtins/Newsletter.js +18 -5
- package/dist/blocks/builtins/Newsletter.js.map +1 -1
- package/dist/blocks/builtins/Pricing.js +3 -1
- package/dist/blocks/builtins/Pricing.js.map +1 -1
- package/dist/blocks/builtins/Sections.js +4 -4
- package/dist/blocks/builtins/Sections.js.map +1 -1
- package/dist/blocks/builtins/Slider.js +9 -4
- package/dist/blocks/builtins/Slider.js.map +1 -1
- package/dist/blocks/builtins/SymbolRef.js.map +1 -1
- package/dist/blocks/builtins/Video.js +1 -1
- package/dist/blocks/builtins/Video.js.map +1 -1
- package/dist/blocks/builtins/Widgets.js +2 -1
- package/dist/blocks/builtins/Widgets.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 -90
- package/dist/schema/blocks.js +4 -6
- 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 +64 -9
- package/package.json +6 -2
|
@@ -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
|
}
|
package/dist/server/seo.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/server/seo.ts"],"sourcesContent":["import \"server-only\";\n\nimport type { Metadata, MetadataRoute } from \"next\";\nimport type { Seo } from \"../schema/seo\";\nimport type { Page } from \"../schema/pages\";\nimport { localizedPath, type LocaleConfig } from \"../i18n/overlay\";\n\n/** Absolute origin (https://domein) uit de site-config; \"\" als er geen domein is. */\nexport function siteOrigin(site: { domain?: string }): string {\n return site.domain ? `https://${site.domain.replace(/^https?:\\/\\//, \"\").replace(/\\/$/, \"\")}` : \"\";\n}\n\n/**\n * Bouwt een Next `sitemap.xml` uit een lijst pagina's. Gebruik in `app/sitemap.ts`:\n * export default function sitemap() { return buildSitemap(pages, siteConfig); }\n */\nexport function buildSitemap(\n pages: Array<{ path: string; updatedAt?: string | number | Date; seo?: { noindex?: boolean } }>,\n site: { domain?: string },\n i18n?: LocaleConfig,\n): MetadataRoute.Sitemap {\n const origin = siteOrigin(site);\n // Meertalige site (5.0): elke pagina één keer per taal (default zonder prefix, rest mét).\n const locales = i18n && i18n.locales.length > 1 ? i18n.locales : [undefined];\n return pages\n .filter((p) => !p.seo?.noindex)\n .flatMap((p) =>\n locales.map((locale) => {\n const path = locale && i18n ? localizedPath(p.path, locale, i18n.defaultLocale) : p.path;\n return {\n url: `${origin}${path === \"/\" ? \"/\" : path}`,\n lastModified: p.updatedAt ? new Date(p.updatedAt) : undefined,\n changeFrequency: p.path === \"/\" ? (\"weekly\" as const) : (\"monthly\" as const),\n priority: p.path === \"/\" ? 1 : 0.7,\n };\n }),\n );\n}\n\n/**\n * Bouwt een Next `robots.txt`. In `mode:\"demo\"` wordt de hele site op noindex gezet; anders alles\n * toegestaan behalve de beheerpaden, met verwijzing naar de sitemap. Gebruik in `app/robots.ts`.\n */\nexport function buildRobots(\n site: { domain?: string; mode?: string },\n opts?: { disallow?: string[] },\n): MetadataRoute.Robots {\n const origin = siteOrigin(site);\n if (site.mode === \"demo\") {\n // Let op: disallow blokkeert alleen het CRAWLEN — de URL kan alsnog (kaal) geïndexeerd\n // raken. buildMetadata zet in demo-modus daarom óók robots.index=false op elke pagina.\n return { rules: { userAgent: \"*\", disallow: \"/\" } };\n }\n return {\n rules: { userAgent: \"*\", allow: \"/\", disallow: opts?.disallow ?? [\"/beheer\", \"/login\"] },\n ...(origin ? { sitemap: `${origin}/sitemap.xml`, host: origin } : {}),\n };\n}\n\ninterface SeoSiteContext {\n name: string;\n domain?: string;\n /** \"demo\" zet elke pagina op noindex (robots.txt-disallow alleen is geen noindex). */\n mode?: string;\n seoDefaults?: Seo;\n}\n\n/**\n * Bouwt Next `Metadata` voor een pagina, met fallback op site-defaults. Geef voor een meertalige\n * site (5.0) `i18n` mee: de canonical wijst dan naar de taalvariant en er komen\n * hreflang-alternates voor élke taal (default zonder prefix, incl. x-default).\n */\nexport function buildMetadata(\n page: Pick<Page, \"title\" | \"path\" | \"seo\">,\n site: SeoSiteContext,\n i18n?: LocaleConfig & { locale: string },\n): Metadata {\n const seo = page.seo ?? {};\n const d = site.seoDefaults ?? {};\n const title = seo.title ?? page.title ?? site.name;\n const description = seo.description ?? d.description;\n // Absolute canonical/OG-URL (Google prefereert absoluut) wanneer het domein bekend is.\n const origin = site.domain ? `https://${site.domain.replace(/^https?:\\/\\//, \"\").replace(/\\/$/, \"\")}` : \"\";\n const rawPath = seo.canonical ?? page.path;\n const path = i18n && !/^https?:\\/\\//.test(rawPath) ? localizedPath(rawPath, i18n.locale, i18n.defaultLocale) : rawPath;\n const canonical = /^https?:\\/\\//.test(path) ? path : `${origin}${path.startsWith(\"/\") ? \"\" : \"/\"}${path}`;\n const languages =\n i18n && i18n.locales.length > 1\n ? Object.fromEntries([\n ...i18n.locales.map((l) => [l, `${origin}${localizedPath(page.path, l, i18n.defaultLocale)}`]),\n [\"x-default\", `${origin}${page.path}`],\n ])\n : undefined;\n const ogImage = seo.ogImage ?? d.ogImage;\n // OG-afbeeldingen met afmetingen (1200×630) → betere previews op social.\n const images = ogImage ? [{ url: ogImage, width: 1200, height: 630 }] : undefined;\n\n return {\n title,\n description,\n metadataBase: origin ? new URL(origin) : undefined,\n alternates: { canonical, ...(languages ? { languages } : {}) },\n robots: seo.noindex || site.mode === \"demo\" ? { index: false, follow: false } : undefined,\n openGraph: {\n title,\n description,\n url: canonical,\n siteName: site.name,\n type: \"website\",\n ...(images ? { images } : {}),\n },\n twitter: {\n card: images ? \"summary_large_image\" : \"summary\",\n title,\n description,\n ...(images ? { images: [ogImage as string] } : {}),\n },\n };\n}\n\n/** Bedrijfsgegevens voor gestructureerde data (schema.org LocalBusiness e.d.). */\nexport interface BusinessInfo {\n /** schema.org-type, bv. \"LocalBusiness\", \"LodgingBusiness\", \"Restaurant\", \"Store\". */\n type?: string;\n name: string;\n description?: string;\n url?: string;\n telephone?: string;\n email?: string;\n image?: string;\n /** Indicatie prijsniveau, bv. \"€€\". */\n priceRange?: string;\n address?: {\n street?: string;\n locality?: string;\n region?: string;\n postalCode?: string;\n country?: string;\n };\n geo?: { lat: number; lng: number };\n /** schema.org openingHours, bv. [\"Mo-Fr 09:00-17:00\", \"Sa 10:00-16:00\"]. */\n openingHours?: string[];\n /** Externe profielen (social, etc.). */\n sameAs?: string[];\n}\n\n/**\n * Bouwt gestructureerde data voor een lokaal bedrijf (herbruikbaar per branche). Plaats het\n * resultaat als JSON-LD in de layout: <script type=\"application/ld+json\">JSON.stringify(...)</script>\n */\nexport function businessJsonLd(info: BusinessInfo): Record<string, unknown> {\n const out: Record<string, unknown> = {\n \"@context\": \"https://schema.org\",\n \"@type\": info.type ?? \"LocalBusiness\",\n // Zelfde relatieve @id als de contact-/openingstijden-blokken: zo voegen zoekmachines de\n // layout-brede en blok-specifieke LocalBusiness-JSON-LD samen tot één entiteit per pagina.\n \"@id\": \"#business\",\n name: info.name,\n };\n if (info.description) out.description = info.description;\n if (info.url) out.url = info.url;\n if (info.telephone) out.telephone = info.telephone;\n if (info.email) out.email = info.email;\n if (info.image) out.image = info.image;\n if (info.priceRange) out.priceRange = info.priceRange;\n if (info.address) {\n out.address = {\n \"@type\": \"PostalAddress\",\n streetAddress: info.address.street,\n addressLocality: info.address.locality,\n addressRegion: info.address.region,\n postalCode: info.address.postalCode,\n addressCountry: info.address.country,\n };\n }\n if (info.geo) out.geo = { \"@type\": \"GeoCoordinates\", latitude: info.geo.lat, longitude: info.geo.lng };\n if (info.openingHours?.length) out.openingHours = info.openingHours;\n if (info.sameAs?.length) out.sameAs = info.sameAs;\n return out;\n}\n\n/** Breadcrumb-trail (schema.org BreadcrumbList) — voor genest navigeren + rich results. */\nexport function breadcrumbListJsonLd(items: { name: string; url: string }[]): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"BreadcrumbList\",\n itemListElement: items.map((it, i) => ({\n \"@type\": \"ListItem\",\n position: i + 1,\n name: it.name,\n item: it.url,\n })),\n };\n}\n\n/** Article-JSON-LD voor nieuws/blog. */\nexport function articleJsonLd(a: {\n title: string;\n description?: string;\n author?: string;\n datePublished?: string;\n image?: string;\n url?: string;\n}): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Article\",\n headline: a.title,\n ...(a.description ? { description: a.description } : {}),\n ...(a.author ? { author: { \"@type\": \"Person\", name: a.author } } : {}),\n ...(a.datePublished ? { datePublished: a.datePublished } : {}),\n ...(a.image ? { image: a.image } : {}),\n ...(a.url ? { url: a.url } : {}),\n };\n}\n\n/** Product/Offer-JSON-LD. */\nexport function productJsonLd(p: {\n name: string;\n description?: string;\n price?: string | number;\n currency?: string;\n image?: string;\n availability?: string;\n}): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Product\",\n name: p.name,\n ...(p.description ? { description: p.description } : {}),\n ...(p.image ? { image: p.image } : {}),\n ...(p.price != null\n ? {\n offers: {\n \"@type\": \"Offer\",\n price: String(p.price),\n priceCurrency: p.currency ?? \"EUR\",\n ...(p.availability ? { availability: `https://schema.org/${p.availability}` } : {}),\n },\n }\n : {}),\n };\n}\n\n/** OpeningHoursSpecification — bv. `[{ days:[\"Mo\",\"Tu\"], opens:\"09:00\", closes:\"17:00\" }]`. */\nexport function openingHoursJsonLd(\n spec: { days: string[]; opens: string; closes: string }[],\n): Record<string, unknown>[] {\n return spec.map((s) => ({\n \"@type\": \"OpeningHoursSpecification\",\n dayOfWeek: s.days,\n opens: s.opens,\n closes: s.closes,\n }));\n}\n\n/** ImageGallery-JSON-LD voor een fotogalerij. */\nexport function galleryJsonLd(items: { url: string; caption?: string }[]): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"ImageGallery\",\n image: items.map((it) => ({ \"@type\": \"ImageObject\", contentUrl: it.url, ...(it.caption ? { caption: it.caption } : {}) })),\n };\n}\n\n/** VideoObject-JSON-LD. */\nexport function videoObjectJsonLd(v: {\n name: string;\n description?: string;\n thumbnailUrl?: string;\n contentUrl?: string;\n uploadDate?: string;\n}): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"VideoObject\",\n name: v.name,\n ...(v.description ? { description: v.description } : {}),\n ...(v.thumbnailUrl ? { thumbnailUrl: v.thumbnailUrl } : {}),\n ...(v.contentUrl ? { contentUrl: v.contentUrl } : {}),\n ...(v.uploadDate ? { uploadDate: v.uploadDate } : {}),\n };\n}\n\n/** Bouwt een JSON-LD-object voor een pagina (schema.org). */\nexport function buildJsonLd(\n page: Pick<Page, \"title\" | \"path\" | \"seo\">,\n site: SeoSiteContext,\n): Record<string, unknown> {\n const type = page.seo?.jsonLdType ?? \"WebPage\";\n const base = site.domain ? `https://${site.domain.replace(/^https?:\\/\\//, \"\")}` : \"\";\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": type,\n name: page.seo?.title ?? page.title,\n description: page.seo?.description ?? site.seoDefaults?.description,\n url: base + page.path,\n publisher: { \"@type\": \"Organization\", name: site.name },\n };\n}\n"],"mappings":"AAAA,OAAO;AAKP,SAAS,qBAAwC;AAG1C,SAAS,WAAW,MAAmC;AAC5D,SAAO,KAAK,SAAS,WAAW,KAAK,OAAO,QAAQ,gBAAgB,EAAE,EAAE,QAAQ,OAAO,EAAE,CAAC,KAAK;AACjG;AAMO,SAAS,aACd,OACA,MACA,MACuB;AACvB,QAAM,SAAS,WAAW,IAAI;AAE9B,QAAM,UAAU,QAAQ,KAAK,QAAQ,SAAS,IAAI,KAAK,UAAU,CAAC,MAAS;AAC3E,SAAO,MACJ,OAAO,CAAC,MAAM,CAAC,EAAE,KAAK,OAAO,EAC7B;AAAA,IAAQ,CAAC,MACR,QAAQ,IAAI,CAAC,WAAW;AACtB,YAAM,OAAO,UAAU,OAAO,cAAc,EAAE,MAAM,QAAQ,KAAK,aAAa,IAAI,EAAE;AACpF,aAAO;AAAA,QACL,KAAK,GAAG,MAAM,GAAG,SAAS,MAAM,MAAM,IAAI;AAAA,QAC1C,cAAc,EAAE,YAAY,IAAI,KAAK,EAAE,SAAS,IAAI;AAAA,QACpD,iBAAiB,EAAE,SAAS,MAAO,WAAsB;AAAA,QACzD,UAAU,EAAE,SAAS,MAAM,IAAI;AAAA,MACjC;AAAA,IACF,CAAC;AAAA,EACH;AACJ;AAMO,SAAS,YACd,MACA,MACsB;AACtB,QAAM,SAAS,WAAW,IAAI;AAC9B,MAAI,KAAK,SAAS,QAAQ;AAGxB,WAAO,EAAE,OAAO,EAAE,WAAW,KAAK,UAAU,IAAI,EAAE;AAAA,EACpD;AACA,SAAO;AAAA,IACL,OAAO,EAAE,WAAW,KAAK,OAAO,KAAK,UAAU,MAAM,YAAY,CAAC,WAAW,QAAQ,EAAE;AAAA,IACvF,GAAI,SAAS,EAAE,SAAS,GAAG,MAAM,gBAAgB,MAAM,OAAO,IAAI,CAAC;AAAA,EACrE;AACF;AAeO,SAAS,cACd,MACA,MACA,MACU;AACV,QAAM,MAAM,KAAK,OAAO,CAAC;AACzB,QAAM,IAAI,KAAK,eAAe,CAAC;AAC/B,QAAM,QAAQ,IAAI,SAAS,KAAK,SAAS,KAAK;AAC9C,QAAM,cAAc,IAAI,eAAe,EAAE;AAEzC,QAAM,SAAS,KAAK,SAAS,WAAW,KAAK,OAAO,QAAQ,gBAAgB,EAAE,EAAE,QAAQ,OAAO,EAAE,CAAC,KAAK;AACvG,QAAM,UAAU,IAAI,aAAa,KAAK;AACtC,QAAM,OAAO,QAAQ,CAAC,eAAe,KAAK,OAAO,IAAI,cAAc,SAAS,KAAK,QAAQ,KAAK,aAAa,IAAI;AAC/G,QAAM,YAAY,eAAe,KAAK,IAAI,IAAI,OAAO,GAAG,MAAM,GAAG,KAAK,WAAW,GAAG,IAAI,KAAK,GAAG,GAAG,IAAI;AACvG,QAAM,YACJ,QAAQ,KAAK,QAAQ,SAAS,IAC1B,OAAO,YAAY;AAAA,IACjB,GAAG,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,GAAG,GAAG,MAAM,GAAG,cAAc,KAAK,MAAM,GAAG,KAAK,aAAa,CAAC,EAAE,CAAC;AAAA,IAC7F,CAAC,aAAa,GAAG,MAAM,GAAG,KAAK,IAAI,EAAE;AAAA,EACvC,CAAC,IACD;AACN,QAAM,UAAU,IAAI,WAAW,EAAE;AAEjC,QAAM,SAAS,UAAU,CAAC,EAAE,KAAK,SAAS,OAAO,MAAM,QAAQ,IAAI,CAAC,IAAI;AAExE,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,cAAc,SAAS,IAAI,IAAI,MAAM,IAAI;AAAA,IACzC,YAAY,EAAE,WAAW,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC,EAAG;AAAA,IAC7D,QAAQ,IAAI,WAAW,KAAK,SAAS,SAAS,EAAE,OAAO,OAAO,QAAQ,MAAM,IAAI;AAAA,IAChF,WAAW;AAAA,MACT;AAAA,MACA;AAAA,MACA,KAAK;AAAA,MACL,UAAU,KAAK;AAAA,MACf,MAAM;AAAA,MACN,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,IAC7B;AAAA,IACA,SAAS;AAAA,MACP,MAAM,SAAS,wBAAwB;AAAA,MACvC;AAAA,MACA;AAAA,MACA,GAAI,SAAS,EAAE,QAAQ,CAAC,OAAiB,EAAE,IAAI,CAAC;AAAA,IAClD;AAAA,EACF;AACF;AAgCO,SAAS,eAAe,MAA6C;AAC1E,QAAM,MAA+B;AAAA,IACnC,YAAY;AAAA,IACZ,SAAS,KAAK,QAAQ;AAAA;AAAA;AAAA,IAGtB,OAAO;AAAA,IACP,MAAM,KAAK;AAAA,EACb;AACA,MAAI,KAAK,YAAa,KAAI,cAAc,KAAK;AAC7C,MAAI,KAAK,IAAK,KAAI,MAAM,KAAK;AAC7B,MAAI,KAAK,UAAW,KAAI,YAAY,KAAK;AACzC,MAAI,KAAK,MAAO,KAAI,QAAQ,KAAK;AACjC,MAAI,KAAK,MAAO,KAAI,QAAQ,KAAK;AACjC,MAAI,KAAK,WAAY,KAAI,aAAa,KAAK;AAC3C,MAAI,KAAK,SAAS;AAChB,QAAI,UAAU;AAAA,MACZ,SAAS;AAAA,MACT,eAAe,KAAK,QAAQ;AAAA,MAC5B,iBAAiB,KAAK,QAAQ;AAAA,MAC9B,eAAe,KAAK,QAAQ;AAAA,MAC5B,YAAY,KAAK,QAAQ;AAAA,MACzB,gBAAgB,KAAK,QAAQ;AAAA,IAC/B;AAAA,EACF;AACA,MAAI,KAAK,IAAK,KAAI,MAAM,EAAE,SAAS,kBAAkB,UAAU,KAAK,IAAI,KAAK,WAAW,KAAK,IAAI,IAAI;AACrG,MAAI,KAAK,cAAc,OAAQ,KAAI,eAAe,KAAK;AACvD,MAAI,KAAK,QAAQ,OAAQ,KAAI,SAAS,KAAK;AAC3C,SAAO;AACT;AAGO,SAAS,qBAAqB,OAAiE;AACpG,SAAO;AAAA,IACL,YAAY;AAAA,IACZ,SAAS;AAAA,IACT,iBAAiB,MAAM,IAAI,CAAC,IAAI,OAAO;AAAA,MACrC,SAAS;AAAA,MACT,UAAU,IAAI;AAAA,MACd,MAAM,GAAG;AAAA,MACT,MAAM,GAAG;AAAA,IACX,EAAE;AAAA,EACJ;AACF;AAGO,SAAS,cAAc,GAOF;AAC1B,SAAO;AAAA,IACL,YAAY;AAAA,IACZ,SAAS;AAAA,IACT,UAAU,EAAE;AAAA,IACZ,GAAI,EAAE,cAAc,EAAE,aAAa,EAAE,YAAY,IAAI,CAAC;AAAA,IACtD,GAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,UAAU,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IACpE,GAAI,EAAE,gBAAgB,EAAE,eAAe,EAAE,cAAc,IAAI,CAAC;AAAA,IAC5D,GAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAAA,IACpC,GAAI,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,IAAI,CAAC;AAAA,EAChC;AACF;AAGO,SAAS,cAAc,GAOF;AAC1B,SAAO;AAAA,IACL,YAAY;AAAA,IACZ,SAAS;AAAA,IACT,MAAM,EAAE;AAAA,IACR,GAAI,EAAE,cAAc,EAAE,aAAa,EAAE,YAAY,IAAI,CAAC;AAAA,IACtD,GAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAAA,IACpC,GAAI,EAAE,SAAS,OACX;AAAA,MACE,QAAQ;AAAA,QACN,SAAS;AAAA,QACT,OAAO,OAAO,EAAE,KAAK;AAAA,QACrB,eAAe,EAAE,YAAY;AAAA,QAC7B,GAAI,EAAE,eAAe,EAAE,cAAc,sBAAsB,EAAE,YAAY,GAAG,IAAI,CAAC;AAAA,MACnF;AAAA,IACF,IACA,CAAC;AAAA,EACP;AACF;AAGO,SAAS,mBACd,MAC2B;AAC3B,SAAO,KAAK,IAAI,CAAC,OAAO;AAAA,IACtB,SAAS;AAAA,IACT,WAAW,EAAE;AAAA,IACb,OAAO,EAAE;AAAA,IACT,QAAQ,EAAE;AAAA,EACZ,EAAE;AACJ;AAGO,SAAS,cAAc,OAAqE;AACjG,SAAO;AAAA,IACL,YAAY;AAAA,IACZ,SAAS;AAAA,IACT,OAAO,MAAM,IAAI,CAAC,QAAQ,EAAE,SAAS,eAAe,YAAY,GAAG,KAAK,GAAI,GAAG,UAAU,EAAE,SAAS,GAAG,QAAQ,IAAI,CAAC,EAAG,EAAE;AAAA,EAC3H;AACF;AAGO,SAAS,kBAAkB,GAMN;AAC1B,SAAO;AAAA,IACL,YAAY;AAAA,IACZ,SAAS;AAAA,IACT,MAAM,EAAE;AAAA,IACR,GAAI,EAAE,cAAc,EAAE,aAAa,EAAE,YAAY,IAAI,CAAC;AAAA,IACtD,GAAI,EAAE,eAAe,EAAE,cAAc,EAAE,aAAa,IAAI,CAAC;AAAA,IACzD,GAAI,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,IAAI,CAAC;AAAA,IACnD,GAAI,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,IAAI,CAAC;AAAA,EACrD;AACF;AAGO,SAAS,YACd,MACA,MACyB;AACzB,QAAM,OAAO,KAAK,KAAK,cAAc;AACrC,QAAM,OAAO,KAAK,SAAS,WAAW,KAAK,OAAO,QAAQ,gBAAgB,EAAE,CAAC,KAAK;AAClF,SAAO;AAAA,IACL,YAAY;AAAA,IACZ,SAAS;AAAA,IACT,MAAM,KAAK,KAAK,SAAS,KAAK;AAAA,IAC9B,aAAa,KAAK,KAAK,eAAe,KAAK,aAAa;AAAA,IACxD,KAAK,OAAO,KAAK;AAAA,IACjB,WAAW,EAAE,SAAS,gBAAgB,MAAM,KAAK,KAAK;AAAA,EACxD;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/server/seo.ts"],"sourcesContent":["import \"server-only\";\n\nimport type { Metadata, MetadataRoute } from \"next\";\nimport type { Seo } from \"../schema/seo\";\nimport type { Page } from \"../schema/pages\";\nimport { localizedPath, type LocaleConfig } from \"../i18n/overlay\";\n\n/** Absolute origin (https://domein) uit de site-config; \"\" als er geen domein is. */\nexport function siteOrigin(site: { domain?: string }): string {\n return site.domain ? `https://${site.domain.replace(/^https?:\\/\\//, \"\").replace(/\\/$/, \"\")}` : \"\";\n}\n\n/**\n * Bouwt een Next `sitemap.xml` uit een lijst pagina's. Gebruik in `app/sitemap.ts`:\n * export default function sitemap() { return buildSitemap(pages, siteConfig); }\n */\nexport function buildSitemap(\n pages: Array<{ path: string; updatedAt?: string | number | Date; seo?: { noindex?: boolean } }>,\n site: { domain?: string },\n i18n?: LocaleConfig,\n): MetadataRoute.Sitemap {\n const origin = siteOrigin(site);\n // Meertalige site (5.0): elke pagina één keer per taal (default zonder prefix, rest mét).\n const locales = i18n && i18n.locales.length > 1 ? i18n.locales : [undefined];\n return pages\n .filter((p) => !p.seo?.noindex)\n .flatMap((p) =>\n locales.map((locale) => {\n const path = locale && i18n ? localizedPath(p.path, locale, i18n.defaultLocale) : p.path;\n return {\n url: `${origin}${path === \"/\" ? \"/\" : path}`,\n lastModified: p.updatedAt ? new Date(p.updatedAt) : undefined,\n changeFrequency: p.path === \"/\" ? (\"weekly\" as const) : (\"monthly\" as const),\n priority: p.path === \"/\" ? 1 : 0.7,\n };\n }),\n );\n}\n\n/**\n * Bouwt een Next `robots.txt`. In `mode:\"demo\"` wordt de hele site op noindex gezet; anders alles\n * toegestaan behalve de beheerpaden, met verwijzing naar de sitemap. Gebruik in `app/robots.ts`.\n */\nexport function buildRobots(\n site: { domain?: string; mode?: string },\n opts?: { disallow?: string[] },\n): MetadataRoute.Robots {\n const origin = siteOrigin(site);\n if (site.mode === \"demo\") {\n // Let op: disallow blokkeert alleen het CRAWLEN — de URL kan alsnog (kaal) geïndexeerd\n // raken. buildMetadata zet in demo-modus daarom óók robots.index=false op elke pagina.\n return { rules: { userAgent: \"*\", disallow: \"/\" } };\n }\n return {\n rules: { userAgent: \"*\", allow: \"/\", disallow: opts?.disallow ?? [\"/beheer\", \"/login\"] },\n ...(origin ? { sitemap: `${origin}/sitemap.xml`, host: origin } : {}),\n };\n}\n\ninterface SeoSiteContext {\n name: string;\n domain?: string;\n /** \"demo\" zet elke pagina op noindex (robots.txt-disallow alleen is geen noindex). */\n mode?: string;\n seoDefaults?: Seo;\n}\n\n/**\n * Bouwt Next `Metadata` voor een pagina, met fallback op site-defaults. Geef voor een meertalige\n * site (5.0) `i18n` mee: de canonical wijst dan naar de taalvariant en er komen\n * hreflang-alternates voor élke taal (default zonder prefix, incl. x-default).\n */\nexport function buildMetadata(\n page: Pick<Page, \"title\" | \"path\" | \"seo\">,\n site: SeoSiteContext,\n i18n?: LocaleConfig & { locale: string },\n): Metadata {\n const seo = page.seo ?? {};\n const d = site.seoDefaults ?? {};\n const title = seo.title ?? page.title ?? site.name;\n const description = seo.description ?? d.description;\n // Absolute canonical/OG-URL (Google prefereert absoluut) wanneer het domein bekend is.\n const origin = site.domain ? `https://${site.domain.replace(/^https?:\\/\\//, \"\").replace(/\\/$/, \"\")}` : \"\";\n const rawPath = seo.canonical ?? page.path;\n // Een door de auteur EXPLICIET gezette canonical wordt verbatim gebruikt — die her-localiseren\n // (taalprefix ervoor plakken) zou de bewuste keuze van de auteur overschrijven.\n const path =\n i18n && !seo.canonical && !/^https?:\\/\\//.test(rawPath)\n ? localizedPath(rawPath, i18n.locale, i18n.defaultLocale)\n : rawPath;\n const canonical = /^https?:\\/\\//.test(path) ? path : `${origin}${path.startsWith(\"/\") ? \"\" : \"/\"}${path}`;\n const languages =\n i18n && i18n.locales.length > 1\n ? Object.fromEntries([\n ...i18n.locales.map((l) => [l, `${origin}${localizedPath(page.path, l, i18n.defaultLocale)}`]),\n [\"x-default\", `${origin}${page.path}`],\n ])\n : undefined;\n const ogImage = seo.ogImage ?? d.ogImage;\n // OG-afbeeldingen met afmetingen (1200×630) → betere previews op social.\n const images = ogImage ? [{ url: ogImage, width: 1200, height: 630 }] : undefined;\n\n return {\n title,\n description,\n metadataBase: origin ? new URL(origin) : undefined,\n alternates: { canonical, ...(languages ? { languages } : {}) },\n robots: seo.noindex || site.mode === \"demo\" ? { index: false, follow: false } : undefined,\n openGraph: {\n title,\n description,\n url: canonical,\n siteName: site.name,\n type: \"website\",\n ...(images ? { images } : {}),\n },\n twitter: {\n card: images ? \"summary_large_image\" : \"summary\",\n title,\n description,\n ...(images ? { images: [ogImage as string] } : {}),\n },\n };\n}\n\n/** Bedrijfsgegevens voor gestructureerde data (schema.org LocalBusiness e.d.). */\nexport interface BusinessInfo {\n /** schema.org-type, bv. \"LocalBusiness\", \"LodgingBusiness\", \"Restaurant\", \"Store\". */\n type?: string;\n name: string;\n description?: string;\n url?: string;\n telephone?: string;\n email?: string;\n image?: string;\n /** Indicatie prijsniveau, bv. \"€€\". */\n priceRange?: string;\n address?: {\n street?: string;\n locality?: string;\n region?: string;\n postalCode?: string;\n country?: string;\n };\n geo?: { lat: number; lng: number };\n /** schema.org openingHours, bv. [\"Mo-Fr 09:00-17:00\", \"Sa 10:00-16:00\"]. */\n openingHours?: string[];\n /** Externe profielen (social, etc.). */\n sameAs?: string[];\n}\n\n/**\n * Bouwt gestructureerde data voor een lokaal bedrijf (herbruikbaar per branche). Plaats het\n * resultaat als JSON-LD in de layout: <script type=\"application/ld+json\">JSON.stringify(...)</script>\n */\nexport function businessJsonLd(info: BusinessInfo): Record<string, unknown> {\n const out: Record<string, unknown> = {\n \"@context\": \"https://schema.org\",\n \"@type\": info.type ?? \"LocalBusiness\",\n // Zelfde relatieve @id als de contact-/openingstijden-blokken: zo voegen zoekmachines de\n // layout-brede en blok-specifieke LocalBusiness-JSON-LD samen tot één entiteit per pagina.\n \"@id\": \"#business\",\n name: info.name,\n };\n if (info.description) out.description = info.description;\n if (info.url) out.url = info.url;\n if (info.telephone) out.telephone = info.telephone;\n if (info.email) out.email = info.email;\n if (info.image) out.image = info.image;\n if (info.priceRange) out.priceRange = info.priceRange;\n // Alleen een PostalAddress-node emitten als er ook echt een adresveld is ingevuld — een leeg\n // object is ongeldige/lage-kwaliteit structured data.\n if (info.address && (info.address.street || info.address.locality || info.address.postalCode)) {\n out.address = {\n \"@type\": \"PostalAddress\",\n streetAddress: info.address.street,\n addressLocality: info.address.locality,\n addressRegion: info.address.region,\n postalCode: info.address.postalCode,\n addressCountry: info.address.country,\n };\n }\n if (info.geo) out.geo = { \"@type\": \"GeoCoordinates\", latitude: info.geo.lat, longitude: info.geo.lng };\n if (info.openingHours?.length) out.openingHours = info.openingHours;\n if (info.sameAs?.length) out.sameAs = info.sameAs;\n return out;\n}\n\n/** Breadcrumb-trail (schema.org BreadcrumbList) — voor genest navigeren + rich results. */\nexport function breadcrumbListJsonLd(items: { name: string; url: string }[]): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"BreadcrumbList\",\n itemListElement: items.map((it, i) => ({\n \"@type\": \"ListItem\",\n position: i + 1,\n name: it.name,\n item: it.url,\n })),\n };\n}\n\n/** Article-JSON-LD voor nieuws/blog. */\nexport function articleJsonLd(a: {\n title: string;\n description?: string;\n author?: string;\n datePublished?: string;\n image?: string;\n url?: string;\n}): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Article\",\n headline: a.title,\n ...(a.description ? { description: a.description } : {}),\n ...(a.author ? { author: { \"@type\": \"Person\", name: a.author } } : {}),\n ...(a.datePublished ? { datePublished: a.datePublished } : {}),\n ...(a.image ? { image: a.image } : {}),\n ...(a.url ? { url: a.url } : {}),\n };\n}\n\n/** Product/Offer-JSON-LD. */\nexport function productJsonLd(p: {\n name: string;\n description?: string;\n price?: string | number;\n currency?: string;\n image?: string;\n availability?: string;\n}): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Product\",\n name: p.name,\n ...(p.description ? { description: p.description } : {}),\n ...(p.image ? { image: p.image } : {}),\n ...(p.price != null\n ? {\n offers: {\n \"@type\": \"Offer\",\n price: String(p.price),\n priceCurrency: p.currency ?? \"EUR\",\n ...(p.availability ? { availability: `https://schema.org/${p.availability}` } : {}),\n },\n }\n : {}),\n };\n}\n\n/** OpeningHoursSpecification — bv. `[{ days:[\"Mo\",\"Tu\"], opens:\"09:00\", closes:\"17:00\" }]`. */\nexport function openingHoursJsonLd(\n spec: { days: string[]; opens: string; closes: string }[],\n): Record<string, unknown>[] {\n return spec.map((s) => ({\n \"@type\": \"OpeningHoursSpecification\",\n dayOfWeek: s.days,\n opens: s.opens,\n closes: s.closes,\n }));\n}\n\n/** ImageGallery-JSON-LD voor een fotogalerij. */\nexport function galleryJsonLd(items: { url: string; caption?: string }[]): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"ImageGallery\",\n image: items.map((it) => ({ \"@type\": \"ImageObject\", contentUrl: it.url, ...(it.caption ? { caption: it.caption } : {}) })),\n };\n}\n\n/** VideoObject-JSON-LD. */\nexport function videoObjectJsonLd(v: {\n name: string;\n description?: string;\n thumbnailUrl?: string;\n contentUrl?: string;\n uploadDate?: string;\n}): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"VideoObject\",\n name: v.name,\n ...(v.description ? { description: v.description } : {}),\n ...(v.thumbnailUrl ? { thumbnailUrl: v.thumbnailUrl } : {}),\n ...(v.contentUrl ? { contentUrl: v.contentUrl } : {}),\n ...(v.uploadDate ? { uploadDate: v.uploadDate } : {}),\n };\n}\n\n/** Bouwt een JSON-LD-object voor een pagina (schema.org). */\nexport function buildJsonLd(\n page: Pick<Page, \"title\" | \"path\" | \"seo\">,\n site: SeoSiteContext,\n): Record<string, unknown> {\n const type = page.seo?.jsonLdType ?? \"WebPage\";\n const base = site.domain ? `https://${site.domain.replace(/^https?:\\/\\//, \"\")}` : \"\";\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": type,\n name: page.seo?.title ?? page.title,\n description: page.seo?.description ?? site.seoDefaults?.description,\n // Alleen een url-node met een echte origin: een relatieve url (\"/over-ons\") is ongeldige\n // structured data wanneer het domein (nog) niet geconfigureerd is.\n ...(base ? { url: base + page.path } : {}),\n publisher: { \"@type\": \"Organization\", name: site.name },\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -5,21 +5,33 @@ import type { SiteConfig } from "../schema/siteConfig";
|
|
|
5
5
|
* Live, publiek-veilige site-instellingen (`sites/{siteId}/settings/public`) zodat dingen die de
|
|
6
6
|
* klant in /beheer wijzigt (footer, nav, thema, module-instellingen) direct op de site verschijnen.
|
|
7
7
|
*
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
8
|
+
* - Met SSR-seed (`initial`, via XiCMSProvider `settingsData`): géén client-read voor publiek —
|
|
9
|
+
* de settings zitten al in de server-HTML (geen theme-flits) en verversen via ISR.
|
|
10
|
+
* - Zonder seed (publiek): één `getDoc` (geen per-bezoeker listener).
|
|
11
|
+
* - Editors: `useSiteSettingsLiveSync` (onSnapshot) zodat /beheer-wijzigingen meteen zichtbaar zijn.
|
|
10
12
|
* Zonder Firebase of zonder doc valt alles terug op de statische `SiteConfig` (identiek aan vroeger).
|
|
11
13
|
*/
|
|
12
14
|
interface Ctx {
|
|
13
15
|
settings: PublicSiteSettings;
|
|
14
16
|
loaded: boolean;
|
|
17
|
+
/** Interne setter voor de editor-live-sync (useSiteSettingsLiveSync). */
|
|
18
|
+
_set?: (s: PublicSiteSettings) => void;
|
|
15
19
|
}
|
|
16
|
-
export declare function SiteSettingsProvider({ siteId, live, children, }: {
|
|
20
|
+
export declare function SiteSettingsProvider({ siteId, live, initial, children, }: {
|
|
17
21
|
siteId: string;
|
|
18
22
|
live?: boolean;
|
|
23
|
+
/** SSR-startset (server-side gelezen via `getPublicSiteSettings`); slaat de client-read over. */
|
|
24
|
+
initial?: PublicSiteSettings;
|
|
19
25
|
children: ReactNode;
|
|
20
26
|
}): import("react").JSX.Element;
|
|
21
27
|
/** Ruwe publieke settings + laadstatus. */
|
|
22
28
|
export declare function useSiteSettings(): Ctx;
|
|
29
|
+
/**
|
|
30
|
+
* Editor-live-sync: onSnapshot op `settings/public` zolang `enabled` (ingelogde editor). Aparte
|
|
31
|
+
* hook (i.p.v. `live` op de provider) zodat publiek nooit een listener krijgt — zelfde patroon
|
|
32
|
+
* als CustomBlocksSync in XiCMSProvider. Een leesfout laat de bestaande settings staan.
|
|
33
|
+
*/
|
|
34
|
+
export declare function useSiteSettingsLiveSync(siteId: string, enabled: boolean): void;
|
|
23
35
|
/** SiteConfig met de live Firestore-settings eroverheen (Firestore wint waar gezet). */
|
|
24
36
|
export declare function useMergedSiteConfig(): SiteConfig;
|
|
25
37
|
export {};
|
|
@@ -12,11 +12,14 @@ const SettingsContext = createContext({ settings: {}, loaded: false });
|
|
|
12
12
|
function SiteSettingsProvider({
|
|
13
13
|
siteId,
|
|
14
14
|
live = false,
|
|
15
|
+
initial,
|
|
15
16
|
children
|
|
16
17
|
}) {
|
|
17
|
-
const
|
|
18
|
-
const [
|
|
18
|
+
const hasInitial = initial !== void 0;
|
|
19
|
+
const [settings, setSettings] = useState(initial ?? {});
|
|
20
|
+
const [loaded, setLoaded] = useState(hasInitial);
|
|
19
21
|
useEffect(() => {
|
|
22
|
+
if (hasInitial && !live) return;
|
|
20
23
|
const db = getDb();
|
|
21
24
|
if (!db) {
|
|
22
25
|
setLoaded(true);
|
|
@@ -40,12 +43,29 @@ function SiteSettingsProvider({
|
|
|
40
43
|
return () => {
|
|
41
44
|
cancelled = true;
|
|
42
45
|
};
|
|
43
|
-
}, [siteId, live]);
|
|
44
|
-
|
|
46
|
+
}, [siteId, live, hasInitial]);
|
|
47
|
+
const value = useMemo(() => ({ settings, loaded, _set: setSettings }), [settings, loaded]);
|
|
48
|
+
return /* @__PURE__ */ jsx(SettingsContext.Provider, { value, children });
|
|
45
49
|
}
|
|
46
50
|
function useSiteSettings() {
|
|
47
51
|
return useContext(SettingsContext);
|
|
48
52
|
}
|
|
53
|
+
function useSiteSettingsLiveSync(siteId, enabled) {
|
|
54
|
+
const { _set } = useContext(SettingsContext);
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!enabled || !_set) return;
|
|
57
|
+
const db = getDb();
|
|
58
|
+
if (!db) return;
|
|
59
|
+
return onSnapshot(
|
|
60
|
+
doc(db, "sites", siteId, "settings", "public"),
|
|
61
|
+
(snap) => {
|
|
62
|
+
const res = PublicSiteSettingsSchema.safeParse(snap.exists() ? snap.data() : {});
|
|
63
|
+
if (res.success) _set(res.data);
|
|
64
|
+
},
|
|
65
|
+
() => void 0
|
|
66
|
+
);
|
|
67
|
+
}, [enabled, siteId, _set]);
|
|
68
|
+
}
|
|
49
69
|
function useMergedSiteConfig() {
|
|
50
70
|
const base = useSiteConfig();
|
|
51
71
|
const { settings } = useSiteSettings();
|
|
@@ -63,6 +83,7 @@ function useMergedSiteConfig() {
|
|
|
63
83
|
export {
|
|
64
84
|
SiteSettingsProvider,
|
|
65
85
|
useMergedSiteConfig,
|
|
66
|
-
useSiteSettings
|
|
86
|
+
useSiteSettings,
|
|
87
|
+
useSiteSettingsLiveSync
|
|
67
88
|
};
|
|
68
89
|
//# sourceMappingURL=useSiteSettings.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/settings/useSiteSettings.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport { doc, getDoc, onSnapshot } from \"firebase/firestore\";\nimport { getDb } from \"../firebase/client\";\nimport { useSiteConfig } from \"../config\";\nimport {\n PublicSiteSettingsSchema,\n mergeThemeTokens,\n type PublicSiteSettings,\n} from \"../schema/settings\";\nimport type { SiteConfig } from \"../schema/siteConfig\";\n\n/**\n * Live, publiek-veilige site-instellingen (`sites/{siteId}/settings/public`) zodat dingen die de\n * klant in /beheer wijzigt (footer, nav, thema, module-instellingen) direct op de site verschijnen.\n *\n * -
|
|
1
|
+
{"version":3,"sources":["../../src/settings/useSiteSettings.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport { doc, getDoc, onSnapshot } from \"firebase/firestore\";\nimport { getDb } from \"../firebase/client\";\nimport { useSiteConfig } from \"../config\";\nimport {\n PublicSiteSettingsSchema,\n mergeThemeTokens,\n type PublicSiteSettings,\n} from \"../schema/settings\";\nimport type { SiteConfig } from \"../schema/siteConfig\";\n\n/**\n * Live, publiek-veilige site-instellingen (`sites/{siteId}/settings/public`) zodat dingen die de\n * klant in /beheer wijzigt (footer, nav, thema, module-instellingen) direct op de site verschijnen.\n *\n * - Met SSR-seed (`initial`, via XiCMSProvider `settingsData`): géén client-read voor publiek —\n * de settings zitten al in de server-HTML (geen theme-flits) en verversen via ISR.\n * - Zonder seed (publiek): één `getDoc` (geen per-bezoeker listener).\n * - Editors: `useSiteSettingsLiveSync` (onSnapshot) zodat /beheer-wijzigingen meteen zichtbaar zijn.\n * Zonder Firebase of zonder doc valt alles terug op de statische `SiteConfig` (identiek aan vroeger).\n */\ninterface Ctx {\n settings: PublicSiteSettings;\n loaded: boolean;\n /** Interne setter voor de editor-live-sync (useSiteSettingsLiveSync). */\n _set?: (s: PublicSiteSettings) => void;\n}\nconst SettingsContext = createContext<Ctx>({ settings: {}, loaded: false });\n\nexport function SiteSettingsProvider({\n siteId,\n live = false,\n initial,\n children,\n}: {\n siteId: string;\n live?: boolean;\n /** SSR-startset (server-side gelezen via `getPublicSiteSettings`); slaat de client-read over. */\n initial?: PublicSiteSettings;\n children: ReactNode;\n}) {\n const hasInitial = initial !== undefined;\n const [settings, setSettings] = useState<PublicSiteSettings>(initial ?? {});\n const [loaded, setLoaded] = useState(hasInitial);\n\n useEffect(() => {\n // Met een SSR-seed doet de publieke site géén client-read meer: de layout-route is ISR-gecached\n // en levert verse settings per revalidate; editors krijgen apart een live sync.\n if (hasInitial && !live) return;\n const db = getDb();\n if (!db) {\n setLoaded(true);\n return;\n }\n const ref = doc(db, \"sites\", siteId, \"settings\", \"public\");\n const apply = (data: unknown) => {\n const res = PublicSiteSettingsSchema.safeParse(data ?? {});\n if (res.success) setSettings(res.data);\n setLoaded(true);\n };\n if (live) {\n return onSnapshot(\n ref,\n (snap) => apply(snap.exists() ? snap.data() : {}),\n () => setLoaded(true),\n );\n }\n let cancelled = false;\n getDoc(ref)\n .then((snap) => !cancelled && apply(snap.exists() ? snap.data() : {}))\n .catch(() => !cancelled && setLoaded(true));\n return () => {\n cancelled = true;\n };\n }, [siteId, live, hasInitial]);\n\n const value = useMemo<Ctx>(() => ({ settings, loaded, _set: setSettings }), [settings, loaded]);\n return <SettingsContext.Provider value={value}>{children}</SettingsContext.Provider>;\n}\n\n/** Ruwe publieke settings + laadstatus. */\nexport function useSiteSettings(): Ctx {\n return useContext(SettingsContext);\n}\n\n/**\n * Editor-live-sync: onSnapshot op `settings/public` zolang `enabled` (ingelogde editor). Aparte\n * hook (i.p.v. `live` op de provider) zodat publiek nooit een listener krijgt — zelfde patroon\n * als CustomBlocksSync in XiCMSProvider. Een leesfout laat de bestaande settings staan.\n */\nexport function useSiteSettingsLiveSync(siteId: string, enabled: boolean) {\n const { _set } = useContext(SettingsContext);\n useEffect(() => {\n if (!enabled || !_set) return;\n const db = getDb();\n if (!db) return;\n return onSnapshot(\n doc(db, \"sites\", siteId, \"settings\", \"public\"),\n (snap) => {\n const res = PublicSiteSettingsSchema.safeParse(snap.exists() ? snap.data() : {});\n if (res.success) _set(res.data);\n },\n () => undefined,\n );\n }, [enabled, siteId, _set]);\n}\n\n/** SiteConfig met de live Firestore-settings eroverheen (Firestore wint waar gezet). */\nexport function useMergedSiteConfig(): SiteConfig {\n const base = useSiteConfig();\n const { settings } = useSiteSettings();\n return useMemo(() => {\n const nav = settings.nav && settings.nav.length > 0 ? settings.nav : base.nav;\n return {\n ...base,\n name: settings.name ?? base.name,\n nav,\n theme: mergeThemeTokens(base.theme, settings.theme),\n seoDefaults: settings.seoDefaults ?? base.seoDefaults,\n };\n }, [base, settings]);\n}\n"],"mappings":";AA+ES;AA7ET,SAAS,eAAe,YAAY,WAAW,SAAS,gBAAgC;AACxF,SAAS,KAAK,QAAQ,kBAAkB;AACxC,SAAS,aAAa;AACtB,SAAS,qBAAqB;AAC9B;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AAmBP,MAAM,kBAAkB,cAAmB,EAAE,UAAU,CAAC,GAAG,QAAQ,MAAM,CAAC;AAEnE,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA;AACF,GAMG;AACD,QAAM,aAAa,YAAY;AAC/B,QAAM,CAAC,UAAU,WAAW,IAAI,SAA6B,WAAW,CAAC,CAAC;AAC1E,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,UAAU;AAE/C,YAAU,MAAM;AAGd,QAAI,cAAc,CAAC,KAAM;AACzB,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,IAAI;AACP,gBAAU,IAAI;AACd;AAAA,IACF;AACA,UAAM,MAAM,IAAI,IAAI,SAAS,QAAQ,YAAY,QAAQ;AACzD,UAAM,QAAQ,CAAC,SAAkB;AAC/B,YAAM,MAAM,yBAAyB,UAAU,QAAQ,CAAC,CAAC;AACzD,UAAI,IAAI,QAAS,aAAY,IAAI,IAAI;AACrC,gBAAU,IAAI;AAAA,IAChB;AACA,QAAI,MAAM;AACR,aAAO;AAAA,QACL;AAAA,QACA,CAAC,SAAS,MAAM,KAAK,OAAO,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC;AAAA,QAChD,MAAM,UAAU,IAAI;AAAA,MACtB;AAAA,IACF;AACA,QAAI,YAAY;AAChB,WAAO,GAAG,EACP,KAAK,CAAC,SAAS,CAAC,aAAa,MAAM,KAAK,OAAO,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,EACpE,MAAM,MAAM,CAAC,aAAa,UAAU,IAAI,CAAC;AAC5C,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,QAAQ,MAAM,UAAU,CAAC;AAE7B,QAAM,QAAQ,QAAa,OAAO,EAAE,UAAU,QAAQ,MAAM,YAAY,IAAI,CAAC,UAAU,MAAM,CAAC;AAC9F,SAAO,oBAAC,gBAAgB,UAAhB,EAAyB,OAAe,UAAS;AAC3D;AAGO,SAAS,kBAAuB;AACrC,SAAO,WAAW,eAAe;AACnC;AAOO,SAAS,wBAAwB,QAAgB,SAAkB;AACxE,QAAM,EAAE,KAAK,IAAI,WAAW,eAAe;AAC3C,YAAU,MAAM;AACd,QAAI,CAAC,WAAW,CAAC,KAAM;AACvB,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,GAAI;AACT,WAAO;AAAA,MACL,IAAI,IAAI,SAAS,QAAQ,YAAY,QAAQ;AAAA,MAC7C,CAAC,SAAS;AACR,cAAM,MAAM,yBAAyB,UAAU,KAAK,OAAO,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC;AAC/E,YAAI,IAAI,QAAS,MAAK,IAAI,IAAI;AAAA,MAChC;AAAA,MACA,MAAM;AAAA,IACR;AAAA,EACF,GAAG,CAAC,SAAS,QAAQ,IAAI,CAAC;AAC5B;AAGO,SAAS,sBAAkC;AAChD,QAAM,OAAO,cAAc;AAC3B,QAAM,EAAE,SAAS,IAAI,gBAAgB;AACrC,SAAO,QAAQ,MAAM;AACnB,UAAM,MAAM,SAAS,OAAO,SAAS,IAAI,SAAS,IAAI,SAAS,MAAM,KAAK;AAC1E,WAAO;AAAA,MACL,GAAG;AAAA,MACH,MAAM,SAAS,QAAQ,KAAK;AAAA,MAC5B;AAAA,MACA,OAAO,iBAAiB,KAAK,OAAO,SAAS,KAAK;AAAA,MAClD,aAAa,SAAS,eAAe,KAAK;AAAA,IAC5C;AAAA,EACF,GAAG,CAAC,MAAM,QAAQ,CAAC;AACrB;","names":[]}
|