@xiwebdesign/xicms-core 5.0.6 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/XiCMSProvider.d.ts +1 -1
- package/dist/XiCMSProvider.js +42 -30
- package/dist/XiCMSProvider.js.map +1 -1
- package/dist/admin/editors/RichTextEditor.js +7 -1
- package/dist/admin/editors/RichTextEditor.js.map +1 -1
- package/dist/admin/ui/ArrayEditor.d.ts +6 -1
- package/dist/admin/ui/ArrayEditor.js +22 -3
- package/dist/admin/ui/ArrayEditor.js.map +1 -1
- package/dist/admin/visual/VisualEditor.js +104 -12
- package/dist/admin/visual/VisualEditor.js.map +1 -1
- package/dist/admin.css +1 -1
- package/dist/blocks/builtins/Agenda.js +2 -1
- package/dist/blocks/builtins/Agenda.js.map +1 -1
- package/dist/blocks/builtins/Breadcrumbs.js +2 -1
- package/dist/blocks/builtins/Breadcrumbs.js.map +1 -1
- package/dist/blocks/builtins/Contact.js +2 -1
- package/dist/blocks/builtins/Contact.js.map +1 -1
- package/dist/blocks/builtins/Faq.js +2 -1
- package/dist/blocks/builtins/Faq.js.map +1 -1
- package/dist/blocks/builtins/Jobs.js +2 -1
- package/dist/blocks/builtins/Jobs.js.map +1 -1
- package/dist/blocks/builtins/OpeningHours.js +2 -1
- package/dist/blocks/builtins/OpeningHours.js.map +1 -1
- package/dist/blocks/builtins/Pricing.js +2 -1
- package/dist/blocks/builtins/Pricing.js.map +1 -1
- package/dist/blocks/builtins/Search.js +18 -8
- package/dist/blocks/builtins/Search.js.map +1 -1
- package/dist/blocks/builtins/Team.js +2 -1
- package/dist/blocks/builtins/Team.js.map +1 -1
- package/dist/blocks/builtins/Testimonials.js +2 -1
- package/dist/blocks/builtins/Testimonials.js.map +1 -1
- package/dist/blocks/builtins/jsonld.d.ts +9 -0
- package/dist/blocks/builtins/jsonld.js +7 -0
- package/dist/blocks/builtins/jsonld.js.map +1 -0
- package/dist/components/News.js +2 -1
- package/dist/components/News.js.map +1 -1
- package/dist/i18n/overlay.d.ts +13 -4
- package/dist/i18n/overlay.js +54 -8
- package/dist/i18n/overlay.js.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/pages.js +34 -13
- package/dist/server/pages.js.map +1 -1
- package/dist/styles.css +37 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/News.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { collection, getDocs, limit as fbLimit, onSnapshot, orderBy, query, where } from \"firebase/firestore\";\nimport { getDb } from \"../firebase/client\";\nimport { useSiteConfig } from \"../config\";\nimport DOMPurify from \"dompurify\";\nimport { useAuth } from \"../auth/AuthProvider\";\nimport { canEdit } from \"../auth/roles\";\nimport { isDemoActive, getDemoNews } from \"../demo/demoStore\";\nimport { Img } from \"../media/ImageContext\";\nimport type { NewsArticle } from \"../content/types\";\n\n/**\n * Lijst van nieuwsartikelen voor de huidige site.\n * - `live: false` (standaard publiek): één keer ophalen met getDocs → geen per-bezoeker listener\n * (scheelt Firestore-kosten op druk bezochte sites).\n * - `live: true` (admin): realtime via onSnapshot, zodat wijzigingen meteen zichtbaar zijn.\n */\nexport function useNews(opts: { onlyPublished?: boolean; live?: boolean; max?: number } = {}): {\n articles: NewsArticle[];\n loading: boolean;\n} {\n const { siteId } = useSiteConfig();\n // Via useAuth (niet via het deprecated ContentProvider-pad — News hoort niet aan legacy te hangen).\n const { role } = useAuth();\n const editor = canEdit(role);\n const [articles, setArticles] = useState<NewsArticle[]>([]);\n const [loading, setLoading] = useState(true);\n const onlyPublished = opts.onlyPublished ?? !editor;\n const live = opts.live ?? editor; // editors zien live; publiek één keer\n // Begrens de query: zonder cap zou een nieuws-rijke site ALLE artikelen downloaden bij elk bezoek.\n const cap = Math.min(opts.max ?? 50, 50);\n\n useEffect(() => {\n // Demo-modus: nieuws komt uit de lokale seed (geen Firebase), zodat nieuws-blokken/widgets\n // in de demo ook gevuld zijn.\n if (isDemoActive()) {\n const all = getDemoNews(siteId);\n setArticles((onlyPublished ? all.filter((a) => a.published) : all).slice(0, cap));\n setLoading(false);\n return;\n }\n const db = getDb();\n if (!db) {\n setLoading(false);\n return;\n }\n const base = collection(db, \"sites\", siteId, \"news\");\n const q = onlyPublished\n ? query(base, where(\"published\", \"==\", true), orderBy(\"date\", \"desc\"), fbLimit(cap))\n : query(base, orderBy(\"date\", \"desc\"), fbLimit(cap));\n const map = (snap: { docs: { id: string; data: () => unknown }[] }) =>\n setArticles(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Omit<NewsArticle, \"id\">) })));\n\n if (!live) {\n let active = true;\n getDocs(q)\n .then((snap) => {\n if (active) {\n map(snap);\n setLoading(false);\n }\n })\n .catch(() => active && setLoading(false));\n return () => {\n active = false;\n };\n }\n\n const unsub = onSnapshot(\n q,\n (snap) => {\n map(snap);\n setLoading(false);\n },\n () => setLoading(false),\n );\n return () => unsub();\n }, [siteId, onlyPublished, live, cap]);\n\n return { articles, loading };\n}\n\ninterface NewsListProps {\n /** Basispad voor links naar een artikel, bv. \"/nieuws\". */\n basePath?: string;\n className?: string;\n emptyLabel?: string;\n /** Maximaal aantal artikelen om te tonen. */\n limit?: number;\n /** Toon eerst dit aantal en laad de rest met een \"Meer laden\"-knop (0/leeg = alles ineens). */\n pageSize?: number;\n}\n\n/** Standaardweergave van nieuwsartikelen als kaartenlijst, optioneel gepagineerd. */\nexport function NewsList({ basePath = \"/nieuws\", className, emptyLabel = \"Nog geen nieuws.\", limit, pageSize }: NewsListProps) {\n const { articles, loading } = useNews({ max: limit });\n const paged = typeof pageSize === \"number\" && pageSize > 0;\n const [visible, setVisible] = useState(paged ? pageSize : Infinity);\n\n if (loading) return <p>Nieuws laden…</p>;\n if (articles.length === 0) return <p>{emptyLabel}</p>;\n\n const all = typeof limit === \"number\" ? articles.slice(0, limit) : articles;\n const shown = paged ? all.slice(0, visible) : all;\n\n return (\n <div className={className} style={{ display: \"grid\", gap: \"1rem\" }}>\n {shown.map((a, i) => (\n <article key={a.id} className=\"xicms-card\" style={{ padding: \"1.25rem\" }}>\n {a.coverUrl && (\n // Eerste cover boven de vouw = vaak de LCP → priority (geen lazy-load-vertraging).\n <Img src={a.coverUrl} alt=\"\" width={1200} height={700} sizes=\"(max-width: 720px) 100vw, 33vw\" priority={i === 0} style={{ width: \"100%\", height: \"auto\", borderRadius: 8, marginBottom: \"0.75rem\" }} />\n )}\n <p style={{ fontSize: 12, opacity: 0.7, margin: 0 }}>{a.date}</p>\n {/* h2: de lijst staat direct onder de pagina-h1 — h3 zou een kopniveau overslaan. */}\n <h2 style={{ margin: \"0.25rem 0\", fontSize: \"1.35rem\" }}>\n <a href={`${basePath}/${a.slug ?? a.id}`}>{a.title}</a>\n {!a.published && <span style={{ marginLeft: 8, fontSize: 12, color: \"#b45309\" }}>(concept)</span>}\n </h2>\n {a.excerpt && <p style={{ margin: 0, opacity: 0.85 }}>{a.excerpt}</p>}\n </article>\n ))}\n {paged && shown.length < all.length && (\n <div style={{ textAlign: \"center\" }}>\n <button type=\"button\" className=\"xicms-btn\" onClick={() => setVisible((v) => v + (pageSize as number))}>\n Meer laden ({all.length - shown.length})\n </button>\n </div>\n )}\n </div>\n );\n}\n\n/** BlogPosting-JSON-LD voor een artikel — puur, zodat het testbaar is. */\nexport function articleBlogPostingJsonLd(article: NewsArticle, opts: { siteName?: string; url?: string } = {}): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"BlogPosting\",\n headline: article.title,\n datePublished: article.date,\n ...(article.excerpt ? { description: article.excerpt } : {}),\n ...(article.coverUrl ? { image: article.coverUrl } : {}),\n ...(opts.url ? { url: opts.url, mainEntityOfPage: opts.url } : {}),\n ...(opts.siteName ? { publisher: { \"@type\": \"Organization\", name: opts.siteName } } : {}),\n };\n}\n\n/** Detailweergave van één artikel, met BlogPosting-JSON-LD voor zoekmachines. */\nexport function NewsArticleView({ article, jsonLd = true }: { article: NewsArticle; jsonLd?: boolean }) {\n const { name } = useSiteConfig();\n // Rich-text wordt bij het RENDEREN gesanitized (DOMPurify) — de render is de veiligheidsgrens,\n // ook als er ooit ongesaneerde HTML in Firestore zou belanden. DOMPurify heeft echter een echte\n // DOM nodig: op de server (SSR/prerender) is sanitize geen functie. Daarom rendert de eerste\n // render (server én client-hydration, identiek) de platte-tekst-fallback en wisselt het effect\n // — client-only — naar de gesanitizede rich-text.\n const [html, setHtml] = useState<string | null>(null);\n useEffect(() => {\n setHtml(article.bodyHtml ? DOMPurify.sanitize(article.bodyHtml) : null);\n }, [article.bodyHtml]);\n // Fallback tot de sanitizer klaar is: kale tekst (tags gestript, gerenderd als tekstnodes — veilig).\n const fallbackText = article.body ?? (article.bodyHtml ? article.bodyHtml.replace(/<[^>]*>/g, \" \") : \"\");\n return (\n <article>\n <p style={{ fontSize: 12, opacity: 0.6 }}>{article.date}</p>\n <h1>{article.title}</h1>\n {article.coverUrl && (\n <Img src={article.coverUrl} alt=\"\" width={1200} height={700} sizes=\"(max-width: 900px) 100vw, 800px\" priority style={{ width: \"100%\", height: \"auto\", borderRadius: 12, margin: \"1rem 0\" }} />\n )}\n {html ? (\n // eslint-disable-next-line react/no-danger\n <div className=\"xicms-richtext\" dangerouslySetInnerHTML={{ __html: html }} />\n ) : (\n fallbackText.split(/\\n{2,}/).map((p, i) => (\n <p key={i} style={{ whiteSpace: \"pre-wrap\" }}>\n {p}\n </p>\n ))\n )}\n {jsonLd && (\n <script\n type=\"application/ld+json\"\n // eslint-disable-next-line react/no-danger\n dangerouslySetInnerHTML={{ __html: JSON.stringify(articleBlogPostingJsonLd(article, { siteName: name })) }}\n />\n )}\n </article>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/News.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { collection, getDocs, limit as fbLimit, onSnapshot, orderBy, query, where } from \"firebase/firestore\";\nimport { getDb } from \"../firebase/client\";\nimport { useSiteConfig } from \"../config\";\nimport DOMPurify from \"dompurify\";\nimport { useAuth } from \"../auth/AuthProvider\";\nimport { canEdit } from \"../auth/roles\";\nimport { isDemoActive, getDemoNews } from \"../demo/demoStore\";\nimport { Img } from \"../media/ImageContext\";\nimport type { NewsArticle } from \"../content/types\";\nimport { jsonLdHtml } from \"../blocks/builtins/jsonld\";\n\n/**\n * Lijst van nieuwsartikelen voor de huidige site.\n * - `live: false` (standaard publiek): één keer ophalen met getDocs → geen per-bezoeker listener\n * (scheelt Firestore-kosten op druk bezochte sites).\n * - `live: true` (admin): realtime via onSnapshot, zodat wijzigingen meteen zichtbaar zijn.\n */\nexport function useNews(opts: { onlyPublished?: boolean; live?: boolean; max?: number } = {}): {\n articles: NewsArticle[];\n loading: boolean;\n} {\n const { siteId } = useSiteConfig();\n // Via useAuth (niet via het deprecated ContentProvider-pad — News hoort niet aan legacy te hangen).\n const { role } = useAuth();\n const editor = canEdit(role);\n const [articles, setArticles] = useState<NewsArticle[]>([]);\n const [loading, setLoading] = useState(true);\n const onlyPublished = opts.onlyPublished ?? !editor;\n const live = opts.live ?? editor; // editors zien live; publiek één keer\n // Begrens de query: zonder cap zou een nieuws-rijke site ALLE artikelen downloaden bij elk bezoek.\n const cap = Math.min(opts.max ?? 50, 50);\n\n useEffect(() => {\n // Demo-modus: nieuws komt uit de lokale seed (geen Firebase), zodat nieuws-blokken/widgets\n // in de demo ook gevuld zijn.\n if (isDemoActive()) {\n const all = getDemoNews(siteId);\n setArticles((onlyPublished ? all.filter((a) => a.published) : all).slice(0, cap));\n setLoading(false);\n return;\n }\n const db = getDb();\n if (!db) {\n setLoading(false);\n return;\n }\n const base = collection(db, \"sites\", siteId, \"news\");\n const q = onlyPublished\n ? query(base, where(\"published\", \"==\", true), orderBy(\"date\", \"desc\"), fbLimit(cap))\n : query(base, orderBy(\"date\", \"desc\"), fbLimit(cap));\n const map = (snap: { docs: { id: string; data: () => unknown }[] }) =>\n setArticles(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Omit<NewsArticle, \"id\">) })));\n\n if (!live) {\n let active = true;\n getDocs(q)\n .then((snap) => {\n if (active) {\n map(snap);\n setLoading(false);\n }\n })\n .catch(() => active && setLoading(false));\n return () => {\n active = false;\n };\n }\n\n const unsub = onSnapshot(\n q,\n (snap) => {\n map(snap);\n setLoading(false);\n },\n () => setLoading(false),\n );\n return () => unsub();\n }, [siteId, onlyPublished, live, cap]);\n\n return { articles, loading };\n}\n\ninterface NewsListProps {\n /** Basispad voor links naar een artikel, bv. \"/nieuws\". */\n basePath?: string;\n className?: string;\n emptyLabel?: string;\n /** Maximaal aantal artikelen om te tonen. */\n limit?: number;\n /** Toon eerst dit aantal en laad de rest met een \"Meer laden\"-knop (0/leeg = alles ineens). */\n pageSize?: number;\n}\n\n/** Standaardweergave van nieuwsartikelen als kaartenlijst, optioneel gepagineerd. */\nexport function NewsList({ basePath = \"/nieuws\", className, emptyLabel = \"Nog geen nieuws.\", limit, pageSize }: NewsListProps) {\n const { articles, loading } = useNews({ max: limit });\n const paged = typeof pageSize === \"number\" && pageSize > 0;\n const [visible, setVisible] = useState(paged ? pageSize : Infinity);\n\n if (loading) return <p>Nieuws laden…</p>;\n if (articles.length === 0) return <p>{emptyLabel}</p>;\n\n const all = typeof limit === \"number\" ? articles.slice(0, limit) : articles;\n const shown = paged ? all.slice(0, visible) : all;\n\n return (\n <div className={className} style={{ display: \"grid\", gap: \"1rem\" }}>\n {shown.map((a, i) => (\n <article key={a.id} className=\"xicms-card\" style={{ padding: \"1.25rem\" }}>\n {a.coverUrl && (\n // Eerste cover boven de vouw = vaak de LCP → priority (geen lazy-load-vertraging).\n <Img src={a.coverUrl} alt=\"\" width={1200} height={700} sizes=\"(max-width: 720px) 100vw, 33vw\" priority={i === 0} style={{ width: \"100%\", height: \"auto\", borderRadius: 8, marginBottom: \"0.75rem\" }} />\n )}\n <p style={{ fontSize: 12, opacity: 0.7, margin: 0 }}>{a.date}</p>\n {/* h2: de lijst staat direct onder de pagina-h1 — h3 zou een kopniveau overslaan. */}\n <h2 style={{ margin: \"0.25rem 0\", fontSize: \"1.35rem\" }}>\n <a href={`${basePath}/${a.slug ?? a.id}`}>{a.title}</a>\n {!a.published && <span style={{ marginLeft: 8, fontSize: 12, color: \"#b45309\" }}>(concept)</span>}\n </h2>\n {a.excerpt && <p style={{ margin: 0, opacity: 0.85 }}>{a.excerpt}</p>}\n </article>\n ))}\n {paged && shown.length < all.length && (\n <div style={{ textAlign: \"center\" }}>\n <button type=\"button\" className=\"xicms-btn\" onClick={() => setVisible((v) => v + (pageSize as number))}>\n Meer laden ({all.length - shown.length})\n </button>\n </div>\n )}\n </div>\n );\n}\n\n/** BlogPosting-JSON-LD voor een artikel — puur, zodat het testbaar is. */\nexport function articleBlogPostingJsonLd(article: NewsArticle, opts: { siteName?: string; url?: string } = {}): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"BlogPosting\",\n headline: article.title,\n datePublished: article.date,\n ...(article.excerpt ? { description: article.excerpt } : {}),\n ...(article.coverUrl ? { image: article.coverUrl } : {}),\n ...(opts.url ? { url: opts.url, mainEntityOfPage: opts.url } : {}),\n ...(opts.siteName ? { publisher: { \"@type\": \"Organization\", name: opts.siteName } } : {}),\n };\n}\n\n/** Detailweergave van één artikel, met BlogPosting-JSON-LD voor zoekmachines. */\nexport function NewsArticleView({ article, jsonLd = true }: { article: NewsArticle; jsonLd?: boolean }) {\n const { name } = useSiteConfig();\n // Rich-text wordt bij het RENDEREN gesanitized (DOMPurify) — de render is de veiligheidsgrens,\n // ook als er ooit ongesaneerde HTML in Firestore zou belanden. DOMPurify heeft echter een echte\n // DOM nodig: op de server (SSR/prerender) is sanitize geen functie. Daarom rendert de eerste\n // render (server én client-hydration, identiek) de platte-tekst-fallback en wisselt het effect\n // — client-only — naar de gesanitizede rich-text.\n const [html, setHtml] = useState<string | null>(null);\n useEffect(() => {\n setHtml(article.bodyHtml ? DOMPurify.sanitize(article.bodyHtml) : null);\n }, [article.bodyHtml]);\n // Fallback tot de sanitizer klaar is: kale tekst (tags gestript, gerenderd als tekstnodes — veilig).\n const fallbackText = article.body ?? (article.bodyHtml ? article.bodyHtml.replace(/<[^>]*>/g, \" \") : \"\");\n return (\n <article>\n <p style={{ fontSize: 12, opacity: 0.6 }}>{article.date}</p>\n <h1>{article.title}</h1>\n {article.coverUrl && (\n <Img src={article.coverUrl} alt=\"\" width={1200} height={700} sizes=\"(max-width: 900px) 100vw, 800px\" priority style={{ width: \"100%\", height: \"auto\", borderRadius: 12, margin: \"1rem 0\" }} />\n )}\n {html ? (\n // eslint-disable-next-line react/no-danger\n <div className=\"xicms-richtext\" dangerouslySetInnerHTML={{ __html: html }} />\n ) : (\n fallbackText.split(/\\n{2,}/).map((p, i) => (\n <p key={i} style={{ whiteSpace: \"pre-wrap\" }}>\n {p}\n </p>\n ))\n )}\n {jsonLd && (\n <script\n type=\"application/ld+json\"\n // eslint-disable-next-line react/no-danger\n dangerouslySetInnerHTML={{ __html: jsonLdHtml(articleBlogPostingJsonLd(article, { siteName: name })) }}\n />\n )}\n </article>\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/i18n/overlay.d.ts
CHANGED
|
@@ -33,9 +33,10 @@ export declare function parseLocaleSlugs(slugs: string[] | undefined, cfg: Local
|
|
|
33
33
|
/** Pad mét taalprefix voor niet-default talen: ("/over-ons", "de") → "/de/over-ons". */
|
|
34
34
|
export declare function localizedPath(path: string, locale: string, defaultLocale: string): string;
|
|
35
35
|
/**
|
|
36
|
-
* Past één taal-overlay toe op een lijst blokken
|
|
37
|
-
*
|
|
38
|
-
* zijn
|
|
36
|
+
* Past één taal-overlay toe op een lijst blokken — inclusief blokken genest in indelingsblokken
|
|
37
|
+
* (columns/withSidebar/tabs). Alleen bestaande, tekstuele velden worden overschreven: het doelpad
|
|
38
|
+
* moet oplosbaar zijn en de canonieke waarde moet een string (of leeg) zijn — zo kan een verouderde
|
|
39
|
+
* vertaalsleutel nooit de structuur breken (overlay-drift).
|
|
39
40
|
*/
|
|
40
41
|
export declare function localizeBlocks<T extends {
|
|
41
42
|
_key?: string;
|
|
@@ -51,10 +52,18 @@ export declare function localizePage<T extends {
|
|
|
51
52
|
blocks?: unknown[];
|
|
52
53
|
translations?: Translations;
|
|
53
54
|
}>(page: T, locale: string | undefined, defaultLocale: string): T;
|
|
55
|
+
/**
|
|
56
|
+
* Verzamelt ALLE blok-keys, inclusief blokken genest in indelingsblokken (columns/withSidebar/tabs).
|
|
57
|
+
* Gedeeld door pruneTranslations hier en sanitizeTranslations in de Cloud Functions — beide moeten
|
|
58
|
+
* geneste vertalingen als geldig zien, anders worden ze bij publiceren als "wees" verwijderd.
|
|
59
|
+
*/
|
|
60
|
+
export declare function collectBlockKeys(blocks: Array<{
|
|
61
|
+
_key?: string;
|
|
62
|
+
}> | undefined, into?: Set<string>): Set<string>;
|
|
54
63
|
/**
|
|
55
64
|
* Ruimt wees-vertalingen op: sleutels die naar een niet (meer) bestaand blok wijzen vervallen
|
|
56
65
|
* (`title` en `seo.*` blijven altijd). Draait bij publiceren zodat de overlay niet oneindig
|
|
57
|
-
* aangroeit wanneer blokken verwijderd worden.
|
|
66
|
+
* aangroeit wanneer blokken verwijderd worden. Geneste blokken tellen als bestaand.
|
|
58
67
|
*/
|
|
59
68
|
export declare function pruneTranslations(translations: Translations | undefined, blocks: Array<{
|
|
60
69
|
_key?: string;
|
package/dist/i18n/overlay.js
CHANGED
|
@@ -36,21 +36,54 @@ function setPath(obj, keys, value) {
|
|
|
36
36
|
cur[keys[keys.length - 1]] = value;
|
|
37
37
|
return root;
|
|
38
38
|
}
|
|
39
|
+
function localizeNestedBlock(b, overlay) {
|
|
40
|
+
let cur = b;
|
|
41
|
+
const localizeSlot = (slot) => {
|
|
42
|
+
if (!Array.isArray(slot?.blocks)) return slot;
|
|
43
|
+
const nb = localizeBlocks(slot.blocks, overlay);
|
|
44
|
+
return nb === slot.blocks ? slot : { ...slot, blocks: nb };
|
|
45
|
+
};
|
|
46
|
+
if (Array.isArray(cur.columns)) {
|
|
47
|
+
const cols = cur.columns;
|
|
48
|
+
const next = cols.map(localizeSlot);
|
|
49
|
+
if (next.some((c, i) => c !== cols[i])) cur = { ...cur, columns: next };
|
|
50
|
+
}
|
|
51
|
+
if (Array.isArray(cur.tabs)) {
|
|
52
|
+
const tabs = cur.tabs;
|
|
53
|
+
const next = tabs.map(localizeSlot);
|
|
54
|
+
if (next.some((t, i) => t !== tabs[i])) cur = { ...cur, tabs: next };
|
|
55
|
+
}
|
|
56
|
+
for (const field of ["main", "aside"]) {
|
|
57
|
+
if (Array.isArray(cur[field])) {
|
|
58
|
+
const nb = localizeBlocks(cur[field], overlay);
|
|
59
|
+
if (nb !== cur[field]) cur = { ...cur, [field]: nb };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return cur;
|
|
63
|
+
}
|
|
39
64
|
function localizeBlocks(blocks, overlay) {
|
|
40
65
|
if (!overlay || blocks.length === 0) return blocks;
|
|
41
66
|
let out = blocks;
|
|
67
|
+
const replace = (idx, value) => {
|
|
68
|
+
if (out === blocks) out = [...blocks];
|
|
69
|
+
out[idx] = value;
|
|
70
|
+
};
|
|
42
71
|
for (const [key, value] of Object.entries(overlay)) {
|
|
43
72
|
if (typeof value !== "string") continue;
|
|
44
73
|
const [blockKey, ...rest] = key.split(".");
|
|
45
74
|
if (rest.length === 0) continue;
|
|
46
75
|
const idx = out.findIndex((b) => b?._key === blockKey);
|
|
47
|
-
if (idx
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
76
|
+
if (idx >= 0) {
|
|
77
|
+
const parent = rest.length > 1 ? getPath(out[idx], rest.slice(0, -1)) : out[idx];
|
|
78
|
+
if (parent == null || typeof parent !== "object") continue;
|
|
79
|
+
const current = parent[rest[rest.length - 1]];
|
|
80
|
+
if (current !== void 0 && typeof current !== "string") continue;
|
|
81
|
+
replace(idx, setPath(out[idx], rest, value));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (let i = 0; i < out.length; i++) {
|
|
85
|
+
const updated = localizeNestedBlock(out[i], overlay);
|
|
86
|
+
if (updated !== out[i]) replace(i, updated);
|
|
54
87
|
}
|
|
55
88
|
return out;
|
|
56
89
|
}
|
|
@@ -66,9 +99,21 @@ function localizePage(page, locale, defaultLocale) {
|
|
|
66
99
|
}
|
|
67
100
|
return out;
|
|
68
101
|
}
|
|
102
|
+
function collectBlockKeys(blocks, into = /* @__PURE__ */ new Set()) {
|
|
103
|
+
for (const b of blocks ?? []) {
|
|
104
|
+
if (!b || typeof b !== "object") continue;
|
|
105
|
+
if (typeof b._key === "string") into.add(b._key);
|
|
106
|
+
const a = b;
|
|
107
|
+
if (Array.isArray(a.columns)) for (const c of a.columns) collectBlockKeys(c?.blocks, into);
|
|
108
|
+
if (Array.isArray(a.tabs)) for (const t of a.tabs) collectBlockKeys(t?.blocks, into);
|
|
109
|
+
if (Array.isArray(a.main)) collectBlockKeys(a.main, into);
|
|
110
|
+
if (Array.isArray(a.aside)) collectBlockKeys(a.aside, into);
|
|
111
|
+
}
|
|
112
|
+
return into;
|
|
113
|
+
}
|
|
69
114
|
function pruneTranslations(translations, blocks) {
|
|
70
115
|
if (!translations) return void 0;
|
|
71
|
-
const keys =
|
|
116
|
+
const keys = collectBlockKeys(blocks);
|
|
72
117
|
const out = {};
|
|
73
118
|
for (const [locale, overlay] of Object.entries(translations)) {
|
|
74
119
|
const kept = Object.fromEntries(
|
|
@@ -80,6 +125,7 @@ function pruneTranslations(translations, blocks) {
|
|
|
80
125
|
}
|
|
81
126
|
export {
|
|
82
127
|
TranslationsSchema,
|
|
128
|
+
collectBlockKeys,
|
|
83
129
|
localizeBlocks,
|
|
84
130
|
localizePage,
|
|
85
131
|
localizedPath,
|
package/dist/i18n/overlay.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/i18n/overlay.ts"],"sourcesContent":["/**\n * Localisatie (5.0) — primo's locale-per-waarde vertaald naar een ADDITIEVE overlay:\n * de blokkenstructuur blijft canoniek (de default-taal), en per pagina/symbool ligt er\n * een `translations`-map overheen: `{ [locale]: { \"<blokKey>.<veldpad>\": \"vertaalde tekst\" } }`.\n * Speciale sleutels: `title` (paginatitel) en `seo.<veld>` (SEO-overlay). Waarden zijn altijd\n * strings — structuurwijzigingen (blokken toevoegen/verplaatsen) gebeuren alléén canoniek.\n *\n * Server-safe: geen React, geen Firebase — bruikbaar in server/pages.ts én in de editor.\n */\nimport { z } from \"zod\";\n\n/** `{ [locale]: { \"hero1.title\": \"Willkommen\", \"seo.description\": \"…\" } }` */\nexport const TranslationsSchema = z.record(\n z.string().min(2).max(10),\n z.record(z.string().min(1).max(300), z.string().max(20_000)).refine((m) => Object.keys(m).length <= 2000, {\n message: \"te veel vertaalsleutels (max 2000 per taal)\",\n }),\n);\nexport type Translations = z.infer<typeof TranslationsSchema>;\n\nexport interface LocaleConfig {\n /** Alle talen van de site, default-taal eerst (bv. [\"nl\", \"de\", \"en\"]). */\n locales: string[];\n defaultLocale: string;\n}\n\n/** Normaliseert de site-config naar een LocaleConfig (default: alleen `lang` ?? \"nl\"). */\nexport function siteLocales(site: { locales?: string[]; defaultLocale?: string; lang?: string }): LocaleConfig {\n const defaultLocale = site.defaultLocale ?? site.lang ?? \"nl\";\n const locales = [defaultLocale, ...(site.locales ?? []).filter((l) => l !== defaultLocale)];\n return { locales: [...new Set(locales)], defaultLocale };\n}\n\n/**\n * Herkent een taalprefix in Next-catch-all-segmenten: `/de/over-ons` → locale \"de\", pad \"/over-ons\".\n * De default-taal heeft géén prefix (canonieke URL's blijven ongewijzigd).\n */\nexport function parseLocaleSlugs(\n slugs: string[] | undefined,\n cfg: LocaleConfig,\n): { locale: string; slugs: string[] } {\n const list = slugs ?? [];\n const first = list[0];\n if (first && first !== cfg.defaultLocale && cfg.locales.includes(first)) {\n return { locale: first, slugs: list.slice(1) };\n }\n return { locale: cfg.defaultLocale, slugs: list };\n}\n\n/** Pad mét taalprefix voor niet-default talen: (\"/over-ons\", \"de\") → \"/de/over-ons\". */\nexport function localizedPath(path: string, locale: string, defaultLocale: string): string {\n if (locale === defaultLocale) return path;\n return `/${locale}${path === \"/\" ? \"\" : path}`;\n}\n\n/* Zelfde gepunte-pad-helpers als de visuele editor: immutable schrijven op \"items.0.title\". */\nfunction getPath(obj: unknown, keys: string[]): unknown {\n return keys.reduce<unknown>((o, k) => (o == null ? undefined : (o as Record<string, unknown>)[k]), obj);\n}\nfunction setPath<T>(obj: T, keys: string[], value: unknown): T {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const clone = (v: unknown): any => (Array.isArray(v) ? [...v] : { ...(v as object) });\n const root = clone(obj);\n let cur = root;\n for (let i = 0; i < keys.length - 1; i++) {\n cur[keys[i]] = clone(cur[keys[i]]);\n cur = cur[keys[i]];\n }\n cur[keys[keys.length - 1]] = value;\n return root as T;\n}\n\n/**\n * Past één taal-overlay toe op een lijst blokken. Alleen bestaande, tekstuele velden worden\n * overschreven: het doelpad moet oplosbaar zijn en de canonieke waarde moet een string (of leeg)\n * zijn — zo kan een verouderde vertaalsleutel nooit de structuur breken (overlay-drift).\n */\nexport function localizeBlocks<T extends { _key?: string }>(blocks: T[], overlay: Record<string, string> | undefined): T[] {\n if (!overlay || blocks.length === 0) return blocks;\n let out = blocks;\n for (const [key, value] of Object.entries(overlay)) {\n if (typeof value !== \"string\") continue;\n const [blockKey, ...rest] = key.split(\".\");\n if (rest.length === 0) continue;\n const idx = out.findIndex((b) => b?._key === blockKey);\n if (idx < 0) continue;\n const parent = rest.length > 1 ? getPath(out[idx], rest.slice(0, -1)) : out[idx];\n if (parent == null || typeof parent !== \"object\") continue;\n const current = (parent as Record<string, unknown>)[rest[rest.length - 1]];\n if (current !== undefined && typeof current !== \"string\") continue;\n if (out === blocks) out = [...blocks];\n out[idx] = setPath(out[idx], rest, value);\n }\n return out;\n}\n\n/**\n * Past een taal-overlay toe op een hele pagina (titel + seo + blokken). Geeft de pagina\n * ongewijzigd terug voor de default-taal of zonder vertalingen — nul overhead voor\n * ééntalige sites.\n */\nexport function localizePage<\n T extends { title?: string; seo?: Record<string, unknown>; blocks?: unknown[]; translations?: Translations },\n>(page: T, locale: string | undefined, defaultLocale: string): T {\n if (!locale || locale === defaultLocale) return page;\n const overlay = page.translations?.[locale];\n if (!overlay) return page;\n let out: T = { ...page, blocks: localizeBlocks((page.blocks ?? []) as { _key?: string }[], overlay) };\n for (const [key, value] of Object.entries(overlay)) {\n if (typeof value !== \"string\") continue;\n if (key === \"title\") out = { ...out, title: value };\n else if (key.startsWith(\"seo.\")) out = { ...out, seo: { ...(out.seo ?? {}), [key.slice(4)]: value } };\n }\n return out;\n}\n\n/**\n * Ruimt wees-vertalingen op: sleutels die naar een niet (meer) bestaand blok wijzen vervallen\n * (`title` en `seo.*` blijven altijd). Draait bij publiceren zodat de overlay niet oneindig\n * aangroeit wanneer blokken verwijderd worden.\n */\nexport function pruneTranslations(\n translations: Translations | undefined,\n blocks: Array<{ _key?: string }> | undefined,\n): Translations | undefined {\n if (!translations) return undefined;\n const keys = new Set((blocks ?? []).map((b) => b?._key).filter(Boolean));\n const out: Translations = {};\n for (const [locale, overlay] of Object.entries(translations)) {\n const kept = Object.fromEntries(\n Object.entries(overlay).filter(([k]) => k === \"title\" || k.startsWith(\"seo.\") || keys.has(k.split(\".\")[0])),\n );\n if (Object.keys(kept).length > 0) out[locale] = kept;\n }\n return Object.keys(out).length > 0 ? out : undefined;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/i18n/overlay.ts"],"sourcesContent":["/**\n * Localisatie (5.0) — primo's locale-per-waarde vertaald naar een ADDITIEVE overlay:\n * de blokkenstructuur blijft canoniek (de default-taal), en per pagina/symbool ligt er\n * een `translations`-map overheen: `{ [locale]: { \"<blokKey>.<veldpad>\": \"vertaalde tekst\" } }`.\n * Speciale sleutels: `title` (paginatitel) en `seo.<veld>` (SEO-overlay). Waarden zijn altijd\n * strings — structuurwijzigingen (blokken toevoegen/verplaatsen) gebeuren alléén canoniek.\n *\n * Server-safe: geen React, geen Firebase — bruikbaar in server/pages.ts én in de editor.\n */\nimport { z } from \"zod\";\n\n/** `{ [locale]: { \"hero1.title\": \"Willkommen\", \"seo.description\": \"…\" } }` */\nexport const TranslationsSchema = z.record(\n z.string().min(2).max(10),\n z.record(z.string().min(1).max(300), z.string().max(20_000)).refine((m) => Object.keys(m).length <= 2000, {\n message: \"te veel vertaalsleutels (max 2000 per taal)\",\n }),\n);\nexport type Translations = z.infer<typeof TranslationsSchema>;\n\nexport interface LocaleConfig {\n /** Alle talen van de site, default-taal eerst (bv. [\"nl\", \"de\", \"en\"]). */\n locales: string[];\n defaultLocale: string;\n}\n\n/** Normaliseert de site-config naar een LocaleConfig (default: alleen `lang` ?? \"nl\"). */\nexport function siteLocales(site: { locales?: string[]; defaultLocale?: string; lang?: string }): LocaleConfig {\n const defaultLocale = site.defaultLocale ?? site.lang ?? \"nl\";\n const locales = [defaultLocale, ...(site.locales ?? []).filter((l) => l !== defaultLocale)];\n return { locales: [...new Set(locales)], defaultLocale };\n}\n\n/**\n * Herkent een taalprefix in Next-catch-all-segmenten: `/de/over-ons` → locale \"de\", pad \"/over-ons\".\n * De default-taal heeft géén prefix (canonieke URL's blijven ongewijzigd).\n */\nexport function parseLocaleSlugs(\n slugs: string[] | undefined,\n cfg: LocaleConfig,\n): { locale: string; slugs: string[] } {\n const list = slugs ?? [];\n const first = list[0];\n if (first && first !== cfg.defaultLocale && cfg.locales.includes(first)) {\n return { locale: first, slugs: list.slice(1) };\n }\n return { locale: cfg.defaultLocale, slugs: list };\n}\n\n/** Pad mét taalprefix voor niet-default talen: (\"/over-ons\", \"de\") → \"/de/over-ons\". */\nexport function localizedPath(path: string, locale: string, defaultLocale: string): string {\n if (locale === defaultLocale) return path;\n return `/${locale}${path === \"/\" ? \"\" : path}`;\n}\n\n/* Zelfde gepunte-pad-helpers als de visuele editor: immutable schrijven op \"items.0.title\". */\nfunction getPath(obj: unknown, keys: string[]): unknown {\n return keys.reduce<unknown>((o, k) => (o == null ? undefined : (o as Record<string, unknown>)[k]), obj);\n}\nfunction setPath<T>(obj: T, keys: string[], value: unknown): T {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const clone = (v: unknown): any => (Array.isArray(v) ? [...v] : { ...(v as object) });\n const root = clone(obj);\n let cur = root;\n for (let i = 0; i < keys.length - 1; i++) {\n cur[keys[i]] = clone(cur[keys[i]]);\n cur = cur[keys[i]];\n }\n cur[keys[keys.length - 1]] = value;\n return root as T;\n}\n\n/* Geneste blok-containers (columns/withSidebar/tabs) — structureel herkend zodat de overlay ook\n * blokken BINNEN een indelingsblok bereikt. Elke geneste blok heeft z'n eigen unieke `_key`, dus de\n * vertaalsleutel blijft gewoon \"<blokKey>.<veldpad>\"; we hoeven alleen dieper te zoeken. */\ntype AnyBlock = { _key?: string } & Record<string, unknown>;\n\n/** Past de overlay toe op de geneste blok-lijsten van één (indelings)blok. Referentie-behoudend:\n * ongewijzigd → zelfde object terug (nul overhead voor pagina's zonder vertaalde geneste blokken). */\nfunction localizeNestedBlock(b: AnyBlock, overlay: Record<string, string>): AnyBlock {\n let cur = b;\n const localizeSlot = <S extends { blocks?: AnyBlock[] }>(slot: S): S => {\n if (!Array.isArray(slot?.blocks)) return slot;\n const nb = localizeBlocks(slot.blocks, overlay);\n return nb === slot.blocks ? slot : { ...slot, blocks: nb };\n };\n if (Array.isArray(cur.columns)) {\n const cols = cur.columns as Array<{ blocks?: AnyBlock[] }>;\n const next = cols.map(localizeSlot);\n if (next.some((c, i) => c !== cols[i])) cur = { ...cur, columns: next };\n }\n if (Array.isArray(cur.tabs)) {\n const tabs = cur.tabs as Array<{ blocks?: AnyBlock[] }>;\n const next = tabs.map(localizeSlot);\n if (next.some((t, i) => t !== tabs[i])) cur = { ...cur, tabs: next };\n }\n for (const field of [\"main\", \"aside\"] as const) {\n if (Array.isArray(cur[field])) {\n const nb = localizeBlocks(cur[field] as AnyBlock[], overlay);\n if (nb !== cur[field]) cur = { ...cur, [field]: nb };\n }\n }\n return cur;\n}\n\n/**\n * Past één taal-overlay toe op een lijst blokken — inclusief blokken genest in indelingsblokken\n * (columns/withSidebar/tabs). Alleen bestaande, tekstuele velden worden overschreven: het doelpad\n * moet oplosbaar zijn en de canonieke waarde moet een string (of leeg) zijn — zo kan een verouderde\n * vertaalsleutel nooit de structuur breken (overlay-drift).\n */\nexport function localizeBlocks<T extends { _key?: string }>(blocks: T[], overlay: Record<string, string> | undefined): T[] {\n if (!overlay || blocks.length === 0) return blocks;\n let out = blocks;\n const replace = (idx: number, value: T) => {\n if (out === blocks) out = [...blocks];\n out[idx] = value;\n };\n for (const [key, value] of Object.entries(overlay)) {\n if (typeof value !== \"string\") continue;\n const [blockKey, ...rest] = key.split(\".\");\n if (rest.length === 0) continue;\n const idx = out.findIndex((b) => b?._key === blockKey);\n if (idx >= 0) {\n const parent = rest.length > 1 ? getPath(out[idx], rest.slice(0, -1)) : out[idx];\n if (parent == null || typeof parent !== \"object\") continue;\n const current = (parent as Record<string, unknown>)[rest[rest.length - 1]];\n if (current !== undefined && typeof current !== \"string\") continue;\n replace(idx, setPath(out[idx], rest, value));\n }\n }\n // Recursie in indelingsblokken: pas dezelfde overlay toe op geneste blok-lijsten. Alleen bij een\n // daadwerkelijke wijziging (referentie-check) wordt het container-blok gekloond — ongewijzigde\n // pagina's houden nul overhead.\n for (let i = 0; i < out.length; i++) {\n const updated = localizeNestedBlock(out[i] as AnyBlock, overlay);\n if (updated !== (out[i] as AnyBlock)) replace(i, updated as T);\n }\n return out;\n}\n\n/**\n * Past een taal-overlay toe op een hele pagina (titel + seo + blokken). Geeft de pagina\n * ongewijzigd terug voor de default-taal of zonder vertalingen — nul overhead voor\n * ééntalige sites.\n */\nexport function localizePage<\n T extends { title?: string; seo?: Record<string, unknown>; blocks?: unknown[]; translations?: Translations },\n>(page: T, locale: string | undefined, defaultLocale: string): T {\n if (!locale || locale === defaultLocale) return page;\n const overlay = page.translations?.[locale];\n if (!overlay) return page;\n let out: T = { ...page, blocks: localizeBlocks((page.blocks ?? []) as { _key?: string }[], overlay) };\n for (const [key, value] of Object.entries(overlay)) {\n if (typeof value !== \"string\") continue;\n if (key === \"title\") out = { ...out, title: value };\n else if (key.startsWith(\"seo.\")) out = { ...out, seo: { ...(out.seo ?? {}), [key.slice(4)]: value } };\n }\n return out;\n}\n\n/**\n * Verzamelt ALLE blok-keys, inclusief blokken genest in indelingsblokken (columns/withSidebar/tabs).\n * Gedeeld door pruneTranslations hier en sanitizeTranslations in de Cloud Functions — beide moeten\n * geneste vertalingen als geldig zien, anders worden ze bij publiceren als \"wees\" verwijderd.\n */\nexport function collectBlockKeys(blocks: Array<{ _key?: string }> | undefined, into: Set<string> = new Set()): Set<string> {\n for (const b of blocks ?? []) {\n if (!b || typeof b !== \"object\") continue;\n if (typeof b._key === \"string\") into.add(b._key);\n const a = b as AnyBlock;\n if (Array.isArray(a.columns)) for (const c of a.columns as Array<{ blocks?: AnyBlock[] }>) collectBlockKeys(c?.blocks, into);\n if (Array.isArray(a.tabs)) for (const t of a.tabs as Array<{ blocks?: AnyBlock[] }>) collectBlockKeys(t?.blocks, into);\n if (Array.isArray(a.main)) collectBlockKeys(a.main as AnyBlock[], into);\n if (Array.isArray(a.aside)) collectBlockKeys(a.aside as AnyBlock[], into);\n }\n return into;\n}\n\n/**\n * Ruimt wees-vertalingen op: sleutels die naar een niet (meer) bestaand blok wijzen vervallen\n * (`title` en `seo.*` blijven altijd). Draait bij publiceren zodat de overlay niet oneindig\n * aangroeit wanneer blokken verwijderd worden. Geneste blokken tellen als bestaand.\n */\nexport function pruneTranslations(\n translations: Translations | undefined,\n blocks: Array<{ _key?: string }> | undefined,\n): Translations | undefined {\n if (!translations) return undefined;\n const keys = collectBlockKeys(blocks);\n const out: Translations = {};\n for (const [locale, overlay] of Object.entries(translations)) {\n const kept = Object.fromEntries(\n Object.entries(overlay).filter(([k]) => k === \"title\" || k.startsWith(\"seo.\") || keys.has(k.split(\".\")[0])),\n );\n if (Object.keys(kept).length > 0) out[locale] = kept;\n }\n return Object.keys(out).length > 0 ? out : undefined;\n}\n"],"mappings":"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","names":[]}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import "server-only";
|
|
6
6
|
export { getAdminApp, getAdminAuth, getAdminDb } from "../firebase/admin";
|
|
7
7
|
export { sanitizeRichText } from "./sanitize";
|
|
8
|
+
export { jsonLdHtml } from "../blocks/builtins/jsonld";
|
|
8
9
|
export { buildMetadata, buildJsonLd, buildSitemap, buildRobots, siteOrigin, businessJsonLd, breadcrumbListJsonLd, articleJsonLd, productJsonLd, openingHoursJsonLd, galleryJsonLd, videoObjectJsonLd, type BusinessInfo, } from "./seo";
|
|
9
10
|
export { validateEnv, assertEnv, type EnvReport } from "./env";
|
|
10
11
|
export { pathFromSlugs, listPublishedPages, getPublishedPageByPath, getPublishedPageByPathExpanded, getSiteSymbols, expandSymbols, getSiteCustomBlocks, getDraftPageById, getDraftPageByPath, type StoredPage, } from "./pages";
|
package/dist/server/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import "server-only";
|
|
2
2
|
import { getAdminApp, getAdminAuth, getAdminDb } from "../firebase/admin";
|
|
3
3
|
import { sanitizeRichText } from "./sanitize";
|
|
4
|
+
import { jsonLdHtml } from "../blocks/builtins/jsonld";
|
|
4
5
|
import {
|
|
5
6
|
buildMetadata,
|
|
6
7
|
buildJsonLd,
|
|
@@ -57,6 +58,7 @@ export {
|
|
|
57
58
|
getPublishedPageByPathExpanded,
|
|
58
59
|
getSiteCustomBlocks,
|
|
59
60
|
getSiteSymbols,
|
|
61
|
+
jsonLdHtml,
|
|
60
62
|
listPublishedPages,
|
|
61
63
|
localizeBlocks,
|
|
62
64
|
localizePage,
|
package/dist/server/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/server/index.ts"],"sourcesContent":["/**\n * @xiwebdesign/xicms-core/server — server-only helpers (firebase-admin, sanitization, SEO,\n * pagina-reads). NOOIT importeren in client-componenten.\n */\nimport \"server-only\";\n\nexport { getAdminApp, getAdminAuth, getAdminDb } from \"../firebase/admin\";\nexport { sanitizeRichText } from \"./sanitize\";\nexport {\n buildMetadata,\n buildJsonLd,\n buildSitemap,\n buildRobots,\n siteOrigin,\n businessJsonLd,\n breadcrumbListJsonLd,\n articleJsonLd,\n productJsonLd,\n openingHoursJsonLd,\n galleryJsonLd,\n videoObjectJsonLd,\n type BusinessInfo,\n} from \"./seo\";\nexport { validateEnv, assertEnv, type EnvReport } from \"./env\";\nexport {\n pathFromSlugs,\n listPublishedPages,\n getPublishedPageByPath,\n getPublishedPageByPathExpanded,\n getSiteSymbols,\n expandSymbols,\n getSiteCustomBlocks,\n getDraftPageById,\n getDraftPageByPath,\n type StoredPage,\n} from \"./pages\";\n// Localisatie (5.0) — server-safe overlay-helpers, ook bruikbaar in route-handlers/layouts.\nexport {\n siteLocales,\n parseLocaleSlugs,\n localizedPath,\n localizePage,\n localizeBlocks,\n pruneTranslations,\n TranslationsSchema,\n type Translations,\n type LocaleConfig,\n} from \"../i18n/overlay\";\n"],"mappings":"AAIA,OAAO;AAEP,SAAS,aAAa,cAAc,kBAAkB;AACtD,SAAS,wBAAwB;
|
|
1
|
+
{"version":3,"sources":["../../src/server/index.ts"],"sourcesContent":["/**\n * @xiwebdesign/xicms-core/server — server-only helpers (firebase-admin, sanitization, SEO,\n * pagina-reads). NOOIT importeren in client-componenten.\n */\nimport \"server-only\";\n\nexport { getAdminApp, getAdminAuth, getAdminDb } from \"../firebase/admin\";\nexport { sanitizeRichText } from \"./sanitize\";\n// Veilige JSON-LD-serialisatie voor <script type=\"application/ld+json\"> in consumer-routes\n// (escapet `<` zodat klant-tekst met </script> de pagina niet kan breken).\nexport { jsonLdHtml } from \"../blocks/builtins/jsonld\";\nexport {\n buildMetadata,\n buildJsonLd,\n buildSitemap,\n buildRobots,\n siteOrigin,\n businessJsonLd,\n breadcrumbListJsonLd,\n articleJsonLd,\n productJsonLd,\n openingHoursJsonLd,\n galleryJsonLd,\n videoObjectJsonLd,\n type BusinessInfo,\n} from \"./seo\";\nexport { validateEnv, assertEnv, type EnvReport } from \"./env\";\nexport {\n pathFromSlugs,\n listPublishedPages,\n getPublishedPageByPath,\n getPublishedPageByPathExpanded,\n getSiteSymbols,\n expandSymbols,\n getSiteCustomBlocks,\n getDraftPageById,\n getDraftPageByPath,\n type StoredPage,\n} from \"./pages\";\n// Localisatie (5.0) — server-safe overlay-helpers, ook bruikbaar in route-handlers/layouts.\nexport {\n siteLocales,\n parseLocaleSlugs,\n localizedPath,\n localizePage,\n localizeBlocks,\n pruneTranslations,\n TranslationsSchema,\n type Translations,\n type LocaleConfig,\n} from \"../i18n/overlay\";\n"],"mappings":"AAIA,OAAO;AAEP,SAAS,aAAa,cAAc,kBAAkB;AACtD,SAAS,wBAAwB;AAGjC,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,aAAa,iBAAiC;AACvD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;","names":[]}
|
package/dist/server/pages.js
CHANGED
|
@@ -14,9 +14,20 @@ function pathFromSlugs(slugs) {
|
|
|
14
14
|
}
|
|
15
15
|
function parse(id, data) {
|
|
16
16
|
const res = PageSchema.safeParse(data);
|
|
17
|
-
if (!res.success)
|
|
17
|
+
if (!res.success) {
|
|
18
|
+
console.warn(`[xicms] pagina "${id}" faalt schema-validatie en wordt overgeslagen:`, res.error.issues[0]);
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
18
21
|
return { id, ...res.data };
|
|
19
22
|
}
|
|
23
|
+
async function safeAdminRead(fallback, what, fn) {
|
|
24
|
+
try {
|
|
25
|
+
return await fn();
|
|
26
|
+
} catch (e) {
|
|
27
|
+
console.warn(`[xicms] Admin-read mislukt (${what}) \u2014 fallback gebruikt:`, e?.message ?? e);
|
|
28
|
+
return fallback;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
20
31
|
function decode(v) {
|
|
21
32
|
if (v == null) return null;
|
|
22
33
|
if ("stringValue" in v) return v.stringValue;
|
|
@@ -64,17 +75,21 @@ async function restQuery(siteId, filters, limit) {
|
|
|
64
75
|
async function listPublishedPages(siteId) {
|
|
65
76
|
const adb = getAdminDb();
|
|
66
77
|
if (adb) {
|
|
67
|
-
|
|
68
|
-
|
|
78
|
+
return safeAdminRead([], `listPublishedPages ${siteId}`, async () => {
|
|
79
|
+
const snap = await adb.collection(`sites/${siteId}/pages`).where("status", "==", "published").get();
|
|
80
|
+
return snap.docs.map((d) => parse(d.id, d.data())).filter((p) => p !== null);
|
|
81
|
+
});
|
|
69
82
|
}
|
|
70
83
|
return restQuery(siteId, [eq("status", "published")]);
|
|
71
84
|
}
|
|
72
85
|
async function getPublishedPageByPath(siteId, path) {
|
|
73
86
|
const adb = getAdminDb();
|
|
74
87
|
if (adb) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
88
|
+
return safeAdminRead(null, `getPublishedPageByPath ${siteId}${path}`, async () => {
|
|
89
|
+
const snap = await adb.collection(`sites/${siteId}/pages`).where("status", "==", "published").where("path", "==", path).limit(1).get();
|
|
90
|
+
const d = snap.docs[0];
|
|
91
|
+
return d ? parse(d.id, d.data()) : null;
|
|
92
|
+
});
|
|
78
93
|
}
|
|
79
94
|
const rows = await restQuery(siteId, [eq("status", "published"), eq("path", path)], 1);
|
|
80
95
|
return rows[0] ?? null;
|
|
@@ -82,21 +97,27 @@ async function getPublishedPageByPath(siteId, path) {
|
|
|
82
97
|
async function getDraftPageById(siteId, pageId) {
|
|
83
98
|
const db = getAdminDb();
|
|
84
99
|
if (!db) return null;
|
|
85
|
-
|
|
86
|
-
|
|
100
|
+
return safeAdminRead(null, `getDraftPageById ${siteId}/${pageId}`, async () => {
|
|
101
|
+
const d = await db.doc(`sites/${siteId}/drafts/${pageId}`).get();
|
|
102
|
+
return d.exists ? parse(d.id, d.data()) : null;
|
|
103
|
+
});
|
|
87
104
|
}
|
|
88
105
|
async function getDraftPageByPath(siteId, path) {
|
|
89
106
|
const db = getAdminDb();
|
|
90
107
|
if (!db) return null;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
108
|
+
return safeAdminRead(null, `getDraftPageByPath ${siteId}${path}`, async () => {
|
|
109
|
+
const snap = await db.collection(`sites/${siteId}/drafts`).where("path", "==", path).limit(1).get();
|
|
110
|
+
const d = snap.docs[0];
|
|
111
|
+
return d ? parse(d.id, d.data()) : null;
|
|
112
|
+
});
|
|
94
113
|
}
|
|
95
114
|
async function readCollection(siteId, collectionId) {
|
|
96
115
|
const adb = getAdminDb();
|
|
97
116
|
if (adb) {
|
|
98
|
-
|
|
99
|
-
|
|
117
|
+
return safeAdminRead([], `readCollection ${siteId}/${collectionId}`, async () => {
|
|
118
|
+
const snap = await adb.collection(`sites/${siteId}/${collectionId}`).get();
|
|
119
|
+
return snap.docs.map((d) => ({ id: d.id, data: d.data() }));
|
|
120
|
+
});
|
|
100
121
|
}
|
|
101
122
|
const apiKey = process.env.NEXT_PUBLIC_FIREBASE_API_KEY;
|
|
102
123
|
const projectId = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID;
|
package/dist/server/pages.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/server/pages.ts"],"sourcesContent":["import \"server-only\";\n\nimport { getAdminDb } from \"../firebase/admin\";\nimport { PageSchema, type Page } from \"../schema/pages\";\nimport { SymbolSchema, type StoredSymbol } from \"../schema/symbols\";\nimport { CustomBlockSchema, type CustomBlock } from \"../schema/customBlocks\";\nimport { localizePage, localizeBlocks } from \"../i18n/overlay\";\n\nexport type StoredPage = Page & { id: string };\n\nfunction toPath(slugs: string[] | undefined): string {\n const clean = (slugs ?? []).filter(Boolean);\n return \"/\" + clean.join(\"/\");\n}\n\n/** Pad uit Next catch-all params (`[[...slug]]`). \"/\" voor de root. */\nexport function pathFromSlugs(slugs: string[] | undefined): string {\n const p = toPath(slugs);\n return p === \"/\" ? \"/\" : p.replace(/\\/$/, \"\");\n}\n\nfunction parse(id: string, data: unknown): StoredPage | null {\n const res = PageSchema.safeParse(data);\n if (!res.success) return null;\n return { id, ...res.data };\n}\n\n/* ---------------- keyless public reads via Firestore REST (alleen apiKey) ----------------\n * Gepubliceerde pagina's zijn publiek leesbaar (rules). De Firestore REST-API honoreert die\n * rules met enkel de web-apiKey (geen Admin-credentials, geen client-SDK), dus dit werkt\n * server-side op keyless deployments (org-policy zonder service-account-keys).\n */\ntype FsValue = Record<string, unknown>;\nfunction decode(v: FsValue): unknown {\n if (v == null) return null;\n if (\"stringValue\" in v) return v.stringValue;\n if (\"integerValue\" in v) return Number(v.integerValue);\n if (\"doubleValue\" in v) return v.doubleValue;\n if (\"booleanValue\" in v) return v.booleanValue;\n if (\"nullValue\" in v) return null;\n if (\"timestampValue\" in v) return v.timestampValue;\n if (\"referenceValue\" in v) return v.referenceValue;\n if (\"geoPointValue\" in v) return v.geoPointValue;\n if (\"arrayValue\" in v) return (((v.arrayValue as FsValue)?.values as FsValue[]) ?? []).map(decode);\n if (\"mapValue\" in v) return decodeFields(((v.mapValue as FsValue)?.fields as Record<string, FsValue>) ?? {});\n return null;\n}\nfunction decodeFields(fields: Record<string, FsValue>): Record<string, unknown> {\n return Object.fromEntries(Object.entries(fields).map(([k, x]) => [k, decode(x)]));\n}\n\ninterface Filter { fieldFilter: { field: { fieldPath: string }; op: \"EQUAL\"; value: { stringValue: string } } }\nfunction eq(fieldPath: string, value: string): Filter {\n return { fieldFilter: { field: { fieldPath }, op: \"EQUAL\", value: { stringValue: value } } };\n}\n\nasync function restQuery(siteId: string, filters: Filter[], limit?: number): Promise<StoredPage[]> {\n const apiKey = process.env.NEXT_PUBLIC_FIREBASE_API_KEY;\n const projectId = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID;\n if (!apiKey || !projectId) return [];\n const url = `https://firestore.googleapis.com/v1/projects/${projectId}/databases/(default)/documents/sites/${siteId}:runQuery?key=${apiKey}`;\n const where = filters.length === 1 ? filters[0] : { compositeFilter: { op: \"AND\", filters } };\n const structuredQuery: Record<string, unknown> = { from: [{ collectionId: \"pages\" }], where };\n if (limit) structuredQuery.limit = limit;\n // Netwerk-/parsefouten mogen een publieke pagina nooit laten crashen (500) — dan liever een\n // lege lijst (Next toont dan een 404 of de fallback, en de volgende revalidate probeert opnieuw).\n try {\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ structuredQuery }),\n // Pagina's zijn ISR-gecached; next-cache mag dit hergebruiken.\n next: { revalidate: 300 },\n });\n if (!res.ok) return [];\n const rows = (await res.json()) as { document?: { name: string; fields: Record<string, FsValue> } }[];\n return rows\n .filter((r) => r.document)\n .map((r) => parse(r.document!.name.split(\"/\").pop()!, decodeFields(r.document!.fields)))\n .filter((p): p is StoredPage => p !== null);\n } catch (err) {\n console.error(\"[xicms] Firestore REST-query mislukt:\", err);\n return [];\n }\n}\n\n/** Alle gepubliceerde pagina's (sitemap / generateStaticParams). Keyless via REST als geen Admin. */\nexport async function listPublishedPages(siteId: string): Promise<StoredPage[]> {\n const adb = getAdminDb();\n if (adb) {\n 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 restQuery(siteId, [eq(\"status\", \"published\")]);\n}\n\n/** Eén gepubliceerde pagina op pad. Keyless via REST als geen Admin. */\nexport async function getPublishedPageByPath(siteId: string, path: string): Promise<StoredPage | null> {\n const adb = getAdminDb();\n if (adb) {\n 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 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 const d = await db.doc(`sites/${siteId}/drafts/${pageId}`).get();\n return d.exists ? parse(d.id, d.data()) : null;\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 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/* ---------------- 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 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 = process.env.NEXT_PUBLIC_FIREBASE_API_KEY;\n const projectId = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID;\n if (!apiKey || !projectId) return [];\n const url = `https://firestore.googleapis.com/v1/projects/${projectId}/databases/(default)/documents/sites/${siteId}:runQuery?key=${apiKey}`;\n try {\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ structuredQuery: { from: [{ collectionId }] } }),\n next: { revalidate: 300 },\n });\n if (!res.ok) return [];\n const rows = (await res.json()) as { document?: { name: string; fields: Record<string, FsValue> } }[];\n return rows\n .filter((r) => r.document)\n .map((r) => ({ id: r.document!.name.split(\"/\").pop()!, data: decodeFields(r.document!.fields ?? {}) }));\n } catch (err) {\n console.error(`[xicms] Firestore REST-read van ${collectionId} mislukt:`, err);\n return [];\n }\n}\n\n/** Alle symbolen van een site (gevalideerd; ongeldige overgeslagen met waarschuwing). */\nexport async function getSiteSymbols(siteId: string): Promise<StoredSymbol[]> {\n const rows = await readCollection(siteId, \"symbols\");\n const out: StoredSymbol[] = [];\n for (const r of rows) {\n const res = SymbolSchema.safeParse(r.data);\n if (res.success) out.push({ id: r.id, ...res.data });\n else console.warn(`[xicms] symbool \"${r.id}\" ongeldig — overgeslagen.`);\n }\n return out;\n}\n\n/**\n * Vervangt symbolRef-blokken door de blokken van hun symbool (één niveau — symbolen kunnen zelf\n * geen symbolRef bevatten). `_key`s van de ingevoegde blokken worden geprefixt met de ref-key\n * zodat ze uniek blijven wanneer hetzelfde symbool vaker op een pagina staat. Verweesde refs\n * verdwijnen stil (de pagina mag nooit breken op een verwijderd symbool).\n */\nexport function expandSymbols<T extends { blocks?: unknown[] }>(page: T, symbols: StoredSymbol[]): T {\n const byId = new Map(symbols.map((s) => [s.id, s]));\n const blocks = (page.blocks ?? []).flatMap((b) => {\n const blk = b as { _type?: string; _key?: string; symbolId?: string };\n if (blk?._type !== \"symbolRef\") return [b];\n const sym = blk.symbolId ? byId.get(blk.symbolId) : undefined;\n if (!sym) return [];\n return sym.blocks.map((sb, i) => ({ ...(sb as Record<string, unknown>), _key: `${blk._key ?? \"sym\"}-${(sb as { _key?: string })._key ?? i}` }));\n });\n return { ...page, blocks };\n}\n\n/**\n * Gepubliceerde pagina op pad mét symbolRef-expansie (SEO: symboolinhoud zit in de SSR-HTML).\n * Geef `locale` mee (een NIET-default taal) om de vertaal-overlay toe te passen — op de pagina\n * én op de ingevoegde symbolen (elk symbool draagt zijn eigen `translations`).\n */\nexport async function getPublishedPageByPathExpanded(\n siteId: string,\n path: string,\n locale?: string,\n): Promise<StoredPage | null> {\n let page = await getPublishedPageByPath(siteId, path);\n if (!page) return null;\n // De caller geeft alléén een niet-default taal door; \"\" als default matcht dus nooit.\n if (locale) page = localizePage(page, locale, \"\");\n if (!(page.blocks ?? []).some((b) => (b as { _type?: string })._type === \"symbolRef\")) return page;\n let symbols = await getSiteSymbols(siteId);\n if (locale) {\n symbols = symbols.map((s) => ({\n ...s,\n blocks: localizeBlocks(s.blocks as Array<{ _key?: string }>, s.translations?.[locale]) as typeof s.blocks,\n }));\n }\n return expandSymbols(page, symbols);\n}\n\n/** Alle custom blocks van een site (gevalideerd) — voor SSR-registratie via XiCMSProvider. */\nexport async function getSiteCustomBlocks(siteId: string): Promise<CustomBlock[]> {\n const rows = await readCollection(siteId, \"customBlocks\");\n const out: CustomBlock[] = [];\n for (const r of rows) {\n const res = CustomBlockSchema.safeParse({ ...r.data, type: r.data.type ?? r.id });\n if (res.success) out.push(res.data);\n else console.warn(`[xicms] custom block \"${r.id}\" ongeldig — overgeslagen.`);\n }\n return out;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/server/pages.ts"],"sourcesContent":["import \"server-only\";\n\nimport { getAdminDb } from \"../firebase/admin\";\nimport { PageSchema, type Page } from \"../schema/pages\";\nimport { SymbolSchema, type StoredSymbol } from \"../schema/symbols\";\nimport { CustomBlockSchema, type CustomBlock } from \"../schema/customBlocks\";\nimport { localizePage, localizeBlocks } from \"../i18n/overlay\";\n\nexport type StoredPage = Page & { id: string };\n\nfunction toPath(slugs: string[] | undefined): string {\n const clean = (slugs ?? []).filter(Boolean);\n return \"/\" + clean.join(\"/\");\n}\n\n/** Pad uit Next catch-all params (`[[...slug]]`). \"/\" voor de root. */\nexport function pathFromSlugs(slugs: string[] | undefined): string {\n const p = toPath(slugs);\n return p === \"/\" ? \"/\" : p.replace(/\\/$/, \"\");\n}\n\nfunction parse(id: string, data: unknown): StoredPage | null {\n const res = PageSchema.safeParse(data);\n if (!res.success) {\n // Niet stil 404'en: een gepubliceerde pagina die het schema niet haalt is een diagnose-signaal\n // (schema-evolutie zonder default, of corrupte write) — zelfde patroon als symbols/customBlocks.\n console.warn(`[xicms] pagina \"${id}\" faalt schema-validatie en wordt overgeslagen:`, res.error.issues[0]);\n return null;\n }\n return { id, ...res.data };\n}\n\n/**\n * Admin-SDK-reads mogen een PUBLIEKE pagina nooit laten 500'en — zelfde belofte als het REST-pad\n * (dat al overal try/catch → lege fallback heeft). Een Firestore-hikje wordt gelogd en levert de\n * fallback op; de route rendert dan 404/fallback i.p.v. een servererror.\n */\nasync function safeAdminRead<T>(fallback: T, what: string, fn: () => Promise<T>): Promise<T> {\n try {\n return await fn();\n } catch (e) {\n console.warn(`[xicms] Admin-read mislukt (${what}) — fallback gebruikt:`, (e as Error)?.message ?? e);\n return fallback;\n }\n}\n\n/* ---------------- keyless public reads via Firestore REST (alleen apiKey) ----------------\n * Gepubliceerde pagina's zijn publiek leesbaar (rules). De Firestore REST-API honoreert die\n * rules met enkel de web-apiKey (geen Admin-credentials, geen client-SDK), dus dit werkt\n * server-side op keyless deployments (org-policy zonder service-account-keys).\n */\ntype FsValue = Record<string, unknown>;\nfunction decode(v: FsValue): unknown {\n if (v == null) return null;\n if (\"stringValue\" in v) return v.stringValue;\n if (\"integerValue\" in v) return Number(v.integerValue);\n if (\"doubleValue\" in v) return v.doubleValue;\n if (\"booleanValue\" in v) return v.booleanValue;\n if (\"nullValue\" in v) return null;\n if (\"timestampValue\" in v) return v.timestampValue;\n if (\"referenceValue\" in v) return v.referenceValue;\n if (\"geoPointValue\" in v) return v.geoPointValue;\n if (\"arrayValue\" in v) return (((v.arrayValue as FsValue)?.values as FsValue[]) ?? []).map(decode);\n if (\"mapValue\" in v) return decodeFields(((v.mapValue as FsValue)?.fields as Record<string, FsValue>) ?? {});\n return null;\n}\nfunction decodeFields(fields: Record<string, FsValue>): Record<string, unknown> {\n return Object.fromEntries(Object.entries(fields).map(([k, x]) => [k, decode(x)]));\n}\n\ninterface Filter { fieldFilter: { field: { fieldPath: string }; op: \"EQUAL\"; value: { stringValue: string } } }\nfunction eq(fieldPath: string, value: string): Filter {\n return { fieldFilter: { field: { fieldPath }, op: \"EQUAL\", value: { stringValue: value } } };\n}\n\nasync function restQuery(siteId: string, filters: Filter[], limit?: number): Promise<StoredPage[]> {\n const apiKey = process.env.NEXT_PUBLIC_FIREBASE_API_KEY;\n const projectId = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID;\n if (!apiKey || !projectId) return [];\n const url = `https://firestore.googleapis.com/v1/projects/${projectId}/databases/(default)/documents/sites/${siteId}:runQuery?key=${apiKey}`;\n const where = filters.length === 1 ? filters[0] : { compositeFilter: { op: \"AND\", filters } };\n const structuredQuery: Record<string, unknown> = { from: [{ collectionId: \"pages\" }], where };\n if (limit) structuredQuery.limit = limit;\n // Netwerk-/parsefouten mogen een publieke pagina nooit laten crashen (500) — dan liever een\n // lege lijst (Next toont dan een 404 of de fallback, en de volgende revalidate probeert opnieuw).\n try {\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ structuredQuery }),\n // Pagina's zijn ISR-gecached; next-cache mag dit hergebruiken.\n next: { revalidate: 300 },\n });\n if (!res.ok) return [];\n const rows = (await res.json()) as { document?: { name: string; fields: Record<string, FsValue> } }[];\n return rows\n .filter((r) => r.document)\n .map((r) => parse(r.document!.name.split(\"/\").pop()!, decodeFields(r.document!.fields)))\n .filter((p): p is StoredPage => p !== null);\n } catch (err) {\n console.error(\"[xicms] Firestore REST-query mislukt:\", err);\n return [];\n }\n}\n\n/** Alle gepubliceerde pagina's (sitemap / generateStaticParams). Keyless via REST als geen Admin. */\nexport async function listPublishedPages(siteId: string): Promise<StoredPage[]> {\n const adb = getAdminDb();\n if (adb) {\n return safeAdminRead([], `listPublishedPages ${siteId}`, async () => {\n const snap = await adb.collection(`sites/${siteId}/pages`).where(\"status\", \"==\", \"published\").get();\n return snap.docs.map((d) => parse(d.id, d.data())).filter((p): p is StoredPage => p !== null);\n });\n }\n return restQuery(siteId, [eq(\"status\", \"published\")]);\n}\n\n/** Eén gepubliceerde pagina op pad. Keyless via REST als geen Admin. */\nexport async function getPublishedPageByPath(siteId: string, path: string): Promise<StoredPage | null> {\n const adb = getAdminDb();\n if (adb) {\n return safeAdminRead(null, `getPublishedPageByPath ${siteId}${path}`, async () => {\n const snap = await adb\n .collection(`sites/${siteId}/pages`)\n .where(\"status\", \"==\", \"published\")\n .where(\"path\", \"==\", path)\n .limit(1)\n .get();\n const d = snap.docs[0];\n return d ? parse(d.id, d.data()) : null;\n });\n }\n const rows = await restQuery(siteId, [eq(\"status\", \"published\"), eq(\"path\", path)], 1);\n return rows[0] ?? null;\n}\n\n/** Draft-versie (voor preview achter auth). */\nexport async function getDraftPageById(siteId: string, pageId: string): Promise<StoredPage | null> {\n const db = getAdminDb();\n if (!db) return null;\n return safeAdminRead(null, `getDraftPageById ${siteId}/${pageId}`, async () => {\n const d = await db.doc(`sites/${siteId}/drafts/${pageId}`).get();\n return d.exists ? parse(d.id, d.data()) : null;\n });\n}\n\n/** Draft op pad (preview). */\nexport async function getDraftPageByPath(siteId: string, path: string): Promise<StoredPage | null> {\n const db = getAdminDb();\n if (!db) return null;\n return safeAdminRead(null, `getDraftPageByPath ${siteId}${path}`, async () => {\n const snap = await db.collection(`sites/${siteId}/drafts`).where(\"path\", \"==\", path).limit(1).get();\n const d = snap.docs[0];\n return d ? parse(d.id, d.data()) : null;\n });\n}\n\n/* ---------------- symbols & custom blocks (5.0) ---------------- */\n\n/** Generieke publieke collectie-read onder sites/{siteId}/ — Admin als beschikbaar, anders REST. */\nasync function readCollection(siteId: string, collectionId: string): Promise<{ id: string; data: Record<string, unknown> }[]> {\n const adb = getAdminDb();\n if (adb) {\n return safeAdminRead([], `readCollection ${siteId}/${collectionId}`, async () => {\n const snap = await adb.collection(`sites/${siteId}/${collectionId}`).get();\n return snap.docs.map((d) => ({ id: d.id, data: d.data() as Record<string, unknown> }));\n });\n }\n const apiKey = process.env.NEXT_PUBLIC_FIREBASE_API_KEY;\n const projectId = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID;\n if (!apiKey || !projectId) return [];\n const url = `https://firestore.googleapis.com/v1/projects/${projectId}/databases/(default)/documents/sites/${siteId}:runQuery?key=${apiKey}`;\n try {\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ structuredQuery: { from: [{ collectionId }] } }),\n next: { revalidate: 300 },\n });\n if (!res.ok) return [];\n const rows = (await res.json()) as { document?: { name: string; fields: Record<string, FsValue> } }[];\n return rows\n .filter((r) => r.document)\n .map((r) => ({ id: r.document!.name.split(\"/\").pop()!, data: decodeFields(r.document!.fields ?? {}) }));\n } catch (err) {\n console.error(`[xicms] Firestore REST-read van ${collectionId} mislukt:`, err);\n return [];\n }\n}\n\n/** Alle symbolen van een site (gevalideerd; ongeldige overgeslagen met waarschuwing). */\nexport async function getSiteSymbols(siteId: string): Promise<StoredSymbol[]> {\n const rows = await readCollection(siteId, \"symbols\");\n const out: StoredSymbol[] = [];\n for (const r of rows) {\n const res = SymbolSchema.safeParse(r.data);\n if (res.success) out.push({ id: r.id, ...res.data });\n else console.warn(`[xicms] symbool \"${r.id}\" ongeldig — overgeslagen.`);\n }\n return out;\n}\n\n/**\n * Vervangt symbolRef-blokken door de blokken van hun symbool (één niveau — symbolen kunnen zelf\n * geen symbolRef bevatten). `_key`s van de ingevoegde blokken worden geprefixt met de ref-key\n * zodat ze uniek blijven wanneer hetzelfde symbool vaker op een pagina staat. Verweesde refs\n * verdwijnen stil (de pagina mag nooit breken op een verwijderd symbool).\n */\nexport function expandSymbols<T extends { blocks?: unknown[] }>(page: T, symbols: StoredSymbol[]): T {\n const byId = new Map(symbols.map((s) => [s.id, s]));\n const blocks = (page.blocks ?? []).flatMap((b) => {\n const blk = b as { _type?: string; _key?: string; symbolId?: string };\n if (blk?._type !== \"symbolRef\") return [b];\n const sym = blk.symbolId ? byId.get(blk.symbolId) : undefined;\n if (!sym) return [];\n return sym.blocks.map((sb, i) => ({ ...(sb as Record<string, unknown>), _key: `${blk._key ?? \"sym\"}-${(sb as { _key?: string })._key ?? i}` }));\n });\n return { ...page, blocks };\n}\n\n/**\n * Gepubliceerde pagina op pad mét symbolRef-expansie (SEO: symboolinhoud zit in de SSR-HTML).\n * Geef `locale` mee (een NIET-default taal) om de vertaal-overlay toe te passen — op de pagina\n * én op de ingevoegde symbolen (elk symbool draagt zijn eigen `translations`).\n */\nexport async function getPublishedPageByPathExpanded(\n siteId: string,\n path: string,\n locale?: string,\n): Promise<StoredPage | null> {\n let page = await getPublishedPageByPath(siteId, path);\n if (!page) return null;\n // De caller geeft alléén een niet-default taal door; \"\" als default matcht dus nooit.\n if (locale) page = localizePage(page, locale, \"\");\n if (!(page.blocks ?? []).some((b) => (b as { _type?: string })._type === \"symbolRef\")) return page;\n let symbols = await getSiteSymbols(siteId);\n if (locale) {\n symbols = symbols.map((s) => ({\n ...s,\n blocks: localizeBlocks(s.blocks as Array<{ _key?: string }>, s.translations?.[locale]) as typeof s.blocks,\n }));\n }\n return expandSymbols(page, symbols);\n}\n\n/** Alle custom blocks van een site (gevalideerd) — voor SSR-registratie via XiCMSProvider. */\nexport async function getSiteCustomBlocks(siteId: string): Promise<CustomBlock[]> {\n const rows = await readCollection(siteId, \"customBlocks\");\n const out: CustomBlock[] = [];\n for (const r of rows) {\n const res = CustomBlockSchema.safeParse({ ...r.data, type: r.data.type ?? r.id });\n if (res.success) out.push(res.data);\n else console.warn(`[xicms] custom block \"${r.id}\" ongeldig — overgeslagen.`);\n }\n return out;\n}\n"],"mappings":"AAAA,OAAO;AAEP,SAAS,kBAAkB;AAC3B,SAAS,kBAA6B;AACtC,SAAS,oBAAuC;AAChD,SAAS,yBAA2C;AACpD,SAAS,cAAc,sBAAsB;AAI7C,SAAS,OAAO,OAAqC;AACnD,QAAM,SAAS,SAAS,CAAC,GAAG,OAAO,OAAO;AAC1C,SAAO,MAAM,MAAM,KAAK,GAAG;AAC7B;AAGO,SAAS,cAAc,OAAqC;AACjE,QAAM,IAAI,OAAO,KAAK;AACtB,SAAO,MAAM,MAAM,MAAM,EAAE,QAAQ,OAAO,EAAE;AAC9C;AAEA,SAAS,MAAM,IAAY,MAAkC;AAC3D,QAAM,MAAM,WAAW,UAAU,IAAI;AACrC,MAAI,CAAC,IAAI,SAAS;AAGhB,YAAQ,KAAK,mBAAmB,EAAE,mDAAmD,IAAI,MAAM,OAAO,CAAC,CAAC;AACxG,WAAO;AAAA,EACT;AACA,SAAO,EAAE,IAAI,GAAG,IAAI,KAAK;AAC3B;AAOA,eAAe,cAAiB,UAAa,MAAc,IAAkC;AAC3F,MAAI;AACF,WAAO,MAAM,GAAG;AAAA,EAClB,SAAS,GAAG;AACV,YAAQ,KAAK,+BAA+B,IAAI,+BAA2B,GAAa,WAAW,CAAC;AACpG,WAAO;AAAA,EACT;AACF;AAQA,SAAS,OAAO,GAAqB;AACnC,MAAI,KAAK,KAAM,QAAO;AACtB,MAAI,iBAAiB,EAAG,QAAO,EAAE;AACjC,MAAI,kBAAkB,EAAG,QAAO,OAAO,EAAE,YAAY;AACrD,MAAI,iBAAiB,EAAG,QAAO,EAAE;AACjC,MAAI,kBAAkB,EAAG,QAAO,EAAE;AAClC,MAAI,eAAe,EAAG,QAAO;AAC7B,MAAI,oBAAoB,EAAG,QAAO,EAAE;AACpC,MAAI,oBAAoB,EAAG,QAAO,EAAE;AACpC,MAAI,mBAAmB,EAAG,QAAO,EAAE;AACnC,MAAI,gBAAgB,EAAG,SAAU,EAAE,YAAwB,UAAwB,CAAC,GAAG,IAAI,MAAM;AACjG,MAAI,cAAc,EAAG,QAAO,aAAe,EAAE,UAAsB,UAAsC,CAAC,CAAC;AAC3G,SAAO;AACT;AACA,SAAS,aAAa,QAA0D;AAC9E,SAAO,OAAO,YAAY,OAAO,QAAQ,MAAM,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AAClF;AAGA,SAAS,GAAG,WAAmB,OAAuB;AACpD,SAAO,EAAE,aAAa,EAAE,OAAO,EAAE,UAAU,GAAG,IAAI,SAAS,OAAO,EAAE,aAAa,MAAM,EAAE,EAAE;AAC7F;AAEA,eAAe,UAAU,QAAgB,SAAmB,OAAuC;AACjG,QAAM,SAAS,QAAQ,IAAI;AAC3B,QAAM,YAAY,QAAQ,IAAI;AAC9B,MAAI,CAAC,UAAU,CAAC,UAAW,QAAO,CAAC;AACnC,QAAM,MAAM,gDAAgD,SAAS,wCAAwC,MAAM,iBAAiB,MAAM;AAC1I,QAAM,QAAQ,QAAQ,WAAW,IAAI,QAAQ,CAAC,IAAI,EAAE,iBAAiB,EAAE,IAAI,OAAO,QAAQ,EAAE;AAC5F,QAAM,kBAA2C,EAAE,MAAM,CAAC,EAAE,cAAc,QAAQ,CAAC,GAAG,MAAM;AAC5F,MAAI,MAAO,iBAAgB,QAAQ;AAGnC,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,KAAK;AAAA,MAC3B,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,KAAK,UAAU,EAAE,gBAAgB,CAAC;AAAA;AAAA,MAExC,MAAM,EAAE,YAAY,IAAI;AAAA,IAC1B,CAAC;AACD,QAAI,CAAC,IAAI,GAAI,QAAO,CAAC;AACrB,UAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,WAAO,KACJ,OAAO,CAAC,MAAM,EAAE,QAAQ,EACxB,IAAI,CAAC,MAAM,MAAM,EAAE,SAAU,KAAK,MAAM,GAAG,EAAE,IAAI,GAAI,aAAa,EAAE,SAAU,MAAM,CAAC,CAAC,EACtF,OAAO,CAAC,MAAuB,MAAM,IAAI;AAAA,EAC9C,SAAS,KAAK;AACZ,YAAQ,MAAM,yCAAyC,GAAG;AAC1D,WAAO,CAAC;AAAA,EACV;AACF;AAGA,eAAsB,mBAAmB,QAAuC;AAC9E,QAAM,MAAM,WAAW;AACvB,MAAI,KAAK;AACP,WAAO,cAAc,CAAC,GAAG,sBAAsB,MAAM,IAAI,YAAY;AACnE,YAAM,OAAO,MAAM,IAAI,WAAW,SAAS,MAAM,QAAQ,EAAE,MAAM,UAAU,MAAM,WAAW,EAAE,IAAI;AAClG,aAAO,KAAK,KAAK,IAAI,CAAC,MAAM,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,MAAuB,MAAM,IAAI;AAAA,IAC9F,CAAC;AAAA,EACH;AACA,SAAO,UAAU,QAAQ,CAAC,GAAG,UAAU,WAAW,CAAC,CAAC;AACtD;AAGA,eAAsB,uBAAuB,QAAgB,MAA0C;AACrG,QAAM,MAAM,WAAW;AACvB,MAAI,KAAK;AACP,WAAO,cAAc,MAAM,0BAA0B,MAAM,GAAG,IAAI,IAAI,YAAY;AAChF,YAAM,OAAO,MAAM,IAChB,WAAW,SAAS,MAAM,QAAQ,EAClC,MAAM,UAAU,MAAM,WAAW,EACjC,MAAM,QAAQ,MAAM,IAAI,EACxB,MAAM,CAAC,EACP,IAAI;AACP,YAAM,IAAI,KAAK,KAAK,CAAC;AACrB,aAAO,IAAI,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI;AAAA,IACrC,CAAC;AAAA,EACH;AACA,QAAM,OAAO,MAAM,UAAU,QAAQ,CAAC,GAAG,UAAU,WAAW,GAAG,GAAG,QAAQ,IAAI,CAAC,GAAG,CAAC;AACrF,SAAO,KAAK,CAAC,KAAK;AACpB;AAGA,eAAsB,iBAAiB,QAAgB,QAA4C;AACjG,QAAM,KAAK,WAAW;AACtB,MAAI,CAAC,GAAI,QAAO;AAChB,SAAO,cAAc,MAAM,oBAAoB,MAAM,IAAI,MAAM,IAAI,YAAY;AAC7E,UAAM,IAAI,MAAM,GAAG,IAAI,SAAS,MAAM,WAAW,MAAM,EAAE,EAAE,IAAI;AAC/D,WAAO,EAAE,SAAS,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI;AAAA,EAC5C,CAAC;AACH;AAGA,eAAsB,mBAAmB,QAAgB,MAA0C;AACjG,QAAM,KAAK,WAAW;AACtB,MAAI,CAAC,GAAI,QAAO;AAChB,SAAO,cAAc,MAAM,sBAAsB,MAAM,GAAG,IAAI,IAAI,YAAY;AAC5E,UAAM,OAAO,MAAM,GAAG,WAAW,SAAS,MAAM,SAAS,EAAE,MAAM,QAAQ,MAAM,IAAI,EAAE,MAAM,CAAC,EAAE,IAAI;AAClG,UAAM,IAAI,KAAK,KAAK,CAAC;AACrB,WAAO,IAAI,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI;AAAA,EACrC,CAAC;AACH;AAKA,eAAe,eAAe,QAAgB,cAAgF;AAC5H,QAAM,MAAM,WAAW;AACvB,MAAI,KAAK;AACP,WAAO,cAAc,CAAC,GAAG,kBAAkB,MAAM,IAAI,YAAY,IAAI,YAAY;AAC/E,YAAM,OAAO,MAAM,IAAI,WAAW,SAAS,MAAM,IAAI,YAAY,EAAE,EAAE,IAAI;AACzE,aAAO,KAAK,KAAK,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,MAAM,EAAE,KAAK,EAA6B,EAAE;AAAA,IACvF,CAAC;AAAA,EACH;AACA,QAAM,SAAS,QAAQ,IAAI;AAC3B,QAAM,YAAY,QAAQ,IAAI;AAC9B,MAAI,CAAC,UAAU,CAAC,UAAW,QAAO,CAAC;AACnC,QAAM,MAAM,gDAAgD,SAAS,wCAAwC,MAAM,iBAAiB,MAAM;AAC1I,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,KAAK;AAAA,MAC3B,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,KAAK,UAAU,EAAE,iBAAiB,EAAE,MAAM,CAAC,EAAE,aAAa,CAAC,EAAE,EAAE,CAAC;AAAA,MACtE,MAAM,EAAE,YAAY,IAAI;AAAA,IAC1B,CAAC;AACD,QAAI,CAAC,IAAI,GAAI,QAAO,CAAC;AACrB,UAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,WAAO,KACJ,OAAO,CAAC,MAAM,EAAE,QAAQ,EACxB,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,SAAU,KAAK,MAAM,GAAG,EAAE,IAAI,GAAI,MAAM,aAAa,EAAE,SAAU,UAAU,CAAC,CAAC,EAAE,EAAE;AAAA,EAC1G,SAAS,KAAK;AACZ,YAAQ,MAAM,mCAAmC,YAAY,aAAa,GAAG;AAC7E,WAAO,CAAC;AAAA,EACV;AACF;AAGA,eAAsB,eAAe,QAAyC;AAC5E,QAAM,OAAO,MAAM,eAAe,QAAQ,SAAS;AACnD,QAAM,MAAsB,CAAC;AAC7B,aAAW,KAAK,MAAM;AACpB,UAAM,MAAM,aAAa,UAAU,EAAE,IAAI;AACzC,QAAI,IAAI,QAAS,KAAI,KAAK,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI,KAAK,CAAC;AAAA,QAC9C,SAAQ,KAAK,oBAAoB,EAAE,EAAE,iCAA4B;AAAA,EACxE;AACA,SAAO;AACT;AAQO,SAAS,cAAgD,MAAS,SAA4B;AACnG,QAAM,OAAO,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;AAClD,QAAM,UAAU,KAAK,UAAU,CAAC,GAAG,QAAQ,CAAC,MAAM;AAChD,UAAM,MAAM;AACZ,QAAI,KAAK,UAAU,YAAa,QAAO,CAAC,CAAC;AACzC,UAAM,MAAM,IAAI,WAAW,KAAK,IAAI,IAAI,QAAQ,IAAI;AACpD,QAAI,CAAC,IAAK,QAAO,CAAC;AAClB,WAAO,IAAI,OAAO,IAAI,CAAC,IAAI,OAAO,EAAE,GAAI,IAAgC,MAAM,GAAG,IAAI,QAAQ,KAAK,IAAK,GAAyB,QAAQ,CAAC,GAAG,EAAE;AAAA,EAChJ,CAAC;AACD,SAAO,EAAE,GAAG,MAAM,OAAO;AAC3B;AAOA,eAAsB,+BACpB,QACA,MACA,QAC4B;AAC5B,MAAI,OAAO,MAAM,uBAAuB,QAAQ,IAAI;AACpD,MAAI,CAAC,KAAM,QAAO;AAElB,MAAI,OAAQ,QAAO,aAAa,MAAM,QAAQ,EAAE;AAChD,MAAI,EAAE,KAAK,UAAU,CAAC,GAAG,KAAK,CAAC,MAAO,EAAyB,UAAU,WAAW,EAAG,QAAO;AAC9F,MAAI,UAAU,MAAM,eAAe,MAAM;AACzC,MAAI,QAAQ;AACV,cAAU,QAAQ,IAAI,CAAC,OAAO;AAAA,MAC5B,GAAG;AAAA,MACH,QAAQ,eAAe,EAAE,QAAoC,EAAE,eAAe,MAAM,CAAC;AAAA,IACvF,EAAE;AAAA,EACJ;AACA,SAAO,cAAc,MAAM,OAAO;AACpC;AAGA,eAAsB,oBAAoB,QAAwC;AAChF,QAAM,OAAO,MAAM,eAAe,QAAQ,cAAc;AACxD,QAAM,MAAqB,CAAC;AAC5B,aAAW,KAAK,MAAM;AACpB,UAAM,MAAM,kBAAkB,UAAU,EAAE,GAAG,EAAE,MAAM,MAAM,EAAE,KAAK,QAAQ,EAAE,GAAG,CAAC;AAChF,QAAI,IAAI,QAAS,KAAI,KAAK,IAAI,IAAI;AAAA,QAC7B,SAAQ,KAAK,yBAAyB,EAAE,EAAE,iCAA4B;AAAA,EAC7E;AACA,SAAO;AACT;","names":[]}
|
package/dist/styles.css
CHANGED
|
@@ -447,6 +447,43 @@
|
|
|
447
447
|
outline-offset: 3px;
|
|
448
448
|
background: rgba(79, 70, 229, 0.06);
|
|
449
449
|
}
|
|
450
|
+
/* Zwevende opmaak-toolbar boven een gefocust inline rich-text-veld (B/I/U/link/koppen/lijst). */
|
|
451
|
+
.xicms-ve-fmtbar {
|
|
452
|
+
position: fixed;
|
|
453
|
+
z-index: 10005;
|
|
454
|
+
display: flex;
|
|
455
|
+
align-items: center;
|
|
456
|
+
gap: 2px;
|
|
457
|
+
padding: 4px;
|
|
458
|
+
background: #0f172a;
|
|
459
|
+
border-radius: 10px;
|
|
460
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
|
461
|
+
}
|
|
462
|
+
.xicms-ve-fmtbar button {
|
|
463
|
+
min-width: 30px;
|
|
464
|
+
height: 30px;
|
|
465
|
+
padding: 0 7px;
|
|
466
|
+
border: 0;
|
|
467
|
+
border-radius: 7px;
|
|
468
|
+
background: transparent;
|
|
469
|
+
color: #fff;
|
|
470
|
+
font-size: 0.85rem;
|
|
471
|
+
line-height: 1;
|
|
472
|
+
cursor: pointer;
|
|
473
|
+
}
|
|
474
|
+
.xicms-ve-fmtbar button:hover {
|
|
475
|
+
background: rgba(255, 255, 255, 0.16);
|
|
476
|
+
}
|
|
477
|
+
.xicms-ve-fmtbar button:focus-visible {
|
|
478
|
+
outline: 2px solid #a5b4fc;
|
|
479
|
+
outline-offset: 1px;
|
|
480
|
+
}
|
|
481
|
+
.xicms-ve-fmtbar__sep {
|
|
482
|
+
width: 1px;
|
|
483
|
+
align-self: stretch;
|
|
484
|
+
margin: 3px 4px;
|
|
485
|
+
background: rgba(255, 255, 255, 0.25);
|
|
486
|
+
}
|
|
450
487
|
/* Inline afbeelding vervangen (visuele editor): icoontje over het beeld van het geselecteerde blok. */
|
|
451
488
|
.xicms-ve-img { line-height: 0; }
|
|
452
489
|
.xicms-ve-img__btn {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiwebdesign/xicms-core",
|
|
3
|
-
"version": "5.0
|
|
3
|
+
"version": "5.2.0",
|
|
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": {
|