@xiwebdesign/xicms-core 7.11.0 → 7.12.1

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.
@@ -3,6 +3,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useState } from "react";
4
4
  import { FormBlockSchema } from "../../schema/blocks.js";
5
5
  import { defineBlock } from "../registry.js";
6
+ import { useBlockData } from "../context.js";
6
7
  import { submitFormValues } from "./lib/submitForm.js";
7
8
  import { isDemoActive, getDemoForm } from "../../demo/demoStore.js";
8
9
  import { useSiteConfig } from "../../config.js";
@@ -25,7 +26,9 @@ const DEMO_FALLBACK_FORM = {
25
26
  };
26
27
  function FormRender({ block, siteId, mode }) {
27
28
  const siteMode = useSiteConfig().mode;
28
- const [def, setDef] = useState(null);
29
+ const serverForms = useBlockData("forms");
30
+ const serverDef = serverForms?.[block.formId] ?? null;
31
+ const [def, setDef] = useState(serverDef);
29
32
  const [unavailable, setUnavailable] = useState(false);
30
33
  const [values, setValues] = useState({});
31
34
  const [hp, setHp] = useState("");
@@ -39,6 +42,7 @@ function FormRender({ block, siteId, mode }) {
39
42
  setLocalOnly(true);
40
43
  return;
41
44
  }
45
+ if (serverDef) return;
42
46
  void (async () => {
43
47
  const [{ getDb }, { doc, getDoc }] = await Promise.all([
44
48
  import("../../firebase/client.js"),
@@ -63,7 +67,7 @@ function FormRender({ block, siteId, mode }) {
63
67
  setUnavailable(true);
64
68
  }
65
69
  })();
66
- }, [demo, siteId, block.formId, siteMode]);
70
+ }, [demo, siteId, block.formId, siteMode, serverDef]);
67
71
  if (unavailable) {
68
72
  return /* @__PURE__ */ jsx(Card, { padding: 4, variant: "muted", children: /* @__PURE__ */ jsxs(Text, { children: [
69
73
  block.heading ? `${block.heading}: ` : "",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/blocks/builtins/FormBlock.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState, type FormEvent } from \"react\";\nimport type { z } from \"zod\";\nimport { FormBlockSchema } from \"../../schema/blocks\";\nimport type { FormDefinition } from \"../../schema/forms\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { submitFormValues } from \"./lib/submitForm\";\nimport { isDemoActive, getDemoForm } from \"../../demo/demoStore\";\nimport { useSiteConfig } from \"../../config\";\nimport { TextInput } from \"@astryxdesign/core/TextInput\";\nimport { TextArea } from \"@astryxdesign/core/TextArea\";\nimport { Selector } from \"@astryxdesign/core/Selector\";\nimport { CheckboxInput } from \"@astryxdesign/core/CheckboxInput\";\nimport { Spinner } from \"@astryxdesign/core/Spinner\";\nimport { Button } from \"@astryxdesign/core/Button\";\nimport { VStack, Card, Heading, Text } from \"../ui\";\n\ntype FormBlk = z.infer<typeof FormBlockSchema>;\n\n/** Voorbeeldformulier voor de demo wanneer er geen specifieke definitie is geregistreerd. */\nconst DEMO_FALLBACK_FORM: FormDefinition = {\n name: \"Contact\",\n submitLabel: \"Versturen\",\n successMessage: \"Bedankt! Dit is een demo — je bericht is niet echt verstuurd.\",\n fields: [\n { name: \"naam\", label: \"Naam\", type: \"text\", required: true },\n { name: \"email\", label: \"E-mail\", type: \"email\", required: true },\n { name: \"bericht\", label: \"Bericht\", type: \"textarea\", required: false, placeholder: \"Waarmee kunnen we je helpen?\" },\n ],\n};\n\nfunction FormRender({ block, siteId, mode }: BlockRenderProps<FormBlk>) {\n const siteMode = useSiteConfig().mode;\n const [def, setDef] = useState<FormDefinition | null>(null);\n const [unavailable, setUnavailable] = useState(false);\n const [values, setValues] = useState<Record<string, string | boolean>>({});\n const [hp, setHp] = useState(\"\");\n const [status, setStatus] = useState<\"idle\" | \"busy\" | \"ok\" | \"error\">(\"idle\");\n const [error, setError] = useState<string | null>(null);\n\n const demo = isDemoActive();\n // \"Lokaal\" inzenden (geen Firebase-call) bij demo OF wanneer er geen Firebase is maar wél een\n // geregistreerd voorbeeldformulier — zo werkt een statische referentiesite z'n formulier ook.\n const [localOnly, setLocalOnly] = useState(demo);\n\n useEffect(() => {\n if (demo) {\n setDef(getDemoForm(siteId, block.formId) ?? DEMO_FALLBACK_FORM);\n setLocalOnly(true);\n return;\n }\n // Firestore dynamisch: alleen een pagina die écht een formulier toont haalt de SDK binnen.\n void (async () => {\n const [{ getDb }, { doc, getDoc }] = await Promise.all([\n import(\"../../firebase/client\"),\n import(\"firebase/firestore\"),\n ]);\n const db = getDb();\n if (!db) {\n // Geen Firebase: val terug op een geregistreerd formulier, of (op een mode:\"demo\"-site) op\n // het ingebouwde voorbeeld; beide zenden lokaal in. Anders een nette \"niet beschikbaar\".\n const reg = getDemoForm(siteId, block.formId) ?? (siteMode === \"demo\" ? DEMO_FALLBACK_FORM : null);\n if (reg) { setDef(reg); setLocalOnly(true); } else { setUnavailable(true); }\n return;\n }\n try {\n const snap = await getDoc(doc(db, \"sites\", siteId, \"forms\", block.formId));\n if (snap.exists()) setDef(snap.data() as FormDefinition);\n else setUnavailable(true);\n } catch {\n setUnavailable(true);\n }\n })();\n }, [demo, siteId, block.formId, siteMode]);\n\n if (unavailable) {\n return (\n <Card padding={4} variant=\"muted\">\n <Text>{block.heading ? `${block.heading}: ` : \"\"}neem contact met ons op — we reageren snel.</Text>\n </Card>\n );\n }\n if (!def) {\n return (\n <div style={{ display: \"grid\", placeItems: \"center\", minHeight: 120 }}>\n <Spinner label=\"Formulier laden…\" />\n </div>\n );\n }\n\n async function submit(e: FormEvent) {\n e.preventDefault();\n if (mode === \"preview\") return;\n if (localOnly) {\n // Lokaal (demo / geen backend): niets versturen, alleen de successtaat tonen.\n setStatus(\"ok\");\n setValues({});\n return;\n }\n setStatus(\"busy\");\n setError(null);\n try {\n await submitFormValues(siteId, block.formId, values, hp);\n // Optioneel doorsturen naar een bedankt-pagina (conversiemeting/advertenties).\n if (def?.successRedirect) {\n window.location.assign(def.successRedirect);\n return;\n }\n setStatus(\"ok\");\n setValues({});\n } catch {\n setStatus(\"error\");\n setError(\"Versturen mislukt. Probeer het later opnieuw.\");\n }\n }\n\n if (status === \"ok\") {\n return (\n // role=\"status\": screenreaders melden de succesboodschap wanneer het formulier verdwijnt.\n <Card padding={4} variant=\"muted\" role=\"status\">\n <Text>{def.successMessage}</Text>\n </Card>\n );\n }\n\n const setVal = (name: string, v: string | boolean) => setValues((prev) => ({ ...prev, [name]: v }));\n\n return (\n <form className=\"xicms-form\" onSubmit={submit}>\n <VStack gap={3}>\n {block.heading && <Heading level={2} data-xicms-edit=\"heading\">{block.heading}</Heading>}\n {def.fields.map((f) => {\n const sv = (values[f.name] as string) ?? \"\";\n if (f.type === \"textarea\") {\n return (\n <TextArea key={f.name} label={f.label} isRequired={f.required} placeholder={f.placeholder} rows={4}\n value={sv} onChange={(v) => setVal(f.name, v)} />\n );\n }\n if (f.type === \"select\") {\n return (\n <Selector key={f.name} label={f.label} isRequired={f.required} placeholder=\"Kies…\"\n options={f.options ?? []} value={sv} onChange={(v) => setVal(f.name, (v as string) ?? \"\")} />\n );\n }\n if (f.type === \"checkbox\") {\n return (\n <CheckboxInput key={f.name} label={f.label} isRequired={f.required}\n value={Boolean(values[f.name])} onChange={(checked) => setVal(f.name, checked)} />\n );\n }\n if (f.type === \"radio\") {\n return (\n <fieldset key={f.name} className=\"xicms-form__radios\">\n <legend>{f.label}{f.required && \" *\"}</legend>\n {(f.options ?? []).map((opt) => (\n <label key={opt} className=\"xicms-form__radio\">\n <input\n type=\"radio\"\n name={f.name}\n value={opt}\n required={f.required}\n checked={sv === opt}\n onChange={() => setVal(f.name, opt)}\n />\n {opt}\n </label>\n ))}\n </fieldset>\n );\n }\n if (f.type === \"date\" || f.type === \"number\") {\n // Astryx TextInput kent geen date/number → native input in dezelfde stijl.\n return (\n <label key={f.name} className=\"xicms-form__native\">\n <span className=\"xicms-form__native-label\">{f.label}{f.required && \" *\"}</span>\n <input\n className=\"xicms-input\"\n type={f.type}\n required={f.required}\n placeholder={f.placeholder}\n value={sv}\n onChange={(e) => setVal(f.name, e.target.value)}\n />\n </label>\n );\n }\n return (\n <TextInput key={f.name} label={f.label} type={f.type === \"email\" ? \"email\" : \"text\"}\n isRequired={f.required} placeholder={f.placeholder}\n value={sv} onChange={(v) => setVal(f.name, v)} />\n );\n })}\n {/* Honeypot — verborgen voor mensen, bots vullen 'm vaak wel in. */}\n <input\n type=\"text\"\n tabIndex={-1}\n autoComplete=\"off\"\n value={hp}\n onChange={(e) => setHp(e.target.value)}\n style={{ position: \"absolute\", left: \"-9999px\", width: 1, height: 1 }}\n aria-hidden\n />\n {error && <Text role=\"alert\" style={{ color: \"var(--color-error, #c0392b)\" }}>{error}</Text>}\n <div>\n <Button label={status === \"busy\" ? \"Versturen…\" : def.submitLabel} variant=\"primary\" size=\"lg\" type=\"submit\" isLoading={status === \"busy\"} />\n </div>\n </VStack>\n </form>\n );\n}\n\nexport const formBlock = defineBlock({\n type: \"form\",\n schema: FormBlockSchema,\n Render: FormRender,\n label: \"Formulier\",\n icon: \"✉️\",\n defaults: () => ({ formId: \"\" }),\n});\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/blocks/builtins/FormBlock.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState, type FormEvent } from \"react\";\nimport type { z } from \"zod\";\nimport { FormBlockSchema } from \"../../schema/blocks\";\nimport type { FormDefinition } from \"../../schema/forms\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { useBlockData } from \"../context\";\nimport { submitFormValues } from \"./lib/submitForm\";\nimport { isDemoActive, getDemoForm } from \"../../demo/demoStore\";\nimport { useSiteConfig } from \"../../config\";\nimport { TextInput } from \"@astryxdesign/core/TextInput\";\nimport { TextArea } from \"@astryxdesign/core/TextArea\";\nimport { Selector } from \"@astryxdesign/core/Selector\";\nimport { CheckboxInput } from \"@astryxdesign/core/CheckboxInput\";\nimport { Spinner } from \"@astryxdesign/core/Spinner\";\nimport { Button } from \"@astryxdesign/core/Button\";\nimport { VStack, Card, Heading, Text } from \"../ui\";\n\ntype FormBlk = z.infer<typeof FormBlockSchema>;\n\n/** Voorbeeldformulier voor de demo wanneer er geen specifieke definitie is geregistreerd. */\nconst DEMO_FALLBACK_FORM: FormDefinition = {\n name: \"Contact\",\n submitLabel: \"Versturen\",\n successMessage: \"Bedankt! Dit is een demo — je bericht is niet echt verstuurd.\",\n fields: [\n { name: \"naam\", label: \"Naam\", type: \"text\", required: true },\n { name: \"email\", label: \"E-mail\", type: \"email\", required: true },\n { name: \"bericht\", label: \"Bericht\", type: \"textarea\", required: false, placeholder: \"Waarmee kunnen we je helpen?\" },\n ],\n};\n\nfunction FormRender({ block, siteId, mode }: BlockRenderProps<FormBlk>) {\n const siteMode = useSiteConfig().mode;\n // Server-seed (7.12): de route leest formulierdefinities server-side (getPageBlockData) en\n // geeft ze mee als blockData. Het formulier staat dan al in de SSR-HTML en de per-bezoeker\n // `getDoc` hieronder vervalt. Zonder seed (oudere site, admin-preview) werkt het oude pad.\n const serverForms = useBlockData<Record<string, FormDefinition>>(\"forms\");\n const serverDef = serverForms?.[block.formId] ?? null;\n const [def, setDef] = useState<FormDefinition | null>(serverDef);\n const [unavailable, setUnavailable] = useState(false);\n const [values, setValues] = useState<Record<string, string | boolean>>({});\n const [hp, setHp] = useState(\"\");\n const [status, setStatus] = useState<\"idle\" | \"busy\" | \"ok\" | \"error\">(\"idle\");\n const [error, setError] = useState<string | null>(null);\n\n const demo = isDemoActive();\n // \"Lokaal\" inzenden (geen Firebase-call) bij demo OF wanneer er geen Firebase is maar wél een\n // geregistreerd voorbeeldformulier — zo werkt een statische referentiesite z'n formulier ook.\n const [localOnly, setLocalOnly] = useState(demo);\n\n useEffect(() => {\n if (demo) {\n setDef(getDemoForm(siteId, block.formId) ?? DEMO_FALLBACK_FORM);\n setLocalOnly(true);\n return;\n }\n if (serverDef) return; // definitie kwam al server-side mee\n // Firestore dynamisch: alleen een pagina die écht een formulier toont haalt de SDK binnen.\n void (async () => {\n const [{ getDb }, { doc, getDoc }] = await Promise.all([\n import(\"../../firebase/client\"),\n import(\"firebase/firestore\"),\n ]);\n const db = getDb();\n if (!db) {\n // Geen Firebase: val terug op een geregistreerd formulier, of (op een mode:\"demo\"-site) op\n // het ingebouwde voorbeeld; beide zenden lokaal in. Anders een nette \"niet beschikbaar\".\n const reg = getDemoForm(siteId, block.formId) ?? (siteMode === \"demo\" ? DEMO_FALLBACK_FORM : null);\n if (reg) { setDef(reg); setLocalOnly(true); } else { setUnavailable(true); }\n return;\n }\n try {\n const snap = await getDoc(doc(db, \"sites\", siteId, \"forms\", block.formId));\n if (snap.exists()) setDef(snap.data() as FormDefinition);\n else setUnavailable(true);\n } catch {\n setUnavailable(true);\n }\n })();\n }, [demo, siteId, block.formId, siteMode, serverDef]);\n\n if (unavailable) {\n return (\n <Card padding={4} variant=\"muted\">\n <Text>{block.heading ? `${block.heading}: ` : \"\"}neem contact met ons op — we reageren snel.</Text>\n </Card>\n );\n }\n if (!def) {\n return (\n <div style={{ display: \"grid\", placeItems: \"center\", minHeight: 120 }}>\n <Spinner label=\"Formulier laden…\" />\n </div>\n );\n }\n\n async function submit(e: FormEvent) {\n e.preventDefault();\n if (mode === \"preview\") return;\n if (localOnly) {\n // Lokaal (demo / geen backend): niets versturen, alleen de successtaat tonen.\n setStatus(\"ok\");\n setValues({});\n return;\n }\n setStatus(\"busy\");\n setError(null);\n try {\n await submitFormValues(siteId, block.formId, values, hp);\n // Optioneel doorsturen naar een bedankt-pagina (conversiemeting/advertenties).\n if (def?.successRedirect) {\n window.location.assign(def.successRedirect);\n return;\n }\n setStatus(\"ok\");\n setValues({});\n } catch {\n setStatus(\"error\");\n setError(\"Versturen mislukt. Probeer het later opnieuw.\");\n }\n }\n\n if (status === \"ok\") {\n return (\n // role=\"status\": screenreaders melden de succesboodschap wanneer het formulier verdwijnt.\n <Card padding={4} variant=\"muted\" role=\"status\">\n <Text>{def.successMessage}</Text>\n </Card>\n );\n }\n\n const setVal = (name: string, v: string | boolean) => setValues((prev) => ({ ...prev, [name]: v }));\n\n return (\n <form className=\"xicms-form\" onSubmit={submit}>\n <VStack gap={3}>\n {block.heading && <Heading level={2} data-xicms-edit=\"heading\">{block.heading}</Heading>}\n {def.fields.map((f) => {\n const sv = (values[f.name] as string) ?? \"\";\n if (f.type === \"textarea\") {\n return (\n <TextArea key={f.name} label={f.label} isRequired={f.required} placeholder={f.placeholder} rows={4}\n value={sv} onChange={(v) => setVal(f.name, v)} />\n );\n }\n if (f.type === \"select\") {\n return (\n <Selector key={f.name} label={f.label} isRequired={f.required} placeholder=\"Kies…\"\n options={f.options ?? []} value={sv} onChange={(v) => setVal(f.name, (v as string) ?? \"\")} />\n );\n }\n if (f.type === \"checkbox\") {\n return (\n <CheckboxInput key={f.name} label={f.label} isRequired={f.required}\n value={Boolean(values[f.name])} onChange={(checked) => setVal(f.name, checked)} />\n );\n }\n if (f.type === \"radio\") {\n return (\n <fieldset key={f.name} className=\"xicms-form__radios\">\n <legend>{f.label}{f.required && \" *\"}</legend>\n {(f.options ?? []).map((opt) => (\n <label key={opt} className=\"xicms-form__radio\">\n <input\n type=\"radio\"\n name={f.name}\n value={opt}\n required={f.required}\n checked={sv === opt}\n onChange={() => setVal(f.name, opt)}\n />\n {opt}\n </label>\n ))}\n </fieldset>\n );\n }\n if (f.type === \"date\" || f.type === \"number\") {\n // Astryx TextInput kent geen date/number → native input in dezelfde stijl.\n return (\n <label key={f.name} className=\"xicms-form__native\">\n <span className=\"xicms-form__native-label\">{f.label}{f.required && \" *\"}</span>\n <input\n className=\"xicms-input\"\n type={f.type}\n required={f.required}\n placeholder={f.placeholder}\n value={sv}\n onChange={(e) => setVal(f.name, e.target.value)}\n />\n </label>\n );\n }\n return (\n <TextInput key={f.name} label={f.label} type={f.type === \"email\" ? \"email\" : \"text\"}\n isRequired={f.required} placeholder={f.placeholder}\n value={sv} onChange={(v) => setVal(f.name, v)} />\n );\n })}\n {/* Honeypot — verborgen voor mensen, bots vullen 'm vaak wel in. */}\n <input\n type=\"text\"\n tabIndex={-1}\n autoComplete=\"off\"\n value={hp}\n onChange={(e) => setHp(e.target.value)}\n style={{ position: \"absolute\", left: \"-9999px\", width: 1, height: 1 }}\n aria-hidden\n />\n {error && <Text role=\"alert\" style={{ color: \"var(--color-error, #c0392b)\" }}>{error}</Text>}\n <div>\n <Button label={status === \"busy\" ? \"Versturen…\" : def.submitLabel} variant=\"primary\" size=\"lg\" type=\"submit\" isLoading={status === \"busy\"} />\n </div>\n </VStack>\n </form>\n );\n}\n\nexport const formBlock = defineBlock({\n type: \"form\",\n schema: FormBlockSchema,\n Render: FormRender,\n label: \"Formulier\",\n icon: \"✉️\",\n defaults: () => ({ formId: \"\" }),\n});\n"],"mappings":"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","names":[]}
@@ -5,32 +5,14 @@ import { SearchBlockSchema } from "../../schema/blocks.js";
5
5
  import { defineBlock } from "../registry.js";
6
6
  import { TextInput } from "@astryxdesign/core/TextInput";
7
7
  import { rankResults, highlightParts } from "./lib/searchRank.js";
8
- function collectStrings(v, parts, depth) {
9
- if (depth > 6 || v == null) return;
10
- if (typeof v === "string") {
11
- parts.push(v);
12
- return;
13
- }
14
- if (Array.isArray(v)) {
15
- for (const item of v) collectStrings(item, parts, depth + 1);
16
- return;
17
- }
18
- if (typeof v === "object") {
19
- for (const [k, val] of Object.entries(v)) {
20
- if (k.startsWith("_") || k === "url" || k === "imageUrl" || k === "href") continue;
21
- collectStrings(val, parts, depth + 1);
22
- }
23
- }
24
- }
25
- function blocksToText(blocks) {
26
- const parts = [];
27
- collectStrings(blocks, parts, 0);
28
- return parts.join(" ").replace(/<[^>]*>/g, " ");
29
- }
8
+ import { useBlockData } from "../context.js";
9
+ import { blocksToText } from "../searchText.js";
30
10
  function SearchRender({ block, siteId }) {
31
- const [entries, setEntries] = useState([]);
11
+ const serverIndex = useBlockData("searchIndex");
12
+ const [entries, setEntries] = useState(serverIndex ?? []);
32
13
  const [q, setQ] = useState("");
33
14
  useEffect(() => {
15
+ if (serverIndex) return;
34
16
  let cancelled = false;
35
17
  (async () => {
36
18
  const [{ getDb }, { collection, getDocs, query, where }] = await Promise.all([
@@ -72,7 +54,7 @@ function SearchRender({ block, siteId }) {
72
54
  return () => {
73
55
  cancelled = true;
74
56
  };
75
- }, [siteId, block.includeNews]);
57
+ }, [siteId, block.includeNews, serverIndex]);
76
58
  const [debouncedQ, setDebouncedQ] = useState("");
77
59
  useEffect(() => {
78
60
  const t = setTimeout(() => setDebouncedQ(q), 250);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/blocks/builtins/Search.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport type { z } from \"zod\";\nimport { SearchBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { TextInput } from \"@astryxdesign/core/TextInput\";\nimport { rankResults, highlightParts } from \"./lib/searchRank\";\n\ntype SearchBlock = z.infer<typeof SearchBlockSchema>;\n\ninterface Entry {\n title: string;\n path: string;\n text: string;\n}\n\n/**\n * Vlakt block-content uit tot doorzoekbare tekst. RECURSIEF: de meeste content leeft genest —\n * `cards.items[].title/text`, `faq.items[].q/a`, en alles binnen columns/withSidebar/tabs. Alleen\n * top-level strings pakken zou zoeken vrijwel leeg maken op content-rijke pagina's.\n */\nfunction collectStrings(v: unknown, parts: string[], depth: number): void {\n if (depth > 6 || v == null) return; // diepte-cap tegen pathologische structuren\n if (typeof v === \"string\") {\n parts.push(v);\n return;\n }\n if (Array.isArray(v)) {\n for (const item of v) collectStrings(item, parts, depth + 1);\n return;\n }\n if (typeof v === \"object\") {\n for (const [k, val] of Object.entries(v as Record<string, unknown>)) {\n if (k.startsWith(\"_\") || k === \"url\" || k === \"imageUrl\" || k === \"href\") continue; // meta/urls niet doorzoekbaar\n collectStrings(val, parts, depth + 1);\n }\n }\n}\nfunction blocksToText(blocks: unknown): string {\n const parts: string[] = [];\n collectStrings(blocks, parts, 0);\n return parts.join(\" \").replace(/<[^>]*>/g, \" \");\n}\n\n/** Site-zoek — leest gepubliceerde pagina's (en optioneel nieuws) en filtert client-side. */\nfunction SearchRender({ block, siteId }: BlockRenderProps<SearchBlock>) {\n const [entries, setEntries] = useState<Entry[]>([]);\n const [q, setQ] = useState(\"\");\n\n useEffect(() => {\n let cancelled = false;\n (async () => {\n // Firestore dynamisch: de zoekindex wordt pas opgebouwd wanneer het zoekblok in beeld is,\n // dus een gewone pagina downloadt de SDK niet.\n const [{ getDb }, { collection, getDocs, query, where }] = await Promise.all([\n import(\"../../firebase/client\"),\n import(\"firebase/firestore\"),\n ]);\n const db = getDb();\n if (!db || cancelled) return;\n const out: Entry[] = [];\n try {\n const pagesSnap = await getDocs(\n query(collection(db, `sites/${siteId}/pages`), where(\"status\", \"==\", \"published\")),\n );\n pagesSnap.forEach((d) => {\n const data = d.data() as Record<string, unknown>;\n out.push({\n title: String(data.title ?? \"\"),\n path: String(data.path ?? \"/\"),\n text: blocksToText(data.blocks),\n });\n });\n if (block.includeNews) {\n const newsSnap = await getDocs(\n query(collection(db, `sites/${siteId}/news`), where(\"published\", \"==\", true)),\n );\n newsSnap.forEach((d) => {\n const data = d.data() as Record<string, unknown>;\n out.push({\n title: String(data.title ?? \"\"),\n path: `/nieuws/${String(data.slug ?? d.id)}`,\n text: String(data.excerpt ?? \"\") + \" \" + String(data.body ?? \"\").replace(/<[^>]*>/g, \" \"),\n });\n });\n }\n } catch {\n /* stil: zonder leesrechten/Firebase blijft het zoekveld leeg */\n }\n if (!cancelled) setEntries(out);\n })();\n return () => {\n cancelled = true;\n };\n }, [siteId, block.includeNews]);\n\n // Debounce (250ms): niet bij elke toetsaanslag opnieuw ranken/renderen.\n const [debouncedQ, setDebouncedQ] = useState(\"\");\n useEffect(() => {\n const t = setTimeout(() => setDebouncedQ(q), 250);\n return () => clearTimeout(t);\n }, [q]);\n\n const results = useMemo(() => rankResults(debouncedQ, entries), [entries, debouncedQ]);\n\n const mark = (text: string) =>\n highlightParts(text, debouncedQ).map((p, i) => (p.hit ? <mark key={i}>{p.text}</mark> : <span key={i}>{p.text}</span>));\n\n return (\n <div className=\"xicms-search\">\n <TextInput\n label={block.placeholder}\n isLabelHidden\n placeholder={block.placeholder}\n value={q}\n onChange={setQ}\n />\n {debouncedQ.trim() && (\n <ul className=\"xicms-search__results\">\n {results.length === 0 ? (\n <li className=\"xicms-search__empty\">Niets gevonden.</li>\n ) : (\n results.map((r, i) => (\n <li key={i} className=\"xicms-search__result\">\n <a href={r.path}>{mark(r.title || r.path)}</a>\n {r.snippet && <p className=\"xicms-search__snippet\">{mark(r.snippet)}</p>}\n </li>\n ))\n )}\n </ul>\n )}\n </div>\n );\n}\n\nexport const searchBlock = defineBlock({\n type: \"search\",\n schema: SearchBlockSchema,\n Render: SearchRender,\n label: \"Zoeken\",\n icon: \"🔍\",\n defaults: () => ({ placeholder: \"Zoeken…\", includeNews: true }),\n});\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/blocks/builtins/Search.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport type { z } from \"zod\";\nimport { SearchBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { TextInput } from \"@astryxdesign/core/TextInput\";\nimport { rankResults, highlightParts } from \"./lib/searchRank\";\nimport { useBlockData } from \"../context\";\nimport { blocksToText, type SearchEntry } from \"../searchText\";\n\ntype SearchBlock = z.infer<typeof SearchBlockSchema>;\n\ntype Entry = SearchEntry;\n\n// De tekst-vlakkers (collectStrings/blocksToText) zijn gedeeld met de server-zoekindex:\n// zie ../searchText.ts — client en server moeten exact dezelfde tekst zien.\n\n/** Site-zoek — leest gepubliceerde pagina's (en optioneel nieuws) en filtert client-side. */\nfunction SearchRender({ block, siteId }: BlockRenderProps<SearchBlock>) {\n // Server-seed (7.12): de route levert de complete zoekindex als blockData mee. Dan is zoeken\n // instant én gratis — de client-fetch hieronder las anders de HELE pagina-collectie per\n // bezoeker, veruit de duurste read van het hele systeem.\n const serverIndex = useBlockData<Entry[]>(\"searchIndex\");\n const [entries, setEntries] = useState<Entry[]>(serverIndex ?? []);\n const [q, setQ] = useState(\"\");\n\n useEffect(() => {\n if (serverIndex) return;\n let cancelled = false;\n (async () => {\n // Firestore dynamisch: de zoekindex wordt pas opgebouwd wanneer het zoekblok in beeld is,\n // dus een gewone pagina downloadt de SDK niet.\n const [{ getDb }, { collection, getDocs, query, where }] = await Promise.all([\n import(\"../../firebase/client\"),\n import(\"firebase/firestore\"),\n ]);\n const db = getDb();\n if (!db || cancelled) return;\n const out: Entry[] = [];\n try {\n const pagesSnap = await getDocs(\n query(collection(db, `sites/${siteId}/pages`), where(\"status\", \"==\", \"published\")),\n );\n pagesSnap.forEach((d) => {\n const data = d.data() as Record<string, unknown>;\n out.push({\n title: String(data.title ?? \"\"),\n path: String(data.path ?? \"/\"),\n text: blocksToText(data.blocks),\n });\n });\n if (block.includeNews) {\n const newsSnap = await getDocs(\n query(collection(db, `sites/${siteId}/news`), where(\"published\", \"==\", true)),\n );\n newsSnap.forEach((d) => {\n const data = d.data() as Record<string, unknown>;\n out.push({\n title: String(data.title ?? \"\"),\n path: `/nieuws/${String(data.slug ?? d.id)}`,\n text: String(data.excerpt ?? \"\") + \" \" + String(data.body ?? \"\").replace(/<[^>]*>/g, \" \"),\n });\n });\n }\n } catch {\n /* stil: zonder leesrechten/Firebase blijft het zoekveld leeg */\n }\n if (!cancelled) setEntries(out);\n })();\n return () => {\n cancelled = true;\n };\n }, [siteId, block.includeNews, serverIndex]);\n\n // Debounce (250ms): niet bij elke toetsaanslag opnieuw ranken/renderen.\n const [debouncedQ, setDebouncedQ] = useState(\"\");\n useEffect(() => {\n const t = setTimeout(() => setDebouncedQ(q), 250);\n return () => clearTimeout(t);\n }, [q]);\n\n const results = useMemo(() => rankResults(debouncedQ, entries), [entries, debouncedQ]);\n\n const mark = (text: string) =>\n highlightParts(text, debouncedQ).map((p, i) => (p.hit ? <mark key={i}>{p.text}</mark> : <span key={i}>{p.text}</span>));\n\n return (\n <div className=\"xicms-search\">\n <TextInput\n label={block.placeholder}\n isLabelHidden\n placeholder={block.placeholder}\n value={q}\n onChange={setQ}\n />\n {debouncedQ.trim() && (\n <ul className=\"xicms-search__results\">\n {results.length === 0 ? (\n <li className=\"xicms-search__empty\">Niets gevonden.</li>\n ) : (\n results.map((r, i) => (\n <li key={i} className=\"xicms-search__result\">\n <a href={r.path}>{mark(r.title || r.path)}</a>\n {r.snippet && <p className=\"xicms-search__snippet\">{mark(r.snippet)}</p>}\n </li>\n ))\n )}\n </ul>\n )}\n </div>\n );\n}\n\nexport const searchBlock = defineBlock({\n type: \"search\",\n schema: SearchBlockSchema,\n Render: SearchRender,\n label: \"Zoeken\",\n icon: \"🔍\",\n defaults: () => ({ placeholder: \"Zoeken…\", includeNews: true }),\n});\n"],"mappings":"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","names":[]}
@@ -6,8 +6,10 @@ import { SymbolSchema } from "../../schema/symbols.js";
6
6
  import { defineBlock } from "../registry.js";
7
7
  import { BlockRenderer } from "../BlockRenderer.js";
8
8
  import { useRegistry } from "../context.js";
9
+ import { useEditor } from "../../editor/EditorProvider.js";
9
10
  function SymbolRefRender({ block, siteId, mode }) {
10
11
  const registry = useRegistry();
12
+ const bewerken = useEditor().open || mode === "preview";
11
13
  const [symbol, setSymbol] = useState(void 0);
12
14
  useEffect(() => {
13
15
  if (!block.symbolId) {
@@ -26,20 +28,32 @@ function SymbolRefRender({ block, siteId, mode }) {
26
28
  setSymbol(null);
27
29
  return;
28
30
  }
29
- unsub = onSnapshot(
30
- doc(db, "sites", siteId, "symbols", block.symbolId),
31
- (snap) => {
32
- const parsed = snap.exists() ? SymbolSchema.safeParse(snap.data()) : null;
33
- setSymbol(parsed?.success ? parsed.data : null);
34
- },
35
- () => setSymbol(null)
36
- );
31
+ const ref = doc(db, "sites", siteId, "symbols", block.symbolId);
32
+ const zet = (data, bestaat) => {
33
+ const parsed = bestaat ? SymbolSchema.safeParse(data) : null;
34
+ setSymbol(parsed?.success ? parsed.data : null);
35
+ };
36
+ if (bewerken) {
37
+ unsub = onSnapshot(
38
+ ref,
39
+ (snap) => zet(snap.data(), snap.exists()),
40
+ () => setSymbol(null)
41
+ );
42
+ } else {
43
+ const { getDoc } = await import("firebase/firestore");
44
+ try {
45
+ const snap = await getDoc(ref);
46
+ if (!afgebroken) zet(snap.data(), snap.exists());
47
+ } catch {
48
+ if (!afgebroken) setSymbol(null);
49
+ }
50
+ }
37
51
  })();
38
52
  return () => {
39
53
  afgebroken = true;
40
54
  unsub?.();
41
55
  };
42
- }, [siteId, block.symbolId]);
56
+ }, [siteId, block.symbolId, bewerken]);
43
57
  if (symbol === void 0) return null;
44
58
  if (symbol === null) {
45
59
  return mode === "preview" ? /* @__PURE__ */ jsxs("p", { className: "xicms-ve__hint", children: [
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/blocks/builtins/SymbolRef.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport type { z } from \"zod\";\nimport { SymbolRefBlockSchema } from \"../../schema/blocks\";\nimport { SymbolSchema, type SymbolDefinition } from \"../../schema/symbols\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { BlockRenderer } from \"../BlockRenderer\";\nimport { useRegistry } from \"../context\";\n\ntype SymbolRef = z.infer<typeof SymbolRefBlockSchema>;\n\n/**\n * Verwijzing naar een gedeeld symbool. Op de LIVE site wordt een symbolRef normaliter al\n * server-side vervangen door de symbool-blokken (server/pages.ts `expandSymbols`) — dan komt\n * deze Render niet eens aan bod. In de editor/preview (en als client-fallback) rendert hij het\n * symbool live vanuit Firestore, met een subtiel kader zodat de editor ziet dat dit gedeelde\n * inhoud is die op meerdere pagina's meebeweegt.\n */\nfunction SymbolRefRender({ block, siteId, mode }: BlockRenderProps<SymbolRef>) {\n const registry = useRegistry();\n const [symbol, setSymbol] = useState<SymbolDefinition | null | undefined>(undefined);\n useEffect(() => {\n if (!block.symbolId) {\n setSymbol(null);\n return;\n }\n // Firestore dynamisch laden: een pagina zonder symbolen haalt de SDK zo nooit binnen, en een\n // pagina mét symbolen pas op het moment dat er echt iets op te halen valt.\n let unsub: (() => void) | undefined;\n let afgebroken = false;\n void (async () => {\n const [{ getDb }, { doc, onSnapshot }] = await Promise.all([\n import(\"../../firebase/client\"),\n import(\"firebase/firestore\"),\n ]);\n const db = getDb();\n if (!db || afgebroken) {\n setSymbol(null);\n return;\n }\n unsub = onSnapshot(\n doc(db, \"sites\", siteId, \"symbols\", block.symbolId!),\n (snap) => {\n const parsed = snap.exists() ? SymbolSchema.safeParse(snap.data()) : null;\n setSymbol(parsed?.success ? parsed.data : null);\n },\n () => setSymbol(null),\n );\n })();\n return () => {\n afgebroken = true;\n unsub?.();\n };\n }, [siteId, block.symbolId]);\n\n if (symbol === undefined) return null; // laden — geen flits\n if (symbol === null) {\n // Verweesde referentie: nooit de pagina breken; in preview uitleggen, live niets tonen.\n return mode === \"preview\" ? (\n <p className=\"xicms-ve__hint\">🧩 Symbool niet gevonden ({block.symbolId}) — kies een ander symbool of verwijder dit blok.</p>\n ) : null;\n }\n const inner = <BlockRenderer blocks={symbol.blocks} registry={registry} siteId={siteId} mode={mode} />;\n if (mode === \"preview\") {\n return (\n <div className=\"xicms-symbol\" data-symbol={block.symbolId}>\n <div className=\"xicms-symbol__badge\">🧩 {symbol.name} — gedeeld symbool (wijzigingen gelden op élke pagina)</div>\n {inner}\n </div>\n );\n }\n return inner;\n}\n\nexport const symbolRefBlock = defineBlock({\n type: \"symbolRef\",\n schema: SymbolRefBlockSchema,\n Render: SymbolRefRender,\n label: \"Symbool (gedeelde sectie)\",\n icon: \"🧩\",\n defaults: () => ({ symbolId: \"\" }),\n});\n"],"mappings":";AA4DM,SAGU,KAHV;AA1DN,SAAS,WAAW,gBAAgB;AAEpC,SAAS,4BAA4B;AACrC,SAAS,oBAA2C;AACpD,SAAS,mBAA0C;AACnD,SAAS,qBAAqB;AAC9B,SAAS,mBAAmB;AAW5B,SAAS,gBAAgB,EAAE,OAAO,QAAQ,KAAK,GAAgC;AAC7E,QAAM,WAAW,YAAY;AAC7B,QAAM,CAAC,QAAQ,SAAS,IAAI,SAA8C,MAAS;AACnF,YAAU,MAAM;AACd,QAAI,CAAC,MAAM,UAAU;AACnB,gBAAU,IAAI;AACd;AAAA,IACF;AAGA,QAAI;AACJ,QAAI,aAAa;AACjB,UAAM,YAAY;AAChB,YAAM,CAAC,EAAE,MAAM,GAAG,EAAE,KAAK,WAAW,CAAC,IAAI,MAAM,QAAQ,IAAI;AAAA,QACzD,OAAO,uBAAuB;AAAA,QAC9B,OAAO,oBAAoB;AAAA,MAC7B,CAAC;AACD,YAAM,KAAK,MAAM;AACjB,UAAI,CAAC,MAAM,YAAY;AACrB,kBAAU,IAAI;AACd;AAAA,MACF;AACA,cAAQ;AAAA,QACN,IAAI,IAAI,SAAS,QAAQ,WAAW,MAAM,QAAS;AAAA,QACnD,CAAC,SAAS;AACR,gBAAM,SAAS,KAAK,OAAO,IAAI,aAAa,UAAU,KAAK,KAAK,CAAC,IAAI;AACrE,oBAAU,QAAQ,UAAU,OAAO,OAAO,IAAI;AAAA,QAChD;AAAA,QACA,MAAM,UAAU,IAAI;AAAA,MACtB;AAAA,IACF,GAAG;AACH,WAAO,MAAM;AACX,mBAAa;AACb,cAAQ;AAAA,IACV;AAAA,EACF,GAAG,CAAC,QAAQ,MAAM,QAAQ,CAAC;AAE3B,MAAI,WAAW,OAAW,QAAO;AACjC,MAAI,WAAW,MAAM;AAEnB,WAAO,SAAS,YACd,qBAAC,OAAE,WAAU,kBAAiB;AAAA;AAAA,MAA2B,MAAM;AAAA,MAAS;AAAA,OAAiD,IACvH;AAAA,EACN;AACA,QAAM,QAAQ,oBAAC,iBAAc,QAAQ,OAAO,QAAQ,UAAoB,QAAgB,MAAY;AACpG,MAAI,SAAS,WAAW;AACtB,WACE,qBAAC,SAAI,WAAU,gBAAe,eAAa,MAAM,UAC/C;AAAA,2BAAC,SAAI,WAAU,uBAAsB;AAAA;AAAA,QAAI,OAAO;AAAA,QAAK;AAAA,SAAsD;AAAA,MAC1G;AAAA,OACH;AAAA,EAEJ;AACA,SAAO;AACT;AAEO,MAAM,iBAAiB,YAAY;AAAA,EACxC,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,MAAM;AAAA,EACN,UAAU,OAAO,EAAE,UAAU,GAAG;AAClC,CAAC;","names":[]}
1
+ {"version":3,"sources":["../../../src/blocks/builtins/SymbolRef.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport type { z } from \"zod\";\nimport { SymbolRefBlockSchema } from \"../../schema/blocks\";\nimport { SymbolSchema, type SymbolDefinition } from \"../../schema/symbols\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { BlockRenderer } from \"../BlockRenderer\";\nimport { useRegistry } from \"../context\";\nimport { useEditor } from \"../../editor/EditorProvider\";\n\ntype SymbolRef = z.infer<typeof SymbolRefBlockSchema>;\n\n/**\n * Verwijzing naar een gedeeld symbool. Op de LIVE site wordt een symbolRef normaliter al\n * server-side vervangen door de symbool-blokken (server/pages.ts `expandSymbols`) — dan komt\n * deze Render niet eens aan bod. In de editor/preview (en als client-fallback) rendert hij het\n * symbool live vanuit Firestore, met een subtiel kader zodat de editor ziet dat dit gedeelde\n * inhoud is die op meerdere pagina's meebeweegt.\n */\nfunction SymbolRefRender({ block, siteId, mode }: BlockRenderProps<SymbolRef>) {\n const registry = useRegistry();\n // Live meebewegen is alleen nuttig terwijl er BEWERKT wordt. Voor een gewone bezoeker was de\n // onSnapshot hieronder het duurste patroon van het systeem: een read plus een blijvend open\n // verbinding, per bezoeker — voor content die toch pas verandert als iemand publiceert.\n const bewerken = useEditor().open || mode === \"preview\";\n const [symbol, setSymbol] = useState<SymbolDefinition | null | undefined>(undefined);\n useEffect(() => {\n if (!block.symbolId) {\n setSymbol(null);\n return;\n }\n // Firestore dynamisch laden: een pagina zonder symbolen haalt de SDK zo nooit binnen, en een\n // pagina mét symbolen pas op het moment dat er echt iets op te halen valt.\n let unsub: (() => void) | undefined;\n let afgebroken = false;\n void (async () => {\n const [{ getDb }, { doc, onSnapshot }] = await Promise.all([\n import(\"../../firebase/client\"),\n import(\"firebase/firestore\"),\n ]);\n const db = getDb();\n if (!db || afgebroken) {\n setSymbol(null);\n return;\n }\n const ref = doc(db, \"sites\", siteId, \"symbols\", block.symbolId!);\n const zet = (data: unknown, bestaat: boolean) => {\n const parsed = bestaat ? SymbolSchema.safeParse(data) : null;\n setSymbol(parsed?.success ? parsed.data : null);\n };\n if (bewerken) {\n unsub = onSnapshot(\n ref,\n (snap) => zet(snap.data(), snap.exists()),\n () => setSymbol(null),\n );\n } else {\n // Publieke fallback (zeldzaam: normaal is de ref al server-side geëxpandeerd): één read,\n // geen listener.\n const { getDoc } = await import(\"firebase/firestore\");\n try {\n const snap = await getDoc(ref);\n if (!afgebroken) zet(snap.data(), snap.exists());\n } catch {\n if (!afgebroken) setSymbol(null);\n }\n }\n })();\n return () => {\n afgebroken = true;\n unsub?.();\n };\n }, [siteId, block.symbolId, bewerken]);\n\n if (symbol === undefined) return null; // laden — geen flits\n if (symbol === null) {\n // Verweesde referentie: nooit de pagina breken; in preview uitleggen, live niets tonen.\n return mode === \"preview\" ? (\n <p className=\"xicms-ve__hint\">🧩 Symbool niet gevonden ({block.symbolId}) — kies een ander symbool of verwijder dit blok.</p>\n ) : null;\n }\n const inner = <BlockRenderer blocks={symbol.blocks} registry={registry} siteId={siteId} mode={mode} />;\n if (mode === \"preview\") {\n return (\n <div className=\"xicms-symbol\" data-symbol={block.symbolId}>\n <div className=\"xicms-symbol__badge\">🧩 {symbol.name} — gedeeld symbool (wijzigingen gelden op élke pagina)</div>\n {inner}\n </div>\n );\n }\n return inner;\n}\n\nexport const symbolRefBlock = defineBlock({\n type: \"symbolRef\",\n schema: SymbolRefBlockSchema,\n Render: SymbolRefRender,\n label: \"Symbool (gedeelde sectie)\",\n icon: \"🧩\",\n defaults: () => ({ symbolId: \"\" }),\n});\n"],"mappings":";AA+EM,SAGU,KAHV;AA7EN,SAAS,WAAW,gBAAgB;AAEpC,SAAS,4BAA4B;AACrC,SAAS,oBAA2C;AACpD,SAAS,mBAA0C;AACnD,SAAS,qBAAqB;AAC9B,SAAS,mBAAmB;AAC5B,SAAS,iBAAiB;AAW1B,SAAS,gBAAgB,EAAE,OAAO,QAAQ,KAAK,GAAgC;AAC7E,QAAM,WAAW,YAAY;AAI7B,QAAM,WAAW,UAAU,EAAE,QAAQ,SAAS;AAC9C,QAAM,CAAC,QAAQ,SAAS,IAAI,SAA8C,MAAS;AACnF,YAAU,MAAM;AACd,QAAI,CAAC,MAAM,UAAU;AACnB,gBAAU,IAAI;AACd;AAAA,IACF;AAGA,QAAI;AACJ,QAAI,aAAa;AACjB,UAAM,YAAY;AAChB,YAAM,CAAC,EAAE,MAAM,GAAG,EAAE,KAAK,WAAW,CAAC,IAAI,MAAM,QAAQ,IAAI;AAAA,QACzD,OAAO,uBAAuB;AAAA,QAC9B,OAAO,oBAAoB;AAAA,MAC7B,CAAC;AACD,YAAM,KAAK,MAAM;AACjB,UAAI,CAAC,MAAM,YAAY;AACrB,kBAAU,IAAI;AACd;AAAA,MACF;AACA,YAAM,MAAM,IAAI,IAAI,SAAS,QAAQ,WAAW,MAAM,QAAS;AAC/D,YAAM,MAAM,CAAC,MAAe,YAAqB;AAC/C,cAAM,SAAS,UAAU,aAAa,UAAU,IAAI,IAAI;AACxD,kBAAU,QAAQ,UAAU,OAAO,OAAO,IAAI;AAAA,MAChD;AACA,UAAI,UAAU;AACZ,gBAAQ;AAAA,UACN;AAAA,UACA,CAAC,SAAS,IAAI,KAAK,KAAK,GAAG,KAAK,OAAO,CAAC;AAAA,UACxC,MAAM,UAAU,IAAI;AAAA,QACtB;AAAA,MACF,OAAO;AAGL,cAAM,EAAE,OAAO,IAAI,MAAM,OAAO,oBAAoB;AACpD,YAAI;AACF,gBAAM,OAAO,MAAM,OAAO,GAAG;AAC7B,cAAI,CAAC,WAAY,KAAI,KAAK,KAAK,GAAG,KAAK,OAAO,CAAC;AAAA,QACjD,QAAQ;AACN,cAAI,CAAC,WAAY,WAAU,IAAI;AAAA,QACjC;AAAA,MACF;AAAA,IACF,GAAG;AACH,WAAO,MAAM;AACX,mBAAa;AACb,cAAQ;AAAA,IACV;AAAA,EACF,GAAG,CAAC,QAAQ,MAAM,UAAU,QAAQ,CAAC;AAErC,MAAI,WAAW,OAAW,QAAO;AACjC,MAAI,WAAW,MAAM;AAEnB,WAAO,SAAS,YACd,qBAAC,OAAE,WAAU,kBAAiB;AAAA;AAAA,MAA2B,MAAM;AAAA,MAAS;AAAA,OAAiD,IACvH;AAAA,EACN;AACA,QAAM,QAAQ,oBAAC,iBAAc,QAAQ,OAAO,QAAQ,UAAoB,QAAgB,MAAY;AACpG,MAAI,SAAS,WAAW;AACtB,WACE,qBAAC,SAAI,WAAU,gBAAe,eAAa,MAAM,UAC/C;AAAA,2BAAC,SAAI,WAAU,uBAAsB;AAAA;AAAA,QAAI,OAAO;AAAA,QAAK;AAAA,SAAsD;AAAA,MAC1G;AAAA,OACH;AAAA,EAEJ;AACA,SAAO;AACT;AAEO,MAAM,iBAAiB,YAAY;AAAA,EACxC,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,MAAM;AAAA,EACN,UAAU,OAAO,EAAE,UAAU,GAAG;AAClC,CAAC;","names":[]}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Pagina-blokken → doorzoekbare platte tekst. Pure functies, bewust zonder "use client" of
3
+ * "server-only": het zoekblok (client) en de server-zoekindex (server/blockData.ts) moeten
4
+ * EXACT dezelfde tekst zien, anders vindt de een wat de ander mist.
5
+ */
6
+ /** Eén regel in de zoekindex — de vorm die het zoekblok rankt en toont. */
7
+ export interface SearchEntry {
8
+ title: string;
9
+ path: string;
10
+ text: string;
11
+ }
12
+ /**
13
+ * Verzamel alle mens-leesbare strings, recursief: `cards.items[].title/text`, `faq.items[].q/a`,
14
+ * en alles binnen columns/withSidebar/tabs. Alleen top-level strings pakken zou zoeken vrijwel
15
+ * leeg maken op content-rijke pagina's.
16
+ */
17
+ export declare function collectStrings(v: unknown, parts: string[], depth: number): void;
18
+ /** Blokken → één platte tekst (HTML-tags gestript). */
19
+ export declare function blocksToText(blocks: unknown): string;
@@ -0,0 +1,27 @@
1
+ function collectStrings(v, parts, depth) {
2
+ if (depth > 6 || v == null) return;
3
+ if (typeof v === "string") {
4
+ parts.push(v);
5
+ return;
6
+ }
7
+ if (Array.isArray(v)) {
8
+ for (const item of v) collectStrings(item, parts, depth + 1);
9
+ return;
10
+ }
11
+ if (typeof v === "object") {
12
+ for (const [k, val] of Object.entries(v)) {
13
+ if (k.startsWith("_") || k === "url" || k === "imageUrl" || k === "href") continue;
14
+ collectStrings(val, parts, depth + 1);
15
+ }
16
+ }
17
+ }
18
+ function blocksToText(blocks) {
19
+ const parts = [];
20
+ collectStrings(blocks, parts, 0);
21
+ return parts.join(" ").replace(/<[^>]*>/g, " ");
22
+ }
23
+ export {
24
+ blocksToText,
25
+ collectStrings
26
+ };
27
+ //# sourceMappingURL=searchText.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/blocks/searchText.ts"],"sourcesContent":["/**\n * Pagina-blokken → doorzoekbare platte tekst. Pure functies, bewust zonder \"use client\" of\n * \"server-only\": het zoekblok (client) en de server-zoekindex (server/blockData.ts) moeten\n * EXACT dezelfde tekst zien, anders vindt de een wat de ander mist.\n */\n\n/** Eén regel in de zoekindex — de vorm die het zoekblok rankt en toont. */\nexport interface SearchEntry {\n title: string;\n path: string;\n text: string;\n}\n\n/**\n * Verzamel alle mens-leesbare strings, recursief: `cards.items[].title/text`, `faq.items[].q/a`,\n * en alles binnen columns/withSidebar/tabs. Alleen top-level strings pakken zou zoeken vrijwel\n * leeg maken op content-rijke pagina's.\n */\nexport function collectStrings(v: unknown, parts: string[], depth: number): void {\n if (depth > 6 || v == null) return; // diepte-cap tegen pathologische structuren\n if (typeof v === \"string\") {\n parts.push(v);\n return;\n }\n if (Array.isArray(v)) {\n for (const item of v) collectStrings(item, parts, depth + 1);\n return;\n }\n if (typeof v === \"object\") {\n for (const [k, val] of Object.entries(v as Record<string, unknown>)) {\n if (k.startsWith(\"_\") || k === \"url\" || k === \"imageUrl\" || k === \"href\") continue; // meta/urls niet doorzoekbaar\n collectStrings(val, parts, depth + 1);\n }\n }\n}\n\n/** Blokken → één platte tekst (HTML-tags gestript). */\nexport function blocksToText(blocks: unknown): string {\n const parts: string[] = [];\n collectStrings(blocks, parts, 0);\n return parts.join(\" \").replace(/<[^>]*>/g, \" \");\n}\n"],"mappings":"AAkBO,SAAS,eAAe,GAAY,OAAiB,OAAqB;AAC/E,MAAI,QAAQ,KAAK,KAAK,KAAM;AAC5B,MAAI,OAAO,MAAM,UAAU;AACzB,UAAM,KAAK,CAAC;AACZ;AAAA,EACF;AACA,MAAI,MAAM,QAAQ,CAAC,GAAG;AACpB,eAAW,QAAQ,EAAG,gBAAe,MAAM,OAAO,QAAQ,CAAC;AAC3D;AAAA,EACF;AACA,MAAI,OAAO,MAAM,UAAU;AACzB,eAAW,CAAC,GAAG,GAAG,KAAK,OAAO,QAAQ,CAA4B,GAAG;AACnE,UAAI,EAAE,WAAW,GAAG,KAAK,MAAM,SAAS,MAAM,cAAc,MAAM,OAAQ;AAC1E,qBAAe,KAAK,OAAO,QAAQ,CAAC;AAAA,IACtC;AAAA,EACF;AACF;AAGO,SAAS,aAAa,QAAyB;AACpD,QAAM,QAAkB,CAAC;AACzB,iBAAe,QAAQ,OAAO,CAAC;AAC/B,SAAO,MAAM,KAAK,GAAG,EAAE,QAAQ,YAAY,GAAG;AAChD;","names":[]}
@@ -0,0 +1,17 @@
1
+ import "server-only";
2
+ import type { Page } from "../schema/pages.js";
3
+ export interface PageBlockDataOptions {
4
+ /** `searchParams` van de route — alleen nodig voor productdetailpagina's (`?p=<slug>`). */
5
+ searchParams?: {
6
+ p?: string | string[];
7
+ [k: string]: unknown;
8
+ };
9
+ /** De taal van de render — de zoekindex indexeert dan de vertaalde paden. (Nog ongebruikt.) */
10
+ locale?: string;
11
+ }
12
+ /**
13
+ * Server-side blockData voor één pagina, op basis van zijn bloktypes.
14
+ * Geef het resultaat door als `<PublishedPage blockData={...}>`. `undefined`-namespaces blijven
15
+ * weg zodat de blokken hun eigen fallback houden waar deze functie niets voor verzamelde.
16
+ */
17
+ export declare function getPageBlockData(siteId: string, page: Pick<Page, "blocks">, opts?: PageBlockDataOptions): Promise<Record<string, unknown> | undefined>;
@@ -0,0 +1,93 @@
1
+ import "server-only";
2
+ import { listPublishedPages, listPublishedNews, listFormDefinitions } from "./pages.js";
3
+ import { blocksToText } from "../blocks/searchText.js";
4
+ const SHOP_TYPES = /* @__PURE__ */ new Set(["productGrid", "shopCategories", "productDetail", "cart", "checkout", "donation", "orderStatus"]);
5
+ const NEWS_TYPES = /* @__PURE__ */ new Set(["newsList", "newsWidget"]);
6
+ function collectTypes(v, out, depth) {
7
+ if (depth > 6 || v == null || typeof v !== "object") return;
8
+ if (Array.isArray(v)) {
9
+ for (const item of v) collectTypes(item, out, depth + 1);
10
+ return;
11
+ }
12
+ const o = v;
13
+ if (typeof o._type === "string") out.add(o._type);
14
+ for (const val of Object.values(o)) collectTypes(val, out, depth + 1);
15
+ }
16
+ function collectFormIds(v, out, depth) {
17
+ if (depth > 6 || v == null || typeof v !== "object") return;
18
+ if (Array.isArray(v)) {
19
+ for (const item of v) collectFormIds(item, out, depth + 1);
20
+ return;
21
+ }
22
+ const o = v;
23
+ if (o._type === "form" && typeof o.formId === "string" && o.formId) out.add(o.formId);
24
+ for (const val of Object.values(o)) collectFormIds(val, out, depth + 1);
25
+ }
26
+ function searchWantsNews(v, depth) {
27
+ if (depth > 6 || v == null || typeof v !== "object") return false;
28
+ if (Array.isArray(v)) return v.some((item) => searchWantsNews(item, depth + 1));
29
+ const o = v;
30
+ if (o._type === "search" && o.includeNews === true) return true;
31
+ return Object.values(o).some((val) => searchWantsNews(val, depth + 1));
32
+ }
33
+ async function getPageBlockData(siteId, page, opts) {
34
+ const types = /* @__PURE__ */ new Set();
35
+ collectTypes(page.blocks ?? [], types, 0);
36
+ const wilShop = [...SHOP_TYPES].some((t) => types.has(t));
37
+ const wilNews = [...NEWS_TYPES].some((t) => types.has(t));
38
+ const wilSearch = types.has("search");
39
+ const formIds = /* @__PURE__ */ new Set();
40
+ collectFormIds(page.blocks ?? [], formIds, 0);
41
+ if (!wilShop && !wilNews && !wilSearch && formIds.size === 0) return void 0;
42
+ const data = {};
43
+ const zacht = async (naam, fn) => {
44
+ try {
45
+ await fn();
46
+ } catch (e) {
47
+ console.warn(`[xicms] blockData-seed "${naam}" overgeslagen:`, e?.message ?? e);
48
+ }
49
+ };
50
+ await Promise.all([
51
+ zacht("shop", async () => {
52
+ if (!wilShop) return;
53
+ const { getShopPageData } = await import("./shop.js");
54
+ data.shop = await getShopPageData(siteId, opts?.searchParams);
55
+ }),
56
+ zacht("news", async () => {
57
+ if (!wilNews) return;
58
+ data.news = await listPublishedNews(siteId);
59
+ }),
60
+ zacht("forms", async () => {
61
+ if (formIds.size === 0) return;
62
+ const alle = await listFormDefinitions(siteId);
63
+ const map = {};
64
+ for (const f of alle) if (formIds.has(f.id)) map[f.id] = f;
65
+ data.forms = map;
66
+ }),
67
+ zacht("search", async () => {
68
+ if (!wilSearch) return;
69
+ const [pages, news] = await Promise.all([
70
+ listPublishedPages(siteId),
71
+ searchWantsNews(page.blocks ?? [], 0) ? listPublishedNews(siteId) : Promise.resolve([])
72
+ ]);
73
+ const index = pages.map((p) => ({
74
+ title: String(p.title ?? ""),
75
+ path: String(p.path ?? "/"),
76
+ text: blocksToText(p.blocks)
77
+ }));
78
+ for (const n of news) {
79
+ index.push({
80
+ title: n.title,
81
+ path: `/nieuws/${n.slug || n.id}`,
82
+ text: `${n.excerpt ?? ""} ${(n.bodyHtml ?? n.body ?? "").replace(/<[^>]*>/g, " ")}`
83
+ });
84
+ }
85
+ data.searchIndex = index;
86
+ })
87
+ ]);
88
+ return data;
89
+ }
90
+ export {
91
+ getPageBlockData
92
+ };
93
+ //# sourceMappingURL=blockData.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/blockData.ts"],"sourcesContent":["import \"server-only\";\n\nimport type { Page } from \"../schema/pages\";\nimport { listPublishedPages, listPublishedNews, listFormDefinitions } from \"./pages\";\nimport { blocksToText, type SearchEntry } from \"../blocks/searchText\";\n\n/**\n * SSR-seeds voor blokken die data buiten de pagina zelf nodig hebben (7.12).\n *\n * Het duurste patroon in XiCMS was niet een gemiste servercache maar de BROWSER: blokken als de\n * productgrid, het zoekblok en het formulier haalden hun data client-side uit Firestore — per\n * bezoeker, buiten elke cache om. Eén winkelpagina kostte zo honderden reads per bezoeker, en het\n * zoekblok las de complete pagina-collectie. De server-readers bestonden (server/shop.ts), maar\n * elke site moest er zelf aan denken ze aan te roepen — en geen enkele deed dat. SITE-AUDIT noemt\n * ontbrekende SSR-seeds \"de #1 prestatieval\"; dit bestand maakt die val structureel onmogelijk.\n *\n * `getPageBlockData` kijkt naar de blóktypes van de pagina en verzamelt precies de data die deze\n * pagina nodig heeft — niets meer. Een pagina zonder shop-blokken doet nul product-reads; een\n * pagina zonder zoekblok bouwt geen index. Alle onderliggende readers zitten in de tagged\n * servercache (server/cached.ts), dus dit kost Firestore éénmaal per interval, niet per bezoeker.\n *\n * Gebruik in de catch-all van de klantsite (ná symbol-expansie, zodat blokken ín symbolen meetellen):\n *\n * const page = await getPublishedPageByPathExpanded(siteId, path, locale);\n * const blockData = page ? await getPageBlockData(siteId, page, { searchParams }) : undefined;\n * <PublishedPage page={page} siteId={siteId} blockData={blockData} />\n *\n * De blokken zelf houden hun client-fallback: een site die dit (nog) niet doorgeeft blijft werken,\n * alleen duurder en trager — precies het oude gedrag.\n */\n\n/** Bloktypes die de shop-seed nodig hebben (productDetail leest daarnaast `?p=` voor het product). */\nconst SHOP_TYPES = new Set([\"productGrid\", \"shopCategories\", \"productDetail\", \"cart\", \"checkout\", \"donation\", \"orderStatus\"]);\n/** Bloktypes die de nieuws-seed nodig hebben. */\nconst NEWS_TYPES = new Set([\"newsList\", \"newsWidget\"]);\n\n/** Verzamel alle `_type`-waarden, recursief — kolommen/tabs/secties kunnen blokken bevatten. */\nfunction collectTypes(v: unknown, out: Set<string>, depth: number): void {\n if (depth > 6 || v == null || typeof v !== \"object\") return;\n if (Array.isArray(v)) {\n for (const item of v) collectTypes(item, out, depth + 1);\n return;\n }\n const o = v as Record<string, unknown>;\n if (typeof o._type === \"string\") out.add(o._type);\n for (const val of Object.values(o)) collectTypes(val, out, depth + 1);\n}\n\n/** Verzamel de `formId`s van alle formulierblokken (ook genest). */\nfunction collectFormIds(v: unknown, out: Set<string>, depth: number): void {\n if (depth > 6 || v == null || typeof v !== \"object\") return;\n if (Array.isArray(v)) {\n for (const item of v) collectFormIds(item, out, depth + 1);\n return;\n }\n const o = v as Record<string, unknown>;\n if (o._type === \"form\" && typeof o.formId === \"string\" && o.formId) out.add(o.formId);\n for (const val of Object.values(o)) collectFormIds(val, out, depth + 1);\n}\n\n/** Wil één van de zoekblokken op de pagina ook nieuws doorzoeken? */\nfunction searchWantsNews(v: unknown, depth: number): boolean {\n if (depth > 6 || v == null || typeof v !== \"object\") return false;\n if (Array.isArray(v)) return v.some((item) => searchWantsNews(item, depth + 1));\n const o = v as Record<string, unknown>;\n if (o._type === \"search\" && o.includeNews === true) return true;\n return Object.values(o).some((val) => searchWantsNews(val, depth + 1));\n}\n\nexport interface PageBlockDataOptions {\n /** `searchParams` van de route — alleen nodig voor productdetailpagina's (`?p=<slug>`). */\n searchParams?: { p?: string | string[]; [k: string]: unknown };\n /** De taal van de render — de zoekindex indexeert dan de vertaalde paden. (Nog ongebruikt.) */\n locale?: string;\n}\n\n/**\n * Server-side blockData voor één pagina, op basis van zijn bloktypes.\n * Geef het resultaat door als `<PublishedPage blockData={...}>`. `undefined`-namespaces blijven\n * weg zodat de blokken hun eigen fallback houden waar deze functie niets voor verzamelde.\n */\nexport async function getPageBlockData(\n siteId: string,\n page: Pick<Page, \"blocks\">,\n opts?: PageBlockDataOptions,\n): Promise<Record<string, unknown> | undefined> {\n const types = new Set<string>();\n collectTypes(page.blocks ?? [], types, 0);\n\n const wilShop = [...SHOP_TYPES].some((t) => types.has(t));\n const wilNews = [...NEWS_TYPES].some((t) => types.has(t));\n const wilSearch = types.has(\"search\");\n const formIds = new Set<string>();\n collectFormIds(page.blocks ?? [], formIds, 0);\n\n if (!wilShop && !wilNews && !wilSearch && formIds.size === 0) return undefined;\n\n const data: Record<string, unknown> = {};\n\n // Elke seed faalt ZACHT: bij een fout blijft die namespace weg en neemt de client-fallback van\n // het blok het over — hetzelfde gedrag als een site die (nog) geen seeds doorgeeft. De readers\n // gooien bewust bij een hik (uitkomsten worden gecached, fouten niet — zie server/cached.ts),\n // dus hier opvangen is wat \"één mislukte read\" scheidt van \"één mislukte pagina\".\n const zacht = async (naam: string, fn: () => Promise<void>) => {\n try {\n await fn();\n } catch (e) {\n console.warn(`[xicms] blockData-seed \"${naam}\" overgeslagen:`, (e as Error)?.message ?? e);\n }\n };\n\n // Parallel: elke seed leest uit de tagged servercache, dus dit zijn meestal nul echte reads.\n await Promise.all([\n zacht(\"shop\", async () => {\n if (!wilShop) return;\n // Dynamische import: shop-servercode alleen in de graph van pagina's/sites die hem gebruiken.\n const { getShopPageData } = await import(\"./shop\");\n data.shop = await getShopPageData(siteId, opts?.searchParams);\n }),\n zacht(\"news\", async () => {\n if (!wilNews) return;\n data.news = await listPublishedNews(siteId);\n }),\n zacht(\"forms\", async () => {\n if (formIds.size === 0) return;\n const alle = await listFormDefinitions(siteId);\n const map: Record<string, unknown> = {};\n for (const f of alle) if (formIds.has(f.id)) map[f.id] = f;\n data.forms = map;\n }),\n zacht(\"search\", async () => {\n if (!wilSearch) return;\n const [pages, news] = await Promise.all([\n listPublishedPages(siteId),\n searchWantsNews(page.blocks ?? [], 0) ? listPublishedNews(siteId) : Promise.resolve([]),\n ]);\n const index: SearchEntry[] = pages.map((p) => ({\n title: String(p.title ?? \"\"),\n path: String(p.path ?? \"/\"),\n text: blocksToText(p.blocks),\n }));\n for (const n of news) {\n index.push({\n title: n.title,\n path: `/nieuws/${n.slug || n.id}`,\n text: `${n.excerpt ?? \"\"} ${(n.bodyHtml ?? n.body ?? \"\").replace(/<[^>]*>/g, \" \")}`,\n });\n }\n data.searchIndex = index;\n }),\n ]);\n\n return data;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,2 @@
1
+ import "server-only";
2
+ export declare function cachedRead<T>(keyParts: string[], tags: string[], fn: () => Promise<T>): Promise<T>;
@@ -0,0 +1,10 @@
1
+ import "server-only";
2
+ import { unstable_cache } from "next/cache";
3
+ const VANGNET = Number(process.env.XICMS_ISR_FALLBACK) || 3600;
4
+ function cachedRead(keyParts, tags, fn) {
5
+ return unstable_cache(fn, keyParts, { tags, revalidate: VANGNET })();
6
+ }
7
+ export {
8
+ cachedRead
9
+ };
10
+ //# sourceMappingURL=cached.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/cached.ts"],"sourcesContent":["import \"server-only\";\n\nimport { unstable_cache } from \"next/cache\";\n\n/**\n * Tagged servercache om ELKE publieke Firestore-read (7.12).\n *\n * Waarom dit bestaat: de Next-datacache dekte maar één van onze leespaden. Firestore-REST-queries\n * zijn POST-fetches en die cachet Next niet; Admin-SDK-reads zijn geen fetch en cachen dus ook\n * niet. Het enige dat de kosten dempte was route-level ISR — een botte timer die élke render\n * opnieuw liet lezen, en waarop `revalidateTag` maar half werkte. Gevolg: de hele ping-\n * infrastructuur (functions → /api/revalidate → tags) bestond, maar luisterde alleen op het\n * settings-GET-pad.\n *\n * `unstable_cache` legt de uitkomst van de reader zelf in de servercache, mét onze tags. Daarmee:\n * - werken de bestaande publicatie-pings op élk leespad (Admin én REST), binnen seconden;\n * - kost een read Firestore éénmaal per interval in plaats van éénmaal per render;\n * - mag het route-vangnet omhoog (3600), want versheid komt van de push, niet van de timer.\n *\n * `keyParts` moet de read uniek identificeren (siteId + wat er gelezen wordt): de binnenfunctie is\n * een closure zonder argumenten, dus de sleutel komt volledig uit `keyParts`.\n *\n * NIET gebruiken voor: drafts, preview (`skipBundle`), of iets wat per gebruiker verschilt —\n * dit is een gedeelde cache voor publiek-identieke data.\n */\nconst VANGNET = Number(process.env.XICMS_ISR_FALLBACK) || 3600;\n\nexport function cachedRead<T>(keyParts: string[], tags: string[], fn: () => Promise<T>): Promise<T> {\n return unstable_cache(fn, keyParts, { tags, revalidate: VANGNET })();\n}\n"],"mappings":"AAAA,OAAO;AAEP,SAAS,sBAAsB;AAuB/B,MAAM,UAAU,OAAO,QAAQ,IAAI,kBAAkB,KAAK;AAEnD,SAAS,WAAc,UAAoB,MAAgB,IAAkC;AAClG,SAAO,eAAe,IAAI,UAAU,EAAE,MAAM,YAAY,QAAQ,CAAC,EAAE;AACrE;","names":[]}
@@ -8,6 +8,8 @@ export { sanitizeRichText } from "./sanitize.js";
8
8
  export { jsonLdHtml } from "../blocks/builtins/jsonld.js";
9
9
  export { buildMetadata, buildJsonLd, buildSitemap, buildRobots, siteOrigin, businessJsonLd, breadcrumbListJsonLd, articleJsonLd, productJsonLd, openingHoursJsonLd, galleryJsonLd, videoObjectJsonLd, type BusinessInfo, } from "./seo.js";
10
10
  export { validateEnv, assertEnv, type EnvReport } from "./env.js";
11
+ export { getPageBlockData, type PageBlockDataOptions } from "./blockData.js";
12
+ export { listFormDefinitions } from "./pages.js";
11
13
  export { revalidatePOST } from "./revalidate.js";
12
14
  export { draftPOST } from "./draft.js";
13
15
  export { versionGET } from "./version.js";
@@ -17,6 +17,8 @@ import {
17
17
  videoObjectJsonLd
18
18
  } from "./seo.js";
19
19
  import { validateEnv, assertEnv } from "./env.js";
20
+ import { getPageBlockData } from "./blockData.js";
21
+ import { listFormDefinitions } from "./pages.js";
20
22
  import { revalidatePOST } from "./revalidate.js";
21
23
  import { draftPOST } from "./draft.js";
22
24
  import { versionGET } from "./version.js";
@@ -69,12 +71,14 @@ export {
69
71
  getDraftPageById,
70
72
  getDraftPageByPath,
71
73
  getNewsPageData,
74
+ getPageBlockData,
72
75
  getPublicSiteSettings,
73
76
  getPublishedPageByPath,
74
77
  getPublishedPageByPathExpanded,
75
78
  getSiteCustomBlocks,
76
79
  getSiteSymbols,
77
80
  jsonLdHtml,
81
+ listFormDefinitions,
78
82
  listPublishedNews,
79
83
  listPublishedPages,
80
84
  localizeBlocks,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/server/index.ts"],"sourcesContent":["/**\n * @xiwebdesign/xicms-core/server — server-only helpers (firebase-admin, sanitization, SEO,\n * pagina-reads). NOOIT importeren in client-componenten.\n */\nimport \"server-only\";\n\n// `getAdminAuth` zit BEWUST niet hier maar achter de subpath `@xiwebdesign/xicms-core/server/auth`\n// (zie ./auth.ts): firebase-admin/auth sloopt via jwks-rsa/jose élke server-render op Vercel, en\n// hoort dus niet in de bundel van sites die alleen pagina's lezen.\nexport { getAdminApp, getAdminDb } from \"../firebase/admin\";\nexport { sanitizeRichText } from \"./sanitize\";\n// Veilige JSON-LD-serialisatie voor <script type=\"application/ld+json\"> in consumer-routes\n// (escapet `<` zodat klant-tekst met </script> de pagina niet kan breken).\nexport { jsonLdHtml } from \"../blocks/builtins/jsonld\";\nexport {\n buildMetadata,\n buildJsonLd,\n buildSitemap,\n buildRobots,\n siteOrigin,\n businessJsonLd,\n breadcrumbListJsonLd,\n articleJsonLd,\n productJsonLd,\n openingHoursJsonLd,\n galleryJsonLd,\n videoObjectJsonLd,\n type BusinessInfo,\n} from \"./seo\";\nexport { validateEnv, assertEnv, type EnvReport } from \"./env\";\n// On-demand revalidation (6.5): POST-handler voor `app/api/revalidate/route.ts` in de klantsite.\n// De backend roept die route na een publicatie aan → wijziging binnen seconden live, zonder rebuild.\nexport { revalidatePOST } from \"./revalidate\";\n// Preview (Draft Mode): POST-handler voor `app/api/draft/route.ts` in de klantsite. Zet de\n// bypass-cookie zodat een ingelogde editor de content-bundel + ISR-cache overslaat, terwijl de\n// pagina-route voor publiek statisch blijft.\nexport { draftPOST } from \"./draft\";\n// Versie-endpoint: GET-handler voor `app/api/version/route.ts`, zodat het superbeheer kan zien op\n// welke engine een DEPLOYMENT draait (en niet alleen wanneer er voor het laatst is ingelogd).\nexport { versionGET } from \"./version\";\n// Shop-SSR (6.0): opt-in via subpath `@xiwebdesign/xicms-core/server/shop` (readers + product-SEO).\n// Niet meer onvoorwaardelijk her-geëxporteerd, zodat shop-servercode niet in elke site landt.\nexport {\n pathFromSlugs,\n listPublishedPages,\n listPublishedNews,\n getPublishedPageByPath,\n getPublishedPageByPathExpanded,\n getSiteSymbols,\n expandSymbols,\n getSiteCustomBlocks,\n getPublicSiteSettings,\n getDraftPageById,\n getDraftPageByPath,\n type StoredPage,\n} from \"./pages\";\n// Content-bundle (5.7): build-time reader van `content/published.bundle.json`. De pagina-/settings-/\n// symbol-/customBlock-readers hierboven zijn al bundle-first; deze exports zijn voor directe\n// consumptie (bv. nieuws-blockData in de klant-routes).\nexport {\n getBundle,\n getBundlePage,\n getBundleNews,\n getNewsPageData,\n type PublishedBundle,\n type BundleItem,\n} from \"./bundle\";\n// Localisatie (5.0) — server-safe overlay-helpers, ook bruikbaar in route-handlers/layouts.\nexport {\n siteLocales,\n parseLocaleSlugs,\n localizedPath,\n localizePage,\n localizeBlocks,\n pruneTranslations,\n TranslationsSchema,\n type Translations,\n type LocaleConfig,\n} from \"../i18n/overlay\";\n"],"mappings":"AAIA,OAAO;AAKP,SAAS,aAAa,kBAAkB;AACxC,SAAS,wBAAwB;AAGjC,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,aAAa,iBAAiC;AAGvD,SAAS,sBAAsB;AAI/B,SAAS,iBAAiB;AAG1B,SAAS,kBAAkB;AAG3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAIP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;","names":[]}
1
+ {"version":3,"sources":["../../src/server/index.ts"],"sourcesContent":["/**\n * @xiwebdesign/xicms-core/server — server-only helpers (firebase-admin, sanitization, SEO,\n * pagina-reads). NOOIT importeren in client-componenten.\n */\nimport \"server-only\";\n\n// `getAdminAuth` zit BEWUST niet hier maar achter de subpath `@xiwebdesign/xicms-core/server/auth`\n// (zie ./auth.ts): firebase-admin/auth sloopt via jwks-rsa/jose élke server-render op Vercel, en\n// hoort dus niet in de bundel van sites die alleen pagina's lezen.\nexport { getAdminApp, getAdminDb } from \"../firebase/admin\";\nexport { sanitizeRichText } from \"./sanitize\";\n// Veilige JSON-LD-serialisatie voor <script type=\"application/ld+json\"> in consumer-routes\n// (escapet `<` zodat klant-tekst met </script> de pagina niet kan breken).\nexport { jsonLdHtml } from \"../blocks/builtins/jsonld\";\nexport {\n buildMetadata,\n buildJsonLd,\n buildSitemap,\n buildRobots,\n siteOrigin,\n businessJsonLd,\n breadcrumbListJsonLd,\n articleJsonLd,\n productJsonLd,\n openingHoursJsonLd,\n galleryJsonLd,\n videoObjectJsonLd,\n type BusinessInfo,\n} from \"./seo\";\nexport { validateEnv, assertEnv, type EnvReport } from \"./env\";\n// On-demand revalidation (6.5): POST-handler voor `app/api/revalidate/route.ts` in de klantsite.\n// De backend roept die route na een publicatie aan → wijziging binnen seconden live, zonder rebuild.\n// SSR-seeds (7.12): verzamelt server-side de blockData die de blokken van déze pagina nodig\n// hebben (shop, nieuws, formulieren, zoekindex) — één aanroep in de catch-all, en geen enkel blok\n// hoeft meer per bezoeker uit Firestore te lezen.\nexport { getPageBlockData, type PageBlockDataOptions } from \"./blockData\";\nexport { listFormDefinitions } from \"./pages\";\nexport { revalidatePOST } from \"./revalidate\";\n// Preview (Draft Mode): POST-handler voor `app/api/draft/route.ts` in de klantsite. Zet de\n// bypass-cookie zodat een ingelogde editor de content-bundel + ISR-cache overslaat, terwijl de\n// pagina-route voor publiek statisch blijft.\nexport { draftPOST } from \"./draft\";\n// Versie-endpoint: GET-handler voor `app/api/version/route.ts`, zodat het superbeheer kan zien op\n// welke engine een DEPLOYMENT draait (en niet alleen wanneer er voor het laatst is ingelogd).\nexport { versionGET } from \"./version\";\n// Shop-SSR (6.0): opt-in via subpath `@xiwebdesign/xicms-core/server/shop` (readers + product-SEO).\n// Niet meer onvoorwaardelijk her-geëxporteerd, zodat shop-servercode niet in elke site landt.\nexport {\n pathFromSlugs,\n listPublishedPages,\n listPublishedNews,\n getPublishedPageByPath,\n getPublishedPageByPathExpanded,\n getSiteSymbols,\n expandSymbols,\n getSiteCustomBlocks,\n getPublicSiteSettings,\n getDraftPageById,\n getDraftPageByPath,\n type StoredPage,\n} from \"./pages\";\n// Content-bundle (5.7): build-time reader van `content/published.bundle.json`. De pagina-/settings-/\n// symbol-/customBlock-readers hierboven zijn al bundle-first; deze exports zijn voor directe\n// consumptie (bv. nieuws-blockData in de klant-routes).\nexport {\n getBundle,\n getBundlePage,\n getBundleNews,\n getNewsPageData,\n type PublishedBundle,\n type BundleItem,\n} from \"./bundle\";\n// Localisatie (5.0) — server-safe overlay-helpers, ook bruikbaar in route-handlers/layouts.\nexport {\n siteLocales,\n parseLocaleSlugs,\n localizedPath,\n localizePage,\n localizeBlocks,\n pruneTranslations,\n TranslationsSchema,\n type Translations,\n type LocaleConfig,\n} from \"../i18n/overlay\";\n"],"mappings":"AAIA,OAAO;AAKP,SAAS,aAAa,kBAAkB;AACxC,SAAS,wBAAwB;AAGjC,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,aAAa,iBAAiC;AAMvD,SAAS,wBAAmD;AAC5D,SAAS,2BAA2B;AACpC,SAAS,sBAAsB;AAI/B,SAAS,iBAAiB;AAG1B,SAAS,kBAAkB;AAG3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAIP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;","names":[]}
@@ -60,6 +60,20 @@ declare function listPublishedNewsUncached(siteId: string): Promise<NewsArticle[
60
60
  * bij een statische build), maar het is niet de verse bron.
61
61
  */
62
62
  export declare const listPublishedNews: typeof listPublishedNewsUncached;
63
+ /**
64
+ * Alle formulierdefinities van een site, server-side — voor de `forms`-blockData-seed
65
+ * (server/blockData.ts). Tot 7.12 las het formulierblok zijn definitie per BEZOEKER client-side
66
+ * (`getDoc` in de browser): één read per bezoeker per formulier, en het formulier verscheen pas
67
+ * na hydratie. Server-side gelezen zit het formulier in de SSR-HTML en kost het één read per
68
+ * cache-interval voor de hele site.
69
+ *
70
+ * Formulieren zitten niet in de content-bundle; bij een Firestore-storing levert dit gewoon een
71
+ * lege lijst en valt het blok terug op zijn eigen client-pad (dat dan ook faalt, maar met een
72
+ * nette "neem contact op"-melding).
73
+ */
74
+ export declare function listFormDefinitions(siteId: string): Promise<Array<{
75
+ id: string;
76
+ } & Record<string, unknown>>>;
63
77
  /** Alle symbolen van een site (gevalideerd; ongeldige overgeslagen met waarschuwing). Live-first of bundle-first. */
64
78
  declare function getSiteSymbolsUncached(siteId: string): Promise<StoredSymbol[]>;
65
79
  /**
@@ -7,6 +7,7 @@ import { CustomBlockSchema } from "../schema/customBlocks.js";
7
7
  import { PublicSiteSettingsSchema } from "../schema/settings.js";
8
8
  import { localizePage, localizeBlocks } from "../i18n/overlay.js";
9
9
  import { getBundle, getBundlePage } from "./bundle.js";
10
+ import { cachedRead } from "./cached.js";
10
11
  import { sanitizeRichText } from "./sanitize.js";
11
12
  const ISR_FALLBACK = Number(process.env.XICMS_ISR_FALLBACK) || 3600;
12
13
  const ISR_NO_PUSH = Number(process.env.XICMS_ISR_FALLBACK_NOPUSH) || 300;
@@ -158,7 +159,7 @@ function bundlePages(siteId) {
158
159
  }
159
160
  async function listPublishedPages(siteId) {
160
161
  if (LIVE_READS) {
161
- return liveOrBundle(
162
+ return cachedRead(["xicms", "pages", siteId], [cacheTags.pages(siteId)], () => liveOrBundle(
162
163
  `listPublishedPages ${siteId}`,
163
164
  async () => {
164
165
  const adb2 = getAdminDb();
@@ -169,7 +170,7 @@ async function listPublishedPages(siteId) {
169
170
  return restQueryThrowing(siteId, [eq("status", "published")]);
170
171
  },
171
172
  () => bundlePages(siteId)
172
- );
173
+ ));
173
174
  }
174
175
  const bundle = getBundle();
175
176
  if (bundle && bundle.siteId === siteId) {
@@ -190,7 +191,7 @@ const getSiteCustomBlocks = cache(getSiteCustomBlocksUncached);
190
191
  const getPublicSiteSettings = cache(getPublicSiteSettingsUncached);
191
192
  async function getPublishedPageByPathUncached(siteId, path, opts) {
192
193
  if (LIVE_READS && !opts?.skipBundle) {
193
- return liveOrBundle(
194
+ return cachedRead(["xicms", "page", siteId, path], [cacheTags.page(siteId, path)], () => liveOrBundle(
194
195
  `getPublishedPageByPath ${siteId}${path}`,
195
196
  async () => {
196
197
  const adb2 = getAdminDb();
@@ -208,7 +209,7 @@ async function getPublishedPageByPathUncached(siteId, path, opts) {
208
209
  const hit = getBundlePage(b, path);
209
210
  return hit ? parse(hit.id, hit) : null;
210
211
  }
211
- );
212
+ ));
212
213
  }
213
214
  const bundle = opts?.skipBundle ? null : getBundle();
214
215
  if (bundle && bundle.siteId === siteId) {
@@ -303,11 +304,11 @@ function bundleNewsRows(siteId) {
303
304
  async function listPublishedNewsUncached(siteId) {
304
305
  let rows;
305
306
  if (LIVE_READS) {
306
- rows = await liveOrBundle(
307
+ rows = await cachedRead(["xicms", "news", siteId], [cacheTags.collection(siteId, "news")], () => liveOrBundle(
307
308
  `listPublishedNews ${siteId}`,
308
309
  () => readCollectionThrowing(siteId, "news"),
309
310
  () => bundleNewsRows(siteId)
310
- );
311
+ ));
311
312
  } else {
312
313
  const bundle = getBundle();
313
314
  rows = bundle && bundle.siteId === siteId ? bundleNewsRows(siteId) : await readCollection(siteId, "news");
@@ -315,6 +316,14 @@ async function listPublishedNewsUncached(siteId) {
315
316
  return toPublishedNews(rows);
316
317
  }
317
318
  const listPublishedNews = cache(listPublishedNewsUncached);
319
+ async function listFormDefinitions(siteId) {
320
+ const rows = await cachedRead(
321
+ ["xicms", "forms", siteId],
322
+ [cacheTags.collection(siteId, "forms")],
323
+ () => readCollectionThrowing(siteId, "forms")
324
+ );
325
+ return rows.map((r) => ({ id: r.id, ...r.data }));
326
+ }
318
327
  function bundleSymbolRows(siteId) {
319
328
  const bundle = getBundle();
320
329
  return bundle && bundle.siteId === siteId ? bundle.symbols.map((s) => ({ id: s.id, data: s })) : [];
@@ -322,7 +331,7 @@ function bundleSymbolRows(siteId) {
322
331
  async function getSiteSymbolsUncached(siteId) {
323
332
  let rows;
324
333
  if (LIVE_READS) {
325
- rows = await liveOrBundle(`getSiteSymbols ${siteId}`, () => readCollectionThrowing(siteId, "symbols"), () => bundleSymbolRows(siteId));
334
+ rows = await cachedRead(["xicms", "symbols", siteId], [cacheTags.collection(siteId, "symbols")], () => liveOrBundle(`getSiteSymbols ${siteId}`, () => readCollectionThrowing(siteId, "symbols"), () => bundleSymbolRows(siteId)));
326
335
  } else {
327
336
  const bundle = getBundle();
328
337
  rows = bundle && bundle.siteId === siteId ? bundleSymbolRows(siteId) : await readCollection(siteId, "symbols");
@@ -368,7 +377,7 @@ async function getPublicSiteSettingsUncached(siteId) {
368
377
  return parsed2.success ? parsed2.data : {};
369
378
  };
370
379
  if (LIVE_READS) {
371
- return liveOrBundle(
380
+ return cachedRead(["xicms", "settings", siteId], [cacheTags.collection(siteId, "settings")], () => liveOrBundle(
372
381
  `getPublicSiteSettings ${siteId}`,
373
382
  async () => {
374
383
  const adb2 = getAdminDb();
@@ -391,7 +400,7 @@ async function getPublicSiteSettingsUncached(siteId) {
391
400
  return parsed2.success ? parsed2.data : {};
392
401
  },
393
402
  bundleSettings
394
- );
403
+ ));
395
404
  }
396
405
  const bundle = getBundle();
397
406
  if (bundle && bundle.siteId === siteId) {
@@ -430,7 +439,7 @@ function bundleCustomBlockRows(siteId) {
430
439
  async function getSiteCustomBlocksUncached(siteId) {
431
440
  let rows;
432
441
  if (LIVE_READS) {
433
- rows = await liveOrBundle(`getSiteCustomBlocks ${siteId}`, () => readCollectionThrowing(siteId, "customBlocks"), () => bundleCustomBlockRows(siteId));
442
+ rows = await cachedRead(["xicms", "customBlocks", siteId], [cacheTags.collection(siteId, "customBlocks")], () => liveOrBundle(`getSiteCustomBlocks ${siteId}`, () => readCollectionThrowing(siteId, "customBlocks"), () => bundleCustomBlockRows(siteId)));
434
443
  } else {
435
444
  const bundle = getBundle();
436
445
  rows = bundle && bundle.siteId === siteId ? bundleCustomBlockRows(siteId) : await readCollection(siteId, "customBlocks");
@@ -453,6 +462,7 @@ export {
453
462
  getPublishedPageByPathExpanded,
454
463
  getSiteCustomBlocks,
455
464
  getSiteSymbols,
465
+ listFormDefinitions,
456
466
  listPublishedNews,
457
467
  listPublishedPages,
458
468
  liveReadsActive,
@@ -1 +1 @@
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\";\nimport { getBundle, getBundlePage } from \"./bundle\";\nimport { sanitizeRichText } from \"./sanitize\";\nimport type { NewsArticle } from \"../content/types\";\n\n/**\n * BUNDLE-FIRST, Firestore-fallback (5.7).\n *\n * Elke publieke reader hieronder kijkt eerst of er een gecommit content-bundle is (server/bundle.ts,\n * `content/published.bundle.json`). Zo ja → serveer daaruit met exact dezelfde return-vormen als het\n * Firestore-pad (incl. schema-validatie, i18n-overlay en symbol-expansie). Zo nee (dev/lokaal of een\n * repo zonder gecommit bundle) → val ongewijzigd terug op de Admin-SDK / keyless-REST-implementatie.\n * De bundle-tak is puur additief: signatures en gedrag van de fallback blijven identiek.\n */\n\nexport type StoredPage = Page & { id: string };\n\n/**\n * Cache-tags voor gerichte invalidatie.\n *\n * Zonder tags kende de site maar één knop: `revalidatePath(\"/\", \"layout\")` bij élke publicatie,\n * wat de HELE site ongeldig verklaart. Eén tekstwijziging op een site van 20 pagina's kostte\n * daarmee tot 20 verse Firestore-reads in plaats van 1 — precies het tegenovergestelde van goedkoop.\n *\n * Met tags invalideert de backend alleen wat er werkelijk wijzigde: die ene pagina, of de\n * instellingen, of de symbolen. De rest blijft staan en kost niets.\n */\n/**\n * Tijdgebaseerd ISR-venster — nu het VANGNET, niet het normale pad.\n *\n * Verse content komt van de cache-tags: publiceren duwt de wijziging binnen seconden door. De\n * timer dekt alleen het geval dat die push níét aankomt (site even onbereikbaar, geheim verkeerd,\n * deploy ertussen). Op 300s betekende dat: élke pagina hertelt elke 5 minuten bij Firestore, ook\n * als er maanden niets wijzigt — bij ~50 sites duizenden reads per dag voor niets.\n *\n * Een uur houdt de kosten laag en laat een gemiste push zichzelf herstellen. Per site bij te\n * stellen met `XICMS_ISR_FALLBACK` (seconden).\n */\nconst ISR_FALLBACK = Number(process.env.XICMS_ISR_FALLBACK) || 3600;\n\n/**\n * Kortere terugval voor bronnen ZONDER push, mocht die er ooit weer zijn.\n *\n * Sinds 6.6 heeft élke publieke bron een push: pagina's via `publishPage`, symbolen en bouwstenen\n * via hun callables, en instellingen via `saveSiteSettings` — die laatste bestond niet, waardoor\n * het beheer rechtstreeks naar Firestore schreef en er niets viel door te duwen. Deze constante\n * blijft staan als vangnet voor toekomstige bronnen die datzelfde gat zouden hebben.\n */\nconst ISR_NO_PUSH = Number(process.env.XICMS_ISR_FALLBACK_NOPUSH) || 300;\n\nexport const cacheTags = {\n page: (siteId: string, path: string) => `xicms:${siteId}:page:${path}`,\n pages: (siteId: string) => `xicms:${siteId}:pages`,\n collection: (siteId: string, name: string) => `xicms:${siteId}:${name}`,\n};\n\n/**\n * Tags voor een pagina-query. Filtert de query op één pad, dan krijgt de read ALLEEN de PAD-tag;\n * een query zonder padfilter (de lijst) krijgt de lijst-tag.\n *\n * WAAROM NIET ALLEBEI (zoals hiervoor): de lijst-tag stond óók op een read van één enkele pagina,\n * en `publishPage` stuurt die lijst-tag altijd mee. Daarmee verklaarde élke publicatie de cache van\n * élke pagina ongeldig — precies wat de gerichte tags moesten voorkomen. Gemeten op de staging-\n * deploy: `/diensten` stond op `x-vercel-cache: HIT, age: 28` en sprong na het publiceren van een\n * tekst op een ándere pagina terug naar `REVALIDATED, age: 0`. Kosten: een verse Firestore-read per\n * pagina per publicatie.\n *\n * Veilig omdat geen enkele pagina-render van de lijst afhangt: `listPublishedPages` wordt alleen\n * gebruikt in `generateStaticParams` en de sitemap, niet in de nav. De keerzijde is dat een\n * pad-WIJZIGING nu expliciet moet worden opgeruimd — daarom stuurt `publishPage` ook de tag van het\n * oude pad mee wanneer dat verandert (functions/src/index.ts).\n */\nfunction pageQueryTags(siteId: string, filters: Filter[]): string[] {\n const padFilter = filters.find((f) => JSON.stringify(f).includes('\"path\"'));\n const pad = padFilter ? (JSON.stringify(padFilter).match(/\"stringValue\":\"([^\"]*)\"/)?.[1] ?? null) : null;\n return pad ? [cacheTags.page(siteId, pad)] : [cacheTags.pages(siteId)];\n}\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 const tags = pageQueryTags(siteId, filters);\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 // ISR + gerichte tags: de tijdslimiet is het vangnet, de tag is het normale pad.\n next: { revalidate: ISR_FALLBACK, tags },\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/* ================= LIVE-READS-modus (6.5): vers uit Firestore, bundle als STATISCHE FALLBACK =======\n *\n * Standaard is XiCMS **bundle-first** (5.7): een gecommitte `content/published.bundle.json` is de\n * bron → nul Firestore at-runtime, maar een wijziging verschijnt pas na de gedebouncede rebuild.\n *\n * Zet je per site de env-var `XICMS_LIVE_READS=1`, dan draait de reader **Firestore-first**:\n * - Primair lezen we vers uit Firestore (Admin of keyless REST). In combinatie met on-demand\n * revalidation (`/api/revalidate`, aangeroepen door de backend na een publicatie) staat een\n * wijziging binnen seconden live — zónder rebuild, dus zonder Vercel-bouwminuten.\n * - Is Firestore **onbereikbaar** (netwerk/permissie/ontbrekende config), dan valt de reader terug\n * op de gecommitte bundle: een STATISCHE last-known-good back-up. De site blijft dus staan als\n * Firebase plat ligt.\n * - Een BEREIKBARE Firestore is gezaghebbend: \"pagina bestaat niet\" → echte 404 (géén bundle-\n * fallback, anders zou een verwijderde pagina uit de back-up herrijzen).\n *\n * De preview-modus (`?preview=1`, `skipBundle`) leest sowieso rechtstreeks en negeert de bundle.\n */\n/**\n * Live-reads staan STANDAARD aan zodra Firestore bereikbaar is.\n *\n * Firestore is leidend voor de inhoud, dus een tekstwijziging hoort binnen seconden live te staan\n * zónder rebuild. Dat als opt-in-vlag laten betekende dat elke site die hem vergat alsnog een\n * Vercel-build per tekstwijziging kreeg — de trage én dure route, precies wat we niet willen.\n *\n * Daarom afgeleid uit CAPACITEIT in plaats van uit een handmatige schakelaar:\n * - server-credentials (Admin-SDK) of een publieke REST-config aanwezig → live-reads;\n * - geen van beide → bundle-only, want er valt niets vers te lezen;\n * - `XICMS_LIVE_READS=0` → expliciet uit (bewuste keuze voor een volledig statische site).\n *\n * De terugval blijft ongewijzigd: is Firestore onbereikbaar, dan serveert `liveOrBundle` de\n * gecommitte bundle als last-known-good. Standaard aanzetten vergroot dus niet de kans op een\n * kapotte site — alleen de kans dat content vers is.\n */\nfunction liveReadsEnabled(): boolean {\n const flag = process.env.XICMS_LIVE_READS?.toLowerCase();\n if (flag === \"0\" || flag === \"false\") return false;\n if (flag === \"1\" || flag === \"true\") return true;\n const heeftAdmin = Boolean(process.env.FIREBASE_PROJECT_ID && process.env.FIREBASE_CLIENT_EMAIL);\n const heeftRest = Boolean(process.env.NEXT_PUBLIC_FIREBASE_API_KEY && process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID);\n return heeftAdmin || heeftRest;\n}\n\nconst LIVE_READS = liveReadsEnabled();\n\n/** Leest of live-reads actief zijn — voor diagnose en tests. */\nexport function liveReadsActive(): boolean {\n return LIVE_READS;\n}\n\n/**\n * Live-modus-wrapper: probeer `fresh()` (Firestore). Slaagt die → verse data. Gooit die (Firestore\n * onbereikbaar) → `fromBundle()` (last-known-good back-up). Buiten live-modus/preview niet gebruikt.\n */\nasync function liveOrBundle<T>(what: string, fresh: () => Promise<T>, fromBundle: () => T): Promise<T> {\n try {\n return await fresh();\n } catch (e) {\n console.warn(`[xicms] Firestore onbereikbaar (${what}) — statische bundle-fallback gebruikt:`, (e as Error)?.message ?? e);\n return fromBundle();\n }\n}\n\n/** Firestore-config voor het keyless REST-pad, of GOOIT als die ontbreekt (→ bundle-fallback i.p.v.\n * stil een lege lijst serveren). */\nfunction restConfigOrThrow(): { apiKey: string; projectId: string } {\n const apiKey = process.env.NEXT_PUBLIC_FIREBASE_API_KEY;\n const projectId = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID;\n if (!apiKey || !projectId) throw new Error(\"Firebase REST-config ontbreekt (apiKey/projectId)\");\n return { apiKey, projectId };\n}\n\n/** Als `restQuery`, maar GOOIT bij fouten (i.p.v. []) zodat live-modus de bundle-fallback kan kiezen. */\nasync function restQueryThrowing(siteId: string, filters: Filter[], limit?: number): Promise<StoredPage[]> {\n const { apiKey, projectId } = restConfigOrThrow();\n const tags = pageQueryTags(siteId, filters);\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 const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ structuredQuery }),\n next: { revalidate: ISR_FALLBACK, tags },\n });\n if (!res.ok) throw new Error(`Firestore REST-query ${res.status}`);\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}\n\n/** Verse (throwing) collectie-read onder sites/{siteId}/ — Admin als beschikbaar, anders keyless REST. */\nasync function readCollectionThrowing(siteId: string, collectionId: string): Promise<{ id: string; data: Record<string, unknown> }[]> {\n const adb = getAdminDb();\n if (adb) {\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 const { apiKey, projectId } = restConfigOrThrow();\n const url = `https://firestore.googleapis.com/v1/projects/${projectId}/databases/(default)/documents/sites/${siteId}:runQuery?key=${apiKey}`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ structuredQuery: { from: [{ collectionId }] } }),\n // Collectie-tag: het opslaan van een symbool raakt alleen de symbolen-cache, niet de pagina's.\n next: { revalidate: ISR_FALLBACK, tags: [cacheTags.collection(siteId, collectionId)] },\n });\n if (!res.ok) throw new Error(`Firestore REST-read ${collectionId} ${res.status}`);\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}\n\n/** Pagina's uit de gecommitte bundle (of [] als er voor deze site geen bundle is). */\nfunction bundlePages(siteId: string): StoredPage[] {\n const bundle = getBundle();\n if (!bundle || bundle.siteId !== siteId) return [];\n return bundle.pages.map((p) => parse(p.id, p)).filter((p): p is StoredPage => p !== null);\n}\n\n/** Alle gepubliceerde pagina's (sitemap / generateStaticParams). Live-first (bundle-fallback) of bundle-first. */\nexport async function listPublishedPages(siteId: string): Promise<StoredPage[]> {\n if (LIVE_READS) {\n return liveOrBundle(\n `listPublishedPages ${siteId}`,\n async () => {\n const adb = getAdminDb();\n if (adb) {\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 return restQueryThrowing(siteId, [eq(\"status\", \"published\")]);\n },\n () => bundlePages(siteId),\n );\n }\n const bundle = getBundle();\n if (bundle && bundle.siteId === siteId) {\n return bundle.pages.map((p) => parse(p.id, p)).filter((p): p is StoredPage => p !== null);\n }\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/**\n * Eén gepubliceerde pagina op pad. Bundle-first, anders keyless via REST/Admin.\n * `opts.skipBundle` (voorbeeld-modus, `?preview=1` in de visuele editor): negeert de gecommitte\n * bundel en leest rechtstreeks — de bundel komt pas met vertraging mee via de gedebouncede deploy-\n * worker, dus een beheerder die net gepubliceerd heeft ziet anders tot ~15 min lang de oude tekst.\n */\nasync function getPublishedPageByPathUncached(\n siteId: string,\n path: string,\n opts?: { skipBundle?: boolean },\n): Promise<StoredPage | null> {\n // Live-modus (niet in preview): vers uit Firestore, met de bundle als fallback bij een outage.\n if (LIVE_READS && !opts?.skipBundle) {\n return liveOrBundle(\n `getPublishedPageByPath ${siteId}${path}`,\n async () => {\n const adb = getAdminDb();\n if (adb) {\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 const rows = await restQueryThrowing(siteId, [eq(\"status\", \"published\"), eq(\"path\", path)], 1);\n return rows[0] ?? null;\n },\n () => {\n const b = getBundle();\n if (!b || b.siteId !== siteId) return null;\n const hit = getBundlePage(b, path);\n return hit ? parse(hit.id, hit) : null;\n },\n );\n }\n const bundle = opts?.skipBundle ? null : getBundle();\n if (bundle && bundle.siteId === siteId) {\n const hit = getBundlePage(bundle, path);\n return hit ? parse(hit.id, hit) : null;\n }\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: ISR_FALLBACK, tags: [cacheTags.collection(siteId, collectionId)] },\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/* ── Nieuws ───────────────────────────────────────────────────────────────────────────────────\n * Nieuws was het enige publieke contenttype ZONDER live-tak: `getNewsPageData` (server/bundle.ts)\n * leest uitsluitend de build-time bundle. Op een live-reads-site betekende dat: je publiceert een\n * artikel en er verandert niets op de site, zonder één foutmelding (SITE-AUDIT §11). Deze lezer\n * doet voor nieuws wat `listPublishedPages` voor pagina's doet.\n */\n\n/** Gepubliceerd volgens de bundle-writer (functions/src/lib/bundle.ts) — één definitie, twee kanten. */\nfunction isPublishedNews(data: Record<string, unknown>): boolean {\n return data.published === true || data.status === \"published\";\n}\n\n/**\n * Firestore-waarden → gewone JS-waarden.\n *\n * Een Admin-SDK-read geeft `Timestamp`, `DocumentReference` en `GeoPoint` terug: klasse-instanties.\n * React weigert die van een server- naar een client-component te sturen (\"Only plain objects, and a\n * few built-ins, can be passed to Client Components\"), en het nieuws gaat via `BlockDataProvider`\n * precies die grens over. De bundle had dit nooit, want die is al JSON — dus dit kwam pas boven bij\n * het draaien: één artikel met een `serverTimestamp` en `/nieuws/<slug>` gaf een 500.\n *\n * Het REST-pad levert al gewone waarden (`decodeFields`), dus daar is dit een no-op.\n */\nfunction plain(v: unknown): unknown {\n if (v == null || typeof v !== \"object\") return v;\n if (Array.isArray(v)) return v.map(plain);\n if (v instanceof Date) return v.toISOString();\n const kandidaat = v as { toDate?: () => Date };\n if (typeof kandidaat.toDate === \"function\") return kandidaat.toDate().toISOString(); // Timestamp\n const proto = Object.getPrototypeOf(v);\n // Geen gewoon object (DocumentReference, GeoPoint, Buffer…): weglaten in plaats van doorgeven.\n if (proto !== Object.prototype && proto !== null) return undefined;\n const uit: Record<string, unknown> = {};\n for (const [k, x] of Object.entries(v as Record<string, unknown>)) {\n const w = plain(x);\n if (w !== undefined) uit[k] = w;\n }\n return uit;\n}\n\n/**\n * Rijen → artikelen: filteren, nieuwste eerst, en `bodyHtml` SANEREN.\n *\n * Dat saneren is geen extraatje maar een voorwaarde. `NewsArticleView` vertrouwt bodyHtml zodra het\n * exact overeenkomt met wat de server meegaf, en rendert het dan rechtstreeks in de SSR-HTML; alleen\n * ONvertrouwde HTML gaat door DOMPurify in de browser. De bundle-lezer saneert daarom al\n * (`getBundleNews`). Zou deze live-lezer dat overslaan, dan werd ruwe Firestore-HTML \"vertrouwd\" en\n * ongefilterd gerenderd — dan zou een live-tak toevoegen een XSS-gat openen dat er nu niet is.\n */\nfunction toPublishedNews(rows: { id: string; data: Record<string, unknown> }[]): NewsArticle[] {\n return rows\n .filter((r) => isPublishedNews(r.data))\n .map((r) => {\n const raw = r.data.bodyHtml;\n const data: Record<string, unknown> = { id: r.id, ...(plain(r.data) as Record<string, unknown>) };\n if (typeof raw === \"string\") data.bodyHtml = sanitizeRichText(raw);\n return data;\n })\n .sort((a, b) => String(b.date ?? \"\").localeCompare(String(a.date ?? \"\"))) as unknown as NewsArticle[];\n}\n\n/** Nieuws-rijen uit de gecommitte bundle (of [] als er voor deze site geen bundle is). */\nfunction bundleNewsRows(siteId: string): { id: string; data: Record<string, unknown> }[] {\n const bundle = getBundle();\n return bundle && bundle.siteId === siteId\n ? bundle.news.map((n) => ({ id: n.id, data: n as Record<string, unknown> }))\n : [];\n}\n\nasync function listPublishedNewsUncached(siteId: string): Promise<NewsArticle[]> {\n let rows: { id: string; data: Record<string, unknown> }[];\n if (LIVE_READS) {\n rows = await liveOrBundle(\n `listPublishedNews ${siteId}`,\n () => readCollectionThrowing(siteId, \"news\"),\n () => bundleNewsRows(siteId),\n );\n } else {\n const bundle = getBundle();\n rows = bundle && bundle.siteId === siteId ? bundleNewsRows(siteId) : await readCollection(siteId, \"news\");\n }\n return toPublishedNews(rows);\n}\n\n/**\n * Gepubliceerde nieuwsartikelen, nieuwste eerst. Live-first (bundle als statische fallback) of\n * bundle-first — exact hetzelfde contract als `listPublishedPages`.\n *\n * Gebruik dit in de publieke nieuwsroutes. `getNewsPageData` blijft bestaan en doet precies wat de\n * naam zegt: de bundle lezen, synchroon — handig waar je gegarandeerd build-time data wilt (sitemap\n * bij een statische build), maar het is niet de verse bron.\n */\nexport const listPublishedNews = cache(listPublishedNewsUncached);\n\n/** Symbool-rijen uit de gecommitte bundle (of [] als er voor deze site geen bundle is). */\nfunction bundleSymbolRows(siteId: string): { id: string; data: Record<string, unknown> }[] {\n const bundle = getBundle();\n return bundle && bundle.siteId === siteId\n ? bundle.symbols.map((s) => ({ id: s.id, data: s as Record<string, unknown> }))\n : [];\n}\n\n/** Alle symbolen van een site (gevalideerd; ongeldige overgeslagen met waarschuwing). Live-first of bundle-first. */\nasync function getSiteSymbolsUncached(siteId: string): Promise<StoredSymbol[]> {\n let rows: { id: string; data: Record<string, unknown> }[];\n if (LIVE_READS) {\n rows = await liveOrBundle(`getSiteSymbols ${siteId}`, () => readCollectionThrowing(siteId, \"symbols\"), () => bundleSymbolRows(siteId));\n } else {\n const bundle = getBundle();\n rows = bundle && bundle.siteId === siteId ? bundleSymbolRows(siteId) : await readCollection(siteId, \"symbols\");\n }\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 *\n * Bundle-first werkt hier automatisch: deze functie componeert `getPublishedPageByPath` en\n * `getSiteSymbols`, die beide al bundle-first zijn — de i18n-overlay en expansie draaien identiek\n * op bundle- én Firestore-data.\n */\nexport async function getPublishedPageByPathExpanded(\n siteId: string,\n path: string,\n locale?: string,\n opts?: { skipBundle?: boolean },\n): Promise<StoredPage | null> {\n let page = await getPublishedPageByPath(siteId, path, opts);\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 bundleSettings = (): PublicSiteSettings => {\n const b = getBundle();\n if (!b || b.siteId !== siteId) return {};\n const parsed = PublicSiteSettingsSchema.safeParse(b.settings ?? {});\n return parsed.success ? parsed.data : {};\n };\n // Live-modus: vers uit Firestore (throwing), met de bundle-settings als fallback bij een outage.\n if (LIVE_READS) {\n return liveOrBundle(\n `getPublicSiteSettings ${siteId}`,\n async () => {\n const adb = getAdminDb();\n let data: unknown = {};\n if (adb) {\n const d = await adb.doc(`sites/${siteId}/settings/public`).get();\n data = d.exists ? d.data() : {};\n } else {\n const { apiKey, projectId } = restConfigOrThrow();\n const url = `https://firestore.googleapis.com/v1/projects/${projectId}/databases/(default)/documents/sites/${siteId}/settings/public?key=${apiKey}`;\n const res = await fetch(url, { next: { revalidate: ISR_FALLBACK, tags: [cacheTags.collection(siteId, \"settings\")] } });\n // 404 = document bestaat (nog) niet → lege settings; andere niet-2xx = outage → gooi (fallback).\n if (res.status === 404) data = {};\n else if (!res.ok) throw new Error(`Firestore REST-read settings/public ${res.status}`);\n else {\n const doc = (await res.json()) as { fields?: Record<string, FsValue> };\n data = decodeFields(doc.fields ?? {});\n }\n }\n const parsed = PublicSiteSettingsSchema.safeParse(data ?? {});\n return parsed.success ? parsed.data : {};\n },\n bundleSettings,\n );\n }\n const bundle = getBundle();\n if (bundle && bundle.siteId === siteId) {\n return bundleSettings();\n }\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: ISR_FALLBACK, tags: [cacheTags.collection(siteId, \"settings\")] } });\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/** Custom-block-rijen uit de gecommitte bundle (of [] als er voor deze site geen bundle is). */\nfunction bundleCustomBlockRows(siteId: string): { id: string; data: Record<string, unknown> }[] {\n const bundle = getBundle();\n return bundle && bundle.siteId === siteId\n ? bundle.customBlocks.map((c) => ({ id: c.id, data: c as Record<string, unknown> }))\n : [];\n}\n\n/** Alle custom blocks van een site (gevalideerd) — voor SSR-registratie via XiCMSProvider. Live-first of bundle-first. */\nasync function getSiteCustomBlocksUncached(siteId: string): Promise<CustomBlock[]> {\n let rows: { id: string; data: Record<string, unknown> }[];\n if (LIVE_READS) {\n rows = await liveOrBundle(`getSiteCustomBlocks ${siteId}`, () => readCollectionThrowing(siteId, \"customBlocks\"), () => bundleCustomBlockRows(siteId));\n } else {\n const bundle = getBundle();\n rows = bundle && bundle.siteId === siteId ? bundleCustomBlockRows(siteId) : await readCollection(siteId, \"customBlocks\");\n }\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":["adb","rows","parsed","data"]}
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\";\nimport { getBundle, getBundlePage } from \"./bundle\";\nimport { cachedRead } from \"./cached\";\nimport { sanitizeRichText } from \"./sanitize\";\nimport type { NewsArticle } from \"../content/types\";\n\n/**\n * BUNDLE-FIRST, Firestore-fallback (5.7).\n *\n * Elke publieke reader hieronder kijkt eerst of er een gecommit content-bundle is (server/bundle.ts,\n * `content/published.bundle.json`). Zo ja → serveer daaruit met exact dezelfde return-vormen als het\n * Firestore-pad (incl. schema-validatie, i18n-overlay en symbol-expansie). Zo nee (dev/lokaal of een\n * repo zonder gecommit bundle) → val ongewijzigd terug op de Admin-SDK / keyless-REST-implementatie.\n * De bundle-tak is puur additief: signatures en gedrag van de fallback blijven identiek.\n */\n\nexport type StoredPage = Page & { id: string };\n\n/**\n * Cache-tags voor gerichte invalidatie.\n *\n * Zonder tags kende de site maar één knop: `revalidatePath(\"/\", \"layout\")` bij élke publicatie,\n * wat de HELE site ongeldig verklaart. Eén tekstwijziging op een site van 20 pagina's kostte\n * daarmee tot 20 verse Firestore-reads in plaats van 1 — precies het tegenovergestelde van goedkoop.\n *\n * Met tags invalideert de backend alleen wat er werkelijk wijzigde: die ene pagina, of de\n * instellingen, of de symbolen. De rest blijft staan en kost niets.\n */\n/**\n * Tijdgebaseerd ISR-venster — nu het VANGNET, niet het normale pad.\n *\n * Verse content komt van de cache-tags: publiceren duwt de wijziging binnen seconden door. De\n * timer dekt alleen het geval dat die push níét aankomt (site even onbereikbaar, geheim verkeerd,\n * deploy ertussen). Op 300s betekende dat: élke pagina hertelt elke 5 minuten bij Firestore, ook\n * als er maanden niets wijzigt — bij ~50 sites duizenden reads per dag voor niets.\n *\n * Een uur houdt de kosten laag en laat een gemiste push zichzelf herstellen. Per site bij te\n * stellen met `XICMS_ISR_FALLBACK` (seconden).\n */\nconst ISR_FALLBACK = Number(process.env.XICMS_ISR_FALLBACK) || 3600;\n\n/**\n * Kortere terugval voor bronnen ZONDER push, mocht die er ooit weer zijn.\n *\n * Sinds 6.6 heeft élke publieke bron een push: pagina's via `publishPage`, symbolen en bouwstenen\n * via hun callables, en instellingen via `saveSiteSettings` — die laatste bestond niet, waardoor\n * het beheer rechtstreeks naar Firestore schreef en er niets viel door te duwen. Deze constante\n * blijft staan als vangnet voor toekomstige bronnen die datzelfde gat zouden hebben.\n */\nconst ISR_NO_PUSH = Number(process.env.XICMS_ISR_FALLBACK_NOPUSH) || 300;\n\nexport const cacheTags = {\n page: (siteId: string, path: string) => `xicms:${siteId}:page:${path}`,\n pages: (siteId: string) => `xicms:${siteId}:pages`,\n collection: (siteId: string, name: string) => `xicms:${siteId}:${name}`,\n};\n\n/**\n * Tags voor een pagina-query. Filtert de query op één pad, dan krijgt de read ALLEEN de PAD-tag;\n * een query zonder padfilter (de lijst) krijgt de lijst-tag.\n *\n * WAAROM NIET ALLEBEI (zoals hiervoor): de lijst-tag stond óók op een read van één enkele pagina,\n * en `publishPage` stuurt die lijst-tag altijd mee. Daarmee verklaarde élke publicatie de cache van\n * élke pagina ongeldig — precies wat de gerichte tags moesten voorkomen. Gemeten op de staging-\n * deploy: `/diensten` stond op `x-vercel-cache: HIT, age: 28` en sprong na het publiceren van een\n * tekst op een ándere pagina terug naar `REVALIDATED, age: 0`. Kosten: een verse Firestore-read per\n * pagina per publicatie.\n *\n * Veilig omdat geen enkele pagina-render van de lijst afhangt: `listPublishedPages` wordt alleen\n * gebruikt in `generateStaticParams` en de sitemap, niet in de nav. De keerzijde is dat een\n * pad-WIJZIGING nu expliciet moet worden opgeruimd — daarom stuurt `publishPage` ook de tag van het\n * oude pad mee wanneer dat verandert (functions/src/index.ts).\n */\nfunction pageQueryTags(siteId: string, filters: Filter[]): string[] {\n const padFilter = filters.find((f) => JSON.stringify(f).includes('\"path\"'));\n const pad = padFilter ? (JSON.stringify(padFilter).match(/\"stringValue\":\"([^\"]*)\"/)?.[1] ?? null) : null;\n return pad ? [cacheTags.page(siteId, pad)] : [cacheTags.pages(siteId)];\n}\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 const tags = pageQueryTags(siteId, filters);\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 // ISR + gerichte tags: de tijdslimiet is het vangnet, de tag is het normale pad.\n next: { revalidate: ISR_FALLBACK, tags },\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/* ================= LIVE-READS-modus (6.5): vers uit Firestore, bundle als STATISCHE FALLBACK =======\n *\n * Standaard is XiCMS **bundle-first** (5.7): een gecommitte `content/published.bundle.json` is de\n * bron → nul Firestore at-runtime, maar een wijziging verschijnt pas na de gedebouncede rebuild.\n *\n * Zet je per site de env-var `XICMS_LIVE_READS=1`, dan draait de reader **Firestore-first**:\n * - Primair lezen we vers uit Firestore (Admin of keyless REST). In combinatie met on-demand\n * revalidation (`/api/revalidate`, aangeroepen door de backend na een publicatie) staat een\n * wijziging binnen seconden live — zónder rebuild, dus zonder Vercel-bouwminuten.\n * - Is Firestore **onbereikbaar** (netwerk/permissie/ontbrekende config), dan valt de reader terug\n * op de gecommitte bundle: een STATISCHE last-known-good back-up. De site blijft dus staan als\n * Firebase plat ligt.\n * - Een BEREIKBARE Firestore is gezaghebbend: \"pagina bestaat niet\" → echte 404 (géén bundle-\n * fallback, anders zou een verwijderde pagina uit de back-up herrijzen).\n *\n * De preview-modus (`?preview=1`, `skipBundle`) leest sowieso rechtstreeks en negeert de bundle.\n */\n/**\n * Live-reads staan STANDAARD aan zodra Firestore bereikbaar is.\n *\n * Firestore is leidend voor de inhoud, dus een tekstwijziging hoort binnen seconden live te staan\n * zónder rebuild. Dat als opt-in-vlag laten betekende dat elke site die hem vergat alsnog een\n * Vercel-build per tekstwijziging kreeg — de trage én dure route, precies wat we niet willen.\n *\n * Daarom afgeleid uit CAPACITEIT in plaats van uit een handmatige schakelaar:\n * - server-credentials (Admin-SDK) of een publieke REST-config aanwezig → live-reads;\n * - geen van beide → bundle-only, want er valt niets vers te lezen;\n * - `XICMS_LIVE_READS=0` → expliciet uit (bewuste keuze voor een volledig statische site).\n *\n * De terugval blijft ongewijzigd: is Firestore onbereikbaar, dan serveert `liveOrBundle` de\n * gecommitte bundle als last-known-good. Standaard aanzetten vergroot dus niet de kans op een\n * kapotte site — alleen de kans dat content vers is.\n */\nfunction liveReadsEnabled(): boolean {\n const flag = process.env.XICMS_LIVE_READS?.toLowerCase();\n if (flag === \"0\" || flag === \"false\") return false;\n if (flag === \"1\" || flag === \"true\") return true;\n const heeftAdmin = Boolean(process.env.FIREBASE_PROJECT_ID && process.env.FIREBASE_CLIENT_EMAIL);\n const heeftRest = Boolean(process.env.NEXT_PUBLIC_FIREBASE_API_KEY && process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID);\n return heeftAdmin || heeftRest;\n}\n\nconst LIVE_READS = liveReadsEnabled();\n\n/** Leest of live-reads actief zijn — voor diagnose en tests. */\nexport function liveReadsActive(): boolean {\n return LIVE_READS;\n}\n\n/**\n * Live-modus-wrapper: probeer `fresh()` (Firestore). Slaagt die → verse data. Gooit die (Firestore\n * onbereikbaar) → `fromBundle()` (last-known-good back-up). Buiten live-modus/preview niet gebruikt.\n */\nasync function liveOrBundle<T>(what: string, fresh: () => Promise<T>, fromBundle: () => T): Promise<T> {\n try {\n return await fresh();\n } catch (e) {\n console.warn(`[xicms] Firestore onbereikbaar (${what}) — statische bundle-fallback gebruikt:`, (e as Error)?.message ?? e);\n return fromBundle();\n }\n}\n\n/** Firestore-config voor het keyless REST-pad, of GOOIT als die ontbreekt (→ bundle-fallback i.p.v.\n * stil een lege lijst serveren). */\nfunction restConfigOrThrow(): { apiKey: string; projectId: string } {\n const apiKey = process.env.NEXT_PUBLIC_FIREBASE_API_KEY;\n const projectId = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID;\n if (!apiKey || !projectId) throw new Error(\"Firebase REST-config ontbreekt (apiKey/projectId)\");\n return { apiKey, projectId };\n}\n\n/** Als `restQuery`, maar GOOIT bij fouten (i.p.v. []) zodat live-modus de bundle-fallback kan kiezen. */\nasync function restQueryThrowing(siteId: string, filters: Filter[], limit?: number): Promise<StoredPage[]> {\n const { apiKey, projectId } = restConfigOrThrow();\n const tags = pageQueryTags(siteId, filters);\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 const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ structuredQuery }),\n next: { revalidate: ISR_FALLBACK, tags },\n });\n if (!res.ok) throw new Error(`Firestore REST-query ${res.status}`);\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}\n\n/** Verse (throwing) collectie-read onder sites/{siteId}/ — Admin als beschikbaar, anders keyless REST. */\nasync function readCollectionThrowing(siteId: string, collectionId: string): Promise<{ id: string; data: Record<string, unknown> }[]> {\n const adb = getAdminDb();\n if (adb) {\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 const { apiKey, projectId } = restConfigOrThrow();\n const url = `https://firestore.googleapis.com/v1/projects/${projectId}/databases/(default)/documents/sites/${siteId}:runQuery?key=${apiKey}`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ structuredQuery: { from: [{ collectionId }] } }),\n // Collectie-tag: het opslaan van een symbool raakt alleen de symbolen-cache, niet de pagina's.\n next: { revalidate: ISR_FALLBACK, tags: [cacheTags.collection(siteId, collectionId)] },\n });\n if (!res.ok) throw new Error(`Firestore REST-read ${collectionId} ${res.status}`);\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}\n\n/** Pagina's uit de gecommitte bundle (of [] als er voor deze site geen bundle is). */\nfunction bundlePages(siteId: string): StoredPage[] {\n const bundle = getBundle();\n if (!bundle || bundle.siteId !== siteId) return [];\n return bundle.pages.map((p) => parse(p.id, p)).filter((p): p is StoredPage => p !== null);\n}\n\n/** Alle gepubliceerde pagina's (sitemap / generateStaticParams). Live-first (bundle-fallback) of bundle-first. */\nexport async function listPublishedPages(siteId: string): Promise<StoredPage[]> {\n if (LIVE_READS) {\n return cachedRead([\"xicms\", \"pages\", siteId], [cacheTags.pages(siteId)], () => liveOrBundle(\n `listPublishedPages ${siteId}`,\n async () => {\n const adb = getAdminDb();\n if (adb) {\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 return restQueryThrowing(siteId, [eq(\"status\", \"published\")]);\n },\n () => bundlePages(siteId),\n ));\n }\n const bundle = getBundle();\n if (bundle && bundle.siteId === siteId) {\n return bundle.pages.map((p) => parse(p.id, p)).filter((p): p is StoredPage => p !== null);\n }\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/**\n * Eén gepubliceerde pagina op pad. Bundle-first, anders keyless via REST/Admin.\n * `opts.skipBundle` (voorbeeld-modus, `?preview=1` in de visuele editor): negeert de gecommitte\n * bundel en leest rechtstreeks — de bundel komt pas met vertraging mee via de gedebouncede deploy-\n * worker, dus een beheerder die net gepubliceerd heeft ziet anders tot ~15 min lang de oude tekst.\n */\nasync function getPublishedPageByPathUncached(\n siteId: string,\n path: string,\n opts?: { skipBundle?: boolean },\n): Promise<StoredPage | null> {\n // Live-modus (niet in preview): vers uit Firestore, met de bundle als fallback bij een outage.\n if (LIVE_READS && !opts?.skipBundle) {\n // Tagged servercache (zie server/cached.ts): de publicatie-ping op de pagina-tag maakt dit\n // binnen seconden vers; tot die tijd kost een render nul Firestore-reads.\n return cachedRead([\"xicms\", \"page\", siteId, path], [cacheTags.page(siteId, path)], () => liveOrBundle(\n `getPublishedPageByPath ${siteId}${path}`,\n async () => {\n const adb = getAdminDb();\n if (adb) {\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 const rows = await restQueryThrowing(siteId, [eq(\"status\", \"published\"), eq(\"path\", path)], 1);\n return rows[0] ?? null;\n },\n () => {\n const b = getBundle();\n if (!b || b.siteId !== siteId) return null;\n const hit = getBundlePage(b, path);\n return hit ? parse(hit.id, hit) : null;\n },\n ));\n }\n const bundle = opts?.skipBundle ? null : getBundle();\n if (bundle && bundle.siteId === siteId) {\n const hit = getBundlePage(bundle, path);\n return hit ? parse(hit.id, hit) : null;\n }\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: ISR_FALLBACK, tags: [cacheTags.collection(siteId, collectionId)] },\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/* ── Nieuws ───────────────────────────────────────────────────────────────────────────────────\n * Nieuws was het enige publieke contenttype ZONDER live-tak: `getNewsPageData` (server/bundle.ts)\n * leest uitsluitend de build-time bundle. Op een live-reads-site betekende dat: je publiceert een\n * artikel en er verandert niets op de site, zonder één foutmelding (SITE-AUDIT §11). Deze lezer\n * doet voor nieuws wat `listPublishedPages` voor pagina's doet.\n */\n\n/** Gepubliceerd volgens de bundle-writer (functions/src/lib/bundle.ts) — één definitie, twee kanten. */\nfunction isPublishedNews(data: Record<string, unknown>): boolean {\n return data.published === true || data.status === \"published\";\n}\n\n/**\n * Firestore-waarden → gewone JS-waarden.\n *\n * Een Admin-SDK-read geeft `Timestamp`, `DocumentReference` en `GeoPoint` terug: klasse-instanties.\n * React weigert die van een server- naar een client-component te sturen (\"Only plain objects, and a\n * few built-ins, can be passed to Client Components\"), en het nieuws gaat via `BlockDataProvider`\n * precies die grens over. De bundle had dit nooit, want die is al JSON — dus dit kwam pas boven bij\n * het draaien: één artikel met een `serverTimestamp` en `/nieuws/<slug>` gaf een 500.\n *\n * Het REST-pad levert al gewone waarden (`decodeFields`), dus daar is dit een no-op.\n */\nfunction plain(v: unknown): unknown {\n if (v == null || typeof v !== \"object\") return v;\n if (Array.isArray(v)) return v.map(plain);\n if (v instanceof Date) return v.toISOString();\n const kandidaat = v as { toDate?: () => Date };\n if (typeof kandidaat.toDate === \"function\") return kandidaat.toDate().toISOString(); // Timestamp\n const proto = Object.getPrototypeOf(v);\n // Geen gewoon object (DocumentReference, GeoPoint, Buffer…): weglaten in plaats van doorgeven.\n if (proto !== Object.prototype && proto !== null) return undefined;\n const uit: Record<string, unknown> = {};\n for (const [k, x] of Object.entries(v as Record<string, unknown>)) {\n const w = plain(x);\n if (w !== undefined) uit[k] = w;\n }\n return uit;\n}\n\n/**\n * Rijen → artikelen: filteren, nieuwste eerst, en `bodyHtml` SANEREN.\n *\n * Dat saneren is geen extraatje maar een voorwaarde. `NewsArticleView` vertrouwt bodyHtml zodra het\n * exact overeenkomt met wat de server meegaf, en rendert het dan rechtstreeks in de SSR-HTML; alleen\n * ONvertrouwde HTML gaat door DOMPurify in de browser. De bundle-lezer saneert daarom al\n * (`getBundleNews`). Zou deze live-lezer dat overslaan, dan werd ruwe Firestore-HTML \"vertrouwd\" en\n * ongefilterd gerenderd — dan zou een live-tak toevoegen een XSS-gat openen dat er nu niet is.\n */\nfunction toPublishedNews(rows: { id: string; data: Record<string, unknown> }[]): NewsArticle[] {\n return rows\n .filter((r) => isPublishedNews(r.data))\n .map((r) => {\n const raw = r.data.bodyHtml;\n const data: Record<string, unknown> = { id: r.id, ...(plain(r.data) as Record<string, unknown>) };\n if (typeof raw === \"string\") data.bodyHtml = sanitizeRichText(raw);\n return data;\n })\n .sort((a, b) => String(b.date ?? \"\").localeCompare(String(a.date ?? \"\"))) as unknown as NewsArticle[];\n}\n\n/** Nieuws-rijen uit de gecommitte bundle (of [] als er voor deze site geen bundle is). */\nfunction bundleNewsRows(siteId: string): { id: string; data: Record<string, unknown> }[] {\n const bundle = getBundle();\n return bundle && bundle.siteId === siteId\n ? bundle.news.map((n) => ({ id: n.id, data: n as Record<string, unknown> }))\n : [];\n}\n\nasync function listPublishedNewsUncached(siteId: string): Promise<NewsArticle[]> {\n let rows: { id: string; data: Record<string, unknown> }[];\n if (LIVE_READS) {\n rows = await cachedRead([\"xicms\", \"news\", siteId], [cacheTags.collection(siteId, \"news\")], () => liveOrBundle(\n `listPublishedNews ${siteId}`,\n () => readCollectionThrowing(siteId, \"news\"),\n () => bundleNewsRows(siteId),\n ));\n } else {\n const bundle = getBundle();\n rows = bundle && bundle.siteId === siteId ? bundleNewsRows(siteId) : await readCollection(siteId, \"news\");\n }\n return toPublishedNews(rows);\n}\n\n/**\n * Gepubliceerde nieuwsartikelen, nieuwste eerst. Live-first (bundle als statische fallback) of\n * bundle-first — exact hetzelfde contract als `listPublishedPages`.\n *\n * Gebruik dit in de publieke nieuwsroutes. `getNewsPageData` blijft bestaan en doet precies wat de\n * naam zegt: de bundle lezen, synchroon — handig waar je gegarandeerd build-time data wilt (sitemap\n * bij een statische build), maar het is niet de verse bron.\n */\nexport const listPublishedNews = cache(listPublishedNewsUncached);\n\n/* ---------------- formulierdefinities (7.12) ---------------- */\n\n/**\n * Alle formulierdefinities van een site, server-side — voor de `forms`-blockData-seed\n * (server/blockData.ts). Tot 7.12 las het formulierblok zijn definitie per BEZOEKER client-side\n * (`getDoc` in de browser): één read per bezoeker per formulier, en het formulier verscheen pas\n * na hydratie. Server-side gelezen zit het formulier in de SSR-HTML en kost het één read per\n * cache-interval voor de hele site.\n *\n * Formulieren zitten niet in de content-bundle; bij een Firestore-storing levert dit gewoon een\n * lege lijst en valt het blok terug op zijn eigen client-pad (dat dan ook faalt, maar met een\n * nette \"neem contact op\"-melding).\n */\nexport async function listFormDefinitions(siteId: string): Promise<Array<{ id: string } & Record<string, unknown>>> {\n // De THROWING read, bewust: `unstable_cache` cachet uitkomsten, geen exceptions. Een reader die\n // bij een hik een lege lijst teruggeeft, zet die lege lijst een uur in de cache — en dan is één\n // haperende request een uur lang \"er zijn geen formulieren\". Gooien laat de cache leeg en de\n // aanroeper (getPageBlockData) de seed overslaan, zodat de client-fallback het overneemt.\n const rows = await cachedRead(\n [\"xicms\", \"forms\", siteId],\n [cacheTags.collection(siteId, \"forms\")],\n () => readCollectionThrowing(siteId, \"forms\"),\n );\n return rows.map((r) => ({ id: r.id, ...r.data }));\n}\n\n/** Symbool-rijen uit de gecommitte bundle (of [] als er voor deze site geen bundle is). */\nfunction bundleSymbolRows(siteId: string): { id: string; data: Record<string, unknown> }[] {\n const bundle = getBundle();\n return bundle && bundle.siteId === siteId\n ? bundle.symbols.map((s) => ({ id: s.id, data: s as Record<string, unknown> }))\n : [];\n}\n\n/** Alle symbolen van een site (gevalideerd; ongeldige overgeslagen met waarschuwing). Live-first of bundle-first. */\nasync function getSiteSymbolsUncached(siteId: string): Promise<StoredSymbol[]> {\n let rows: { id: string; data: Record<string, unknown> }[];\n if (LIVE_READS) {\n rows = await cachedRead([\"xicms\", \"symbols\", siteId], [cacheTags.collection(siteId, \"symbols\")], () => liveOrBundle(`getSiteSymbols ${siteId}`, () => readCollectionThrowing(siteId, \"symbols\"), () => bundleSymbolRows(siteId)));\n } else {\n const bundle = getBundle();\n rows = bundle && bundle.siteId === siteId ? bundleSymbolRows(siteId) : await readCollection(siteId, \"symbols\");\n }\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 *\n * Bundle-first werkt hier automatisch: deze functie componeert `getPublishedPageByPath` en\n * `getSiteSymbols`, die beide al bundle-first zijn — de i18n-overlay en expansie draaien identiek\n * op bundle- én Firestore-data.\n */\nexport async function getPublishedPageByPathExpanded(\n siteId: string,\n path: string,\n locale?: string,\n opts?: { skipBundle?: boolean },\n): Promise<StoredPage | null> {\n let page = await getPublishedPageByPath(siteId, path, opts);\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 bundleSettings = (): PublicSiteSettings => {\n const b = getBundle();\n if (!b || b.siteId !== siteId) return {};\n const parsed = PublicSiteSettingsSchema.safeParse(b.settings ?? {});\n return parsed.success ? parsed.data : {};\n };\n // Live-modus: vers uit Firestore (throwing), met de bundle-settings als fallback bij een outage.\n if (LIVE_READS) {\n return cachedRead([\"xicms\", \"settings\", siteId], [cacheTags.collection(siteId, \"settings\")], () => liveOrBundle(\n `getPublicSiteSettings ${siteId}`,\n async () => {\n const adb = getAdminDb();\n let data: unknown = {};\n if (adb) {\n const d = await adb.doc(`sites/${siteId}/settings/public`).get();\n data = d.exists ? d.data() : {};\n } else {\n const { apiKey, projectId } = restConfigOrThrow();\n const url = `https://firestore.googleapis.com/v1/projects/${projectId}/databases/(default)/documents/sites/${siteId}/settings/public?key=${apiKey}`;\n const res = await fetch(url, { next: { revalidate: ISR_FALLBACK, tags: [cacheTags.collection(siteId, \"settings\")] } });\n // 404 = document bestaat (nog) niet → lege settings; andere niet-2xx = outage → gooi (fallback).\n if (res.status === 404) data = {};\n else if (!res.ok) throw new Error(`Firestore REST-read settings/public ${res.status}`);\n else {\n const doc = (await res.json()) as { fields?: Record<string, FsValue> };\n data = decodeFields(doc.fields ?? {});\n }\n }\n const parsed = PublicSiteSettingsSchema.safeParse(data ?? {});\n return parsed.success ? parsed.data : {};\n },\n bundleSettings,\n ));\n }\n const bundle = getBundle();\n if (bundle && bundle.siteId === siteId) {\n return bundleSettings();\n }\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: ISR_FALLBACK, tags: [cacheTags.collection(siteId, \"settings\")] } });\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/** Custom-block-rijen uit de gecommitte bundle (of [] als er voor deze site geen bundle is). */\nfunction bundleCustomBlockRows(siteId: string): { id: string; data: Record<string, unknown> }[] {\n const bundle = getBundle();\n return bundle && bundle.siteId === siteId\n ? bundle.customBlocks.map((c) => ({ id: c.id, data: c as Record<string, unknown> }))\n : [];\n}\n\n/** Alle custom blocks van een site (gevalideerd) — voor SSR-registratie via XiCMSProvider. Live-first of bundle-first. */\nasync function getSiteCustomBlocksUncached(siteId: string): Promise<CustomBlock[]> {\n let rows: { id: string; data: Record<string, unknown> }[];\n if (LIVE_READS) {\n rows = await cachedRead([\"xicms\", \"customBlocks\", siteId], [cacheTags.collection(siteId, \"customBlocks\")], () => liveOrBundle(`getSiteCustomBlocks ${siteId}`, () => readCollectionThrowing(siteId, \"customBlocks\"), () => bundleCustomBlockRows(siteId)));\n } else {\n const bundle = getBundle();\n rows = bundle && bundle.siteId === siteId ? bundleCustomBlockRows(siteId) : await readCollection(siteId, \"customBlocks\");\n }\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":["adb","rows","parsed","data"]}
@@ -19,12 +19,14 @@ export type ServerShopCategory = ShopCategory & {
19
19
  };
20
20
  export type { ShopPageData } from "../modules/shop/types.js";
21
21
  import type { ShopPageData } from "../modules/shop/types.js";
22
- declare function getActiveProductsUncached(siteId: string): Promise<ServerProduct[]>;
23
- declare function getShopCategoriesUncached(siteId: string): Promise<ServerShopCategory[]>;
24
- declare function getProductBySlugUncached(siteId: string, slug: string): Promise<ServerProduct | null>;
25
- export declare const getActiveProducts: typeof getActiveProductsUncached;
26
- export declare const getShopCategories: typeof getShopCategoriesUncached;
27
- export declare const getProductBySlug: typeof getProductBySlugUncached;
22
+ /**
23
+ * Tagged servercache (zie server/cached.ts) om élk leespad (Admin én REST): de `products`-ping van
24
+ * `saveProduct`/`deleteProduct` maakt dit binnen seconden vers; tot die tijd kost een render nul
25
+ * Firestore-reads. React `cache()` blijft eromheen voor de per-request-dedup (metadata + render).
26
+ */
27
+ export declare const getActiveProducts: (siteId: string) => Promise<ServerProduct[]>;
28
+ export declare const getShopCategories: (siteId: string) => Promise<ServerShopCategory[]>;
29
+ export declare const getProductBySlug: (siteId: string, slug: string) => Promise<ServerProduct | null>;
28
30
  /**
29
31
  * Verzamelt de shop-startdata voor één pagina. `searchParams.p` (detailpagina) bepaalt of er
30
32
  * ook een enkel product geladen wordt. Geef het resultaat door als
@@ -1,6 +1,7 @@
1
1
  import "server-only";
2
2
  import { cache } from "react";
3
3
  import { getAdminDb } from "../firebase/admin.js";
4
+ import { cachedRead } from "./cached.js";
4
5
  import {
5
6
  ProductSchema,
6
7
  ShopCategorySchema,
@@ -40,7 +41,7 @@ function decodeFields(fields) {
40
41
  async function restQuery(siteId, collectionId, filters, limit) {
41
42
  const apiKey = process.env.NEXT_PUBLIC_FIREBASE_API_KEY;
42
43
  const projectId = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID;
43
- if (!apiKey || !projectId) return [];
44
+ if (!apiKey || !projectId) throw new Error("Firebase REST-config ontbreekt (apiKey/projectId)");
44
45
  const url = `https://firestore.googleapis.com/v1/projects/${projectId}/databases/(default)/documents/sites/${siteId}:runQuery?key=${apiKey}`;
45
46
  const fieldFilters = filters.map((f) => ({
46
47
  fieldFilter: { field: { fieldPath: f.fieldPath }, op: "EQUAL", value: { stringValue: f.value } }
@@ -59,12 +60,11 @@ async function restQuery(siteId, collectionId, filters, limit) {
59
60
  // pure verspilling.
60
61
  next: { revalidate: Number(process.env.XICMS_ISR_FALLBACK) || 3600, tags: [`xicms:${siteId}:${collectionId}`] }
61
62
  });
62
- if (!res.ok) return [];
63
+ if (!res.ok) throw new Error(`Firestore REST-query (shop) ${res.status}`);
63
64
  const rows = await res.json();
64
65
  return rows.filter((r) => r.document).map((r) => ({ id: r.document.name.split("/").pop(), data: decodeFields(r.document.fields ?? {}) }));
65
66
  } catch (err) {
66
- console.error("[xicms] Firestore REST-query (shop) mislukt:", err);
67
- return [];
67
+ throw err instanceof Error ? err : new Error(String(err));
68
68
  }
69
69
  }
70
70
  async function getActiveProductsUncached(siteId) {
@@ -75,8 +75,7 @@ async function getActiveProductsUncached(siteId) {
75
75
  const snap = await adb.collection(`sites/${siteId}/products`).where("status", "==", "active").limit(500).get();
76
76
  docs = snap.docs.map((d) => ({ id: d.id, data: d.data() }));
77
77
  } catch (e) {
78
- console.warn("[xicms] Admin-read producten mislukt \u2014 lege lijst:", e?.message ?? e);
79
- docs = [];
78
+ throw new Error(`Admin-read producten mislukt: ${e?.message ?? e}`);
80
79
  }
81
80
  } else {
82
81
  docs = await restQuery(siteId, "products", [{ fieldPath: "status", value: "active" }], 500);
@@ -91,8 +90,7 @@ async function getShopCategoriesUncached(siteId) {
91
90
  const snap = await adb.collection(`sites/${siteId}/shopCategories`).limit(100).get();
92
91
  docs = snap.docs.map((d) => ({ id: d.id, data: d.data() }));
93
92
  } catch (e) {
94
- console.warn("[xicms] Admin-read categorie\xEBn mislukt \u2014 lege lijst:", e?.message ?? e);
95
- docs = [];
93
+ throw new Error(`Admin-read categorie\xEBn mislukt: ${e?.message ?? e}`);
96
94
  }
97
95
  } else {
98
96
  docs = await restQuery(siteId, "shopCategories", [], 100);
@@ -108,8 +106,7 @@ async function getProductBySlugUncached(siteId, slug) {
108
106
  const snap = await adb.collection(`sites/${siteId}/products`).where("status", "==", "active").where("slug", "==", slug).limit(1).get();
109
107
  docs = snap.docs.map((d2) => ({ id: d2.id, data: d2.data() }));
110
108
  } catch (e) {
111
- console.warn("[xicms] Admin-read product mislukt:", e?.message ?? e);
112
- docs = [];
109
+ throw new Error(`Admin-read product mislukt: ${e?.message ?? e}`);
113
110
  }
114
111
  } else {
115
112
  docs = await restQuery(
@@ -125,9 +122,15 @@ async function getProductBySlugUncached(siteId, slug) {
125
122
  const d = docs[0];
126
123
  return d ? parseProduct(d.id, d.data) : null;
127
124
  }
128
- const getActiveProducts = cache(getActiveProductsUncached);
129
- const getShopCategories = cache(getShopCategoriesUncached);
130
- const getProductBySlug = cache(getProductBySlugUncached);
125
+ const getActiveProducts = cache(
126
+ (siteId) => cachedRead(["xicms", "shop", "products", siteId], [`xicms:${siteId}:products`], () => getActiveProductsUncached(siteId))
127
+ );
128
+ const getShopCategories = cache(
129
+ (siteId) => cachedRead(["xicms", "shop", "categories", siteId], [`xicms:${siteId}:shopCategories`], () => getShopCategoriesUncached(siteId))
130
+ );
131
+ const getProductBySlug = cache(
132
+ (siteId, slug) => cachedRead(["xicms", "shop", "product", siteId, slug], [`xicms:${siteId}:products`], () => getProductBySlugUncached(siteId, slug))
133
+ );
131
134
  async function getShopPageData(siteId, searchParams) {
132
135
  const slugParam = searchParams?.p;
133
136
  const slug = Array.isArray(slugParam) ? slugParam[0] : slugParam;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/server/shop.ts"],"sourcesContent":["import \"server-only\";\n\nimport { cache } from \"react\";\nimport type { Metadata } from \"next\";\n\nimport { getAdminDb } from \"../firebase/admin\";\nimport {\n ProductSchema,\n ShopCategorySchema,\n productPriceCents,\n type Product,\n type ShopCategory,\n} from \"../schema/shop\";\n\n/**\n * Server-readers voor de shop (5.6) — zelfde contract als server/pages.ts: Admin SDK als die\n * er is, anders keyless Firestore-REST met de publieke web-apiKey (de rules staan publieke\n * reads op actieve producten/categorieën toe). Reads zijn per-request gededupt via cache();\n * route-level ISR (`revalidate`) dempt de kosten tussen requests.\n *\n * Gebruik in de klantsite (catch-all):\n * const blockData = { shop: await getShopPageData(siteId, path, searchParams) };\n * <PublishedPage page={page} siteId={siteId} blockData={blockData} />\n */\n\nexport type ServerProduct = Product & { id: string };\nexport type ServerShopCategory = ShopCategory & { id: string };\n\n// Gedeelde vorm met de blocks (useBlockData(\"shop\")) — één bron van waarheid.\nexport type { ShopPageData } from \"../modules/shop/types\";\nimport type { ShopPageData } from \"../modules/shop/types\";\n\nfunction parseProduct(id: string, data: unknown): ServerProduct | null {\n const res = ProductSchema.safeParse(data);\n if (!res.success) {\n console.warn(`[xicms] product \"${id}\" faalt schema-validatie en wordt overgeslagen:`, res.error.issues[0]);\n return null;\n }\n return { id, ...res.data };\n}\n\nfunction parseCategory(id: string, data: unknown): ServerShopCategory | null {\n const res = ShopCategorySchema.safeParse(data);\n if (!res.success) {\n console.warn(`[xicms] categorie \"${id}\" faalt schema-validatie en wordt overgeslagen:`, res.error.issues[0]);\n return null;\n }\n return { id, ...res.data };\n}\n\n/* ── keyless REST (zelfde aanpak als server/pages.ts) ────────────────────── */\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 (\"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 RestDoc {\n id: string;\n data: Record<string, unknown>;\n}\n\nasync function restQuery(\n siteId: string,\n collectionId: string,\n filters: { fieldPath: string; value: string }[],\n limit?: number,\n): Promise<RestDoc[]> {\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 fieldFilters = filters.map((f) => ({\n fieldFilter: { field: { fieldPath: f.fieldPath }, op: \"EQUAL\", value: { stringValue: f.value } },\n }));\n const where =\n fieldFilters.length === 0\n ? undefined\n : fieldFilters.length === 1\n ? fieldFilters[0]\n : { compositeFilter: { op: \"AND\", filters: fieldFilters } };\n const structuredQuery: Record<string, unknown> = { from: [{ collectionId }] };\n if (where) structuredQuery.where = where;\n if (limit) structuredQuery.limit = limit;\n try {\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ structuredQuery }),\n // Zelfde vangnet-logica als pages.ts: de tag is het normale pad, de timer dekt een\n // gemiste revalidate-push. Shop-content wijzigt zelden; elke 5 minuten opnieuw tellen was\n // pure verspilling.\n next: { revalidate: Number(process.env.XICMS_ISR_FALLBACK) || 3600, tags: [`xicms:${siteId}:${collectionId}`] },\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-query (shop) mislukt:\", err);\n return [];\n }\n}\n\n/* ── Readers ─────────────────────────────────────────────────────────────── */\n\nasync function getActiveProductsUncached(siteId: string): Promise<ServerProduct[]> {\n const adb = getAdminDb();\n let docs: RestDoc[];\n if (adb) {\n try {\n const snap = await adb.collection(`sites/${siteId}/products`).where(\"status\", \"==\", \"active\").limit(500).get();\n docs = snap.docs.map((d) => ({ id: d.id, data: d.data() }));\n } catch (e) {\n console.warn(\"[xicms] Admin-read producten mislukt — lege lijst:\", (e as Error)?.message ?? e);\n docs = [];\n }\n } else {\n docs = await restQuery(siteId, \"products\", [{ fieldPath: \"status\", value: \"active\" }], 500);\n }\n return docs\n .map((d) => parseProduct(d.id, d.data))\n .filter((p): p is ServerProduct => p !== null)\n .sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.name.localeCompare(b.name, \"nl\"));\n}\n\nasync function getShopCategoriesUncached(siteId: string): Promise<ServerShopCategory[]> {\n const adb = getAdminDb();\n let docs: RestDoc[];\n if (adb) {\n try {\n const snap = await adb.collection(`sites/${siteId}/shopCategories`).limit(100).get();\n docs = snap.docs.map((d) => ({ id: d.id, data: d.data() }));\n } catch (e) {\n console.warn(\"[xicms] Admin-read categorieën mislukt — lege lijst:\", (e as Error)?.message ?? e);\n docs = [];\n }\n } else {\n docs = await restQuery(siteId, \"shopCategories\", [], 100);\n }\n return docs\n .map((d) => parseCategory(d.id, d.data))\n .filter((c): c is ServerShopCategory => c !== null && c.visible !== false)\n .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));\n}\n\nasync function getProductBySlugUncached(siteId: string, slug: string): Promise<ServerProduct | null> {\n if (!slug) return null;\n const adb = getAdminDb();\n let docs: RestDoc[];\n if (adb) {\n try {\n const snap = await adb\n .collection(`sites/${siteId}/products`)\n .where(\"status\", \"==\", \"active\")\n .where(\"slug\", \"==\", slug)\n .limit(1)\n .get();\n docs = snap.docs.map((d) => ({ id: d.id, data: d.data() }));\n } catch (e) {\n console.warn(\"[xicms] Admin-read product mislukt:\", (e as Error)?.message ?? e);\n docs = [];\n }\n } else {\n docs = await restQuery(\n siteId,\n \"products\",\n [\n { fieldPath: \"status\", value: \"active\" },\n { fieldPath: \"slug\", value: slug },\n ],\n 1,\n );\n }\n const d = docs[0];\n return d ? parseProduct(d.id, d.data) : null;\n}\n\nexport const getActiveProducts = cache(getActiveProductsUncached);\nexport const getShopCategories = cache(getShopCategoriesUncached);\nexport const getProductBySlug = cache(getProductBySlugUncached);\n\n/**\n * Verzamelt de shop-startdata voor één pagina. `searchParams.p` (detailpagina) bepaalt of er\n * ook een enkel product geladen wordt. Geef het resultaat door als\n * `<PublishedPage blockData={{ shop: data }}>`.\n */\nexport async function getShopPageData(\n siteId: string,\n searchParams?: { p?: string | string[]; [k: string]: unknown },\n): Promise<ShopPageData> {\n const slugParam = searchParams?.p;\n const slug = Array.isArray(slugParam) ? slugParam[0] : slugParam;\n const [products, categories, product] = await Promise.all([\n getActiveProducts(siteId),\n getShopCategories(siteId),\n slug ? getProductBySlug(siteId, slug) : Promise.resolve(undefined),\n ]);\n const data: ShopPageData = { products, categories };\n if (slug !== undefined) data.product = product ?? null;\n return data;\n}\n\n/* ── SEO ─────────────────────────────────────────────────────────────────── */\n\n/** Metadata voor een productdetailpagina (title/description/OG-beeld). */\nexport function buildProductMetadata(\n product: ServerProduct | null,\n site: { name: string; domain?: string },\n): Metadata {\n if (!product) return { title: `Product — ${site.name}` };\n const description = (product.description || `${product.name} — ${site.name}`)\n .replace(/\\s+/g, \" \")\n .slice(0, 300);\n const image = product.images?.[0];\n return {\n title: `${product.name} — ${site.name}`,\n description,\n openGraph: {\n title: product.name,\n description,\n type: \"website\",\n ...(image ? { images: [{ url: image }] } : {}),\n },\n };\n}\n\n/** schema.org Product/Offer-JSON-LD met echte prijs en beschikbaarheid. */\nexport function buildProductJsonLd(\n product: ServerProduct,\n site: { name: string; domain?: string },\n currency = \"EUR\",\n): Record<string, unknown> {\n const soldOut =\n product.trackStock === true &&\n (product.options ?? []).length === 0 &&\n product.stock !== undefined &&\n product.stock <= 0;\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Product\",\n name: product.name,\n ...(product.description ? { description: product.description.slice(0, 500) } : {}),\n ...(product.images?.length ? { image: product.images } : {}),\n offers: {\n \"@type\": \"Offer\",\n price: (productPriceCents(product) / 100).toFixed(2),\n priceCurrency: currency,\n availability: soldOut ? \"https://schema.org/OutOfStock\" : \"https://schema.org/InStock\",\n ...(site.domain ? { url: `https://${site.domain}/product?p=${encodeURIComponent(product.slug)}` } : {}),\n },\n brand: { \"@type\": \"Brand\", name: site.name },\n };\n}\n"],"mappings":"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","names":["d"]}
1
+ {"version":3,"sources":["../../src/server/shop.ts"],"sourcesContent":["import \"server-only\";\n\nimport { cache } from \"react\";\nimport type { Metadata } from \"next\";\n\nimport { getAdminDb } from \"../firebase/admin\";\nimport { cachedRead } from \"./cached\";\nimport {\n ProductSchema,\n ShopCategorySchema,\n productPriceCents,\n type Product,\n type ShopCategory,\n} from \"../schema/shop\";\n\n/**\n * Server-readers voor de shop (5.6) — zelfde contract als server/pages.ts: Admin SDK als die\n * er is, anders keyless Firestore-REST met de publieke web-apiKey (de rules staan publieke\n * reads op actieve producten/categorieën toe). Reads zijn per-request gededupt via cache();\n * route-level ISR (`revalidate`) dempt de kosten tussen requests.\n *\n * Gebruik in de klantsite (catch-all):\n * const blockData = { shop: await getShopPageData(siteId, path, searchParams) };\n * <PublishedPage page={page} siteId={siteId} blockData={blockData} />\n */\n\nexport type ServerProduct = Product & { id: string };\nexport type ServerShopCategory = ShopCategory & { id: string };\n\n// Gedeelde vorm met de blocks (useBlockData(\"shop\")) — één bron van waarheid.\nexport type { ShopPageData } from \"../modules/shop/types\";\nimport type { ShopPageData } from \"../modules/shop/types\";\n\nfunction parseProduct(id: string, data: unknown): ServerProduct | null {\n const res = ProductSchema.safeParse(data);\n if (!res.success) {\n console.warn(`[xicms] product \"${id}\" faalt schema-validatie en wordt overgeslagen:`, res.error.issues[0]);\n return null;\n }\n return { id, ...res.data };\n}\n\nfunction parseCategory(id: string, data: unknown): ServerShopCategory | null {\n const res = ShopCategorySchema.safeParse(data);\n if (!res.success) {\n console.warn(`[xicms] categorie \"${id}\" faalt schema-validatie en wordt overgeslagen:`, res.error.issues[0]);\n return null;\n }\n return { id, ...res.data };\n}\n\n/* ── keyless REST (zelfde aanpak als server/pages.ts) ────────────────────── */\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 (\"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 RestDoc {\n id: string;\n data: Record<string, unknown>;\n}\n\nasync function restQuery(\n siteId: string,\n collectionId: string,\n filters: { fieldPath: string; value: string }[],\n limit?: number,\n): Promise<RestDoc[]> {\n const apiKey = process.env.NEXT_PUBLIC_FIREBASE_API_KEY;\n const projectId = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID;\n if (!apiKey || !projectId) throw new Error(\"Firebase REST-config ontbreekt (apiKey/projectId)\");\n const url = `https://firestore.googleapis.com/v1/projects/${projectId}/databases/(default)/documents/sites/${siteId}:runQuery?key=${apiKey}`;\n const fieldFilters = filters.map((f) => ({\n fieldFilter: { field: { fieldPath: f.fieldPath }, op: \"EQUAL\", value: { stringValue: f.value } },\n }));\n const where =\n fieldFilters.length === 0\n ? undefined\n : fieldFilters.length === 1\n ? fieldFilters[0]\n : { compositeFilter: { op: \"AND\", filters: fieldFilters } };\n const structuredQuery: Record<string, unknown> = { from: [{ collectionId }] };\n if (where) structuredQuery.where = where;\n if (limit) structuredQuery.limit = limit;\n try {\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ structuredQuery }),\n // Zelfde vangnet-logica als pages.ts: de tag is het normale pad, de timer dekt een\n // gemiste revalidate-push. Shop-content wijzigt zelden; elke 5 minuten opnieuw tellen was\n // pure verspilling.\n next: { revalidate: Number(process.env.XICMS_ISR_FALLBACK) || 3600, tags: [`xicms:${siteId}:${collectionId}`] },\n });\n if (!res.ok) throw new Error(`Firestore REST-query (shop) ${res.status}`);\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 // Door naar de aanroeper: uitkomsten worden gecached, fouten niet (zie server/cached.ts).\n throw err instanceof Error ? err : new Error(String(err));\n }\n}\n\n/* ── Readers ─────────────────────────────────────────────────────────────── */\n\nasync function getActiveProductsUncached(siteId: string): Promise<ServerProduct[]> {\n const adb = getAdminDb();\n let docs: RestDoc[];\n if (adb) {\n try {\n const snap = await adb.collection(`sites/${siteId}/products`).where(\"status\", \"==\", \"active\").limit(500).get();\n docs = snap.docs.map((d) => ({ id: d.id, data: d.data() }));\n } catch (e) {\n // Gooien, niet [] — een lege uitkomst zou door de servercache een uur worden vastgehouden.\n throw new Error(`Admin-read producten mislukt: ${(e as Error)?.message ?? e}`);\n }\n } else {\n docs = await restQuery(siteId, \"products\", [{ fieldPath: \"status\", value: \"active\" }], 500);\n }\n return docs\n .map((d) => parseProduct(d.id, d.data))\n .filter((p): p is ServerProduct => p !== null)\n .sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.name.localeCompare(b.name, \"nl\"));\n}\n\nasync function getShopCategoriesUncached(siteId: string): Promise<ServerShopCategory[]> {\n const adb = getAdminDb();\n let docs: RestDoc[];\n if (adb) {\n try {\n const snap = await adb.collection(`sites/${siteId}/shopCategories`).limit(100).get();\n docs = snap.docs.map((d) => ({ id: d.id, data: d.data() }));\n } catch (e) {\n throw new Error(`Admin-read categorieën mislukt: ${(e as Error)?.message ?? e}`);\n }\n } else {\n docs = await restQuery(siteId, \"shopCategories\", [], 100);\n }\n return docs\n .map((d) => parseCategory(d.id, d.data))\n .filter((c): c is ServerShopCategory => c !== null && c.visible !== false)\n .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));\n}\n\nasync function getProductBySlugUncached(siteId: string, slug: string): Promise<ServerProduct | null> {\n if (!slug) return null;\n const adb = getAdminDb();\n let docs: RestDoc[];\n if (adb) {\n try {\n const snap = await adb\n .collection(`sites/${siteId}/products`)\n .where(\"status\", \"==\", \"active\")\n .where(\"slug\", \"==\", slug)\n .limit(1)\n .get();\n docs = snap.docs.map((d) => ({ id: d.id, data: d.data() }));\n } catch (e) {\n throw new Error(`Admin-read product mislukt: ${(e as Error)?.message ?? e}`);\n }\n } else {\n docs = await restQuery(\n siteId,\n \"products\",\n [\n { fieldPath: \"status\", value: \"active\" },\n { fieldPath: \"slug\", value: slug },\n ],\n 1,\n );\n }\n const d = docs[0];\n return d ? parseProduct(d.id, d.data) : null;\n}\n\n/**\n * Tagged servercache (zie server/cached.ts) om élk leespad (Admin én REST): de `products`-ping van\n * `saveProduct`/`deleteProduct` maakt dit binnen seconden vers; tot die tijd kost een render nul\n * Firestore-reads. React `cache()` blijft eromheen voor de per-request-dedup (metadata + render).\n */\nexport const getActiveProducts = cache((siteId: string) =>\n cachedRead([\"xicms\", \"shop\", \"products\", siteId], [`xicms:${siteId}:products`], () => getActiveProductsUncached(siteId)),\n);\nexport const getShopCategories = cache((siteId: string) =>\n cachedRead([\"xicms\", \"shop\", \"categories\", siteId], [`xicms:${siteId}:shopCategories`], () => getShopCategoriesUncached(siteId)),\n);\nexport const getProductBySlug = cache((siteId: string, slug: string) =>\n cachedRead([\"xicms\", \"shop\", \"product\", siteId, slug], [`xicms:${siteId}:products`], () => getProductBySlugUncached(siteId, slug)),\n);\n\n/**\n * Verzamelt de shop-startdata voor één pagina. `searchParams.p` (detailpagina) bepaalt of er\n * ook een enkel product geladen wordt. Geef het resultaat door als\n * `<PublishedPage blockData={{ shop: data }}>`.\n */\nexport async function getShopPageData(\n siteId: string,\n searchParams?: { p?: string | string[]; [k: string]: unknown },\n): Promise<ShopPageData> {\n const slugParam = searchParams?.p;\n const slug = Array.isArray(slugParam) ? slugParam[0] : slugParam;\n const [products, categories, product] = await Promise.all([\n getActiveProducts(siteId),\n getShopCategories(siteId),\n slug ? getProductBySlug(siteId, slug) : Promise.resolve(undefined),\n ]);\n const data: ShopPageData = { products, categories };\n if (slug !== undefined) data.product = product ?? null;\n return data;\n}\n\n/* ── SEO ─────────────────────────────────────────────────────────────────── */\n\n/** Metadata voor een productdetailpagina (title/description/OG-beeld). */\nexport function buildProductMetadata(\n product: ServerProduct | null,\n site: { name: string; domain?: string },\n): Metadata {\n if (!product) return { title: `Product — ${site.name}` };\n const description = (product.description || `${product.name} — ${site.name}`)\n .replace(/\\s+/g, \" \")\n .slice(0, 300);\n const image = product.images?.[0];\n return {\n title: `${product.name} — ${site.name}`,\n description,\n openGraph: {\n title: product.name,\n description,\n type: \"website\",\n ...(image ? { images: [{ url: image }] } : {}),\n },\n };\n}\n\n/** schema.org Product/Offer-JSON-LD met echte prijs en beschikbaarheid. */\nexport function buildProductJsonLd(\n product: ServerProduct,\n site: { name: string; domain?: string },\n currency = \"EUR\",\n): Record<string, unknown> {\n const soldOut =\n product.trackStock === true &&\n (product.options ?? []).length === 0 &&\n product.stock !== undefined &&\n product.stock <= 0;\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Product\",\n name: product.name,\n ...(product.description ? { description: product.description.slice(0, 500) } : {}),\n ...(product.images?.length ? { image: product.images } : {}),\n offers: {\n \"@type\": \"Offer\",\n price: (productPriceCents(product) / 100).toFixed(2),\n priceCurrency: currency,\n availability: soldOut ? \"https://schema.org/OutOfStock\" : \"https://schema.org/InStock\",\n ...(site.domain ? { url: `https://${site.domain}/product?p=${encodeURIComponent(product.slug)}` } : {}),\n },\n brand: { \"@type\": \"Brand\", name: site.name },\n };\n}\n"],"mappings":"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","names":["d"]}
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
- const CORE_VERSION = true ? "7.11.0" : "0.0.0-dev";
1
+ const CORE_VERSION = true ? "7.12.1" : "0.0.0-dev";
2
2
  const CORE_PACKAGE = "@xiwebdesign/xicms-core";
3
3
  function isNieuwer(beschikbaar, huidig) {
4
4
  const delen = (v) => v.split("-")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiwebdesign/xicms-core",
3
- "version": "7.11.0",
3
+ "version": "7.12.1",
4
4
  "description": "XiCMS engine: gestructureerde content, pagina's, draft/publish, admin (TipTap+Radix), SEO en beveiliging voor klantsites.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {