@xiwebdesign/xicms-core 4.18.0 → 5.0.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.
- package/dist/XiCMSProvider.d.ts +7 -0
- package/dist/XiCMSProvider.js +29 -6
- package/dist/XiCMSProvider.js.map +1 -1
- package/dist/admin/blockEditors/GenericEditor.d.ts +22 -0
- package/dist/admin/blockEditors/GenericEditor.js +201 -0
- package/dist/admin/blockEditors/GenericEditor.js.map +1 -0
- package/dist/admin/blockEditors/SymbolRefEditor.d.ts +2 -0
- package/dist/admin/blockEditors/SymbolRefEditor.js +55 -0
- package/dist/admin/blockEditors/SymbolRefEditor.js.map +1 -0
- package/dist/admin/blockEditors/index.d.ts +11 -6
- package/dist/admin/blockEditors/index.js +4 -608
- package/dist/admin/blockEditors/index.js.map +1 -1
- package/dist/admin/customBlocks/CustomBlocksPanel.d.ts +3 -0
- package/dist/admin/customBlocks/CustomBlocksPanel.js +145 -0
- package/dist/admin/customBlocks/CustomBlocksPanel.js.map +1 -0
- package/dist/admin/lib/symbolsClient.d.ts +13 -0
- package/dist/admin/lib/symbolsClient.js +51 -0
- package/dist/admin/lib/symbolsClient.js.map +1 -0
- package/dist/admin/lib/zodFields.d.ts +18 -0
- package/dist/admin/lib/zodFields.js +65 -2
- package/dist/admin/lib/zodFields.js.map +1 -1
- package/dist/admin/panelRegistry.js +3 -0
- package/dist/admin/panelRegistry.js.map +1 -1
- package/dist/admin/settings/SettingsPanel.js +2 -0
- package/dist/admin/settings/SettingsPanel.js.map +1 -1
- package/dist/admin/settings/SnapshotsCard.d.ts +3 -0
- package/dist/admin/settings/SnapshotsCard.js +98 -0
- package/dist/admin/settings/SnapshotsCard.js.map +1 -0
- package/dist/admin/ui/dialog.js +11 -4
- package/dist/admin/ui/dialog.js.map +1 -1
- package/dist/admin/visual/TranslationPanel.d.ts +13 -0
- package/dist/admin/visual/TranslationPanel.js +73 -0
- package/dist/admin/visual/TranslationPanel.js.map +1 -0
- package/dist/admin/visual/VisualEditor.js +142 -17
- package/dist/admin/visual/VisualEditor.js.map +1 -1
- package/dist/admin.css +1 -1
- package/dist/agentFiles/catalog.d.ts +21 -0
- package/dist/agentFiles/catalog.js +78 -0
- package/dist/agentFiles/catalog.js.map +1 -0
- package/dist/agentFiles/files.d.ts +59 -0
- package/dist/agentFiles/files.js +109 -0
- package/dist/agentFiles/files.js.map +1 -0
- package/dist/blocks/builtins/BeforeAfter.js +1 -0
- package/dist/blocks/builtins/BeforeAfter.js.map +1 -1
- package/dist/blocks/builtins/Cards.js +1 -0
- package/dist/blocks/builtins/Cards.js.map +1 -1
- package/dist/blocks/builtins/Cta.js +1 -0
- package/dist/blocks/builtins/Cta.js.map +1 -1
- package/dist/blocks/builtins/Elements.js +1 -0
- package/dist/blocks/builtins/Elements.js.map +1 -1
- package/dist/blocks/builtins/Hero.js +2 -2
- package/dist/blocks/builtins/Hero.js.map +1 -1
- package/dist/blocks/builtins/Jobs.js +1 -0
- package/dist/blocks/builtins/Jobs.js.map +1 -1
- package/dist/blocks/builtins/Sections.js +1 -0
- package/dist/blocks/builtins/Sections.js.map +1 -1
- package/dist/blocks/builtins/SymbolRef.d.ts +2 -0
- package/dist/blocks/builtins/SymbolRef.js +61 -0
- package/dist/blocks/builtins/SymbolRef.js.map +1 -0
- package/dist/blocks/customTemplate/definition.d.ts +7 -0
- package/dist/blocks/customTemplate/definition.js +34 -0
- package/dist/blocks/customTemplate/definition.js.map +1 -0
- package/dist/blocks/customTemplate/interpret.d.ts +10 -0
- package/dist/blocks/customTemplate/interpret.js +136 -0
- package/dist/blocks/customTemplate/interpret.js.map +1 -0
- package/dist/blocks/registry.d.ts +8 -0
- package/dist/blocks/registry.js.map +1 -1
- package/dist/i18n/overlay.d.ts +61 -0
- package/dist/i18n/overlay.js +90 -0
- package/dist/i18n/overlay.js.map +1 -0
- package/dist/index.d.ts +10 -8
- package/dist/index.js +32 -9
- package/dist/index.js.map +1 -1
- package/dist/modules/builtins.js +3 -2
- package/dist/modules/builtins.js.map +1 -1
- package/dist/schema/blocks.d.ts +159 -0
- package/dist/schema/blocks.js +8 -0
- package/dist/schema/blocks.js.map +1 -1
- package/dist/schema/customBlocks.d.ts +47 -0
- package/dist/schema/customBlocks.js +32 -0
- package/dist/schema/customBlocks.js.map +1 -0
- package/dist/schema/fields.d.ts +122 -0
- package/dist/schema/fields.js +120 -0
- package/dist/schema/fields.js.map +1 -0
- package/dist/schema/forms.d.ts +6 -6
- package/dist/schema/pages.d.ts +15 -1
- package/dist/schema/pages.js +6 -2
- package/dist/schema/pages.js.map +1 -1
- package/dist/schema/siteConfig.d.ts +14 -8
- package/dist/schema/siteConfig.js +2 -1
- package/dist/schema/siteConfig.js.map +1 -1
- package/dist/schema/symbols.d.ts +205 -0
- package/dist/schema/symbols.js +18 -0
- package/dist/schema/symbols.js.map +1 -0
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +24 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/pages.d.ts +21 -0
- package/dist/server/pages.js +77 -0
- package/dist/server/pages.js.map +1 -1
- package/dist/server/seo.d.ts +10 -3
- package/dist/server/seo.js +22 -10
- package/dist/server/seo.js.map +1 -1
- package/dist/styles.css +27 -0
- package/package.json +9 -9
- package/dist/components/EditableImage.d.ts +0 -18
- package/dist/components/EditableImage.js +0 -62
- package/dist/components/EditableImage.js.map +0 -1
- package/dist/components/EditableRichText.d.ts +0 -14
- package/dist/components/EditableRichText.js +0 -26
- package/dist/components/EditableRichText.js.map +0 -1
- package/dist/components/EditableText.d.ts +0 -21
- package/dist/components/EditableText.js +0 -25
- package/dist/components/EditableText.js.map +0 -1
- package/dist/content/ContentProvider.d.ts +0 -23
- package/dist/content/ContentProvider.js +0 -86
- package/dist/content/ContentProvider.js.map +0 -1
package/dist/server/index.js
CHANGED
|
@@ -20,10 +20,24 @@ import {
|
|
|
20
20
|
pathFromSlugs,
|
|
21
21
|
listPublishedPages,
|
|
22
22
|
getPublishedPageByPath,
|
|
23
|
+
getPublishedPageByPathExpanded,
|
|
24
|
+
getSiteSymbols,
|
|
25
|
+
expandSymbols,
|
|
26
|
+
getSiteCustomBlocks,
|
|
23
27
|
getDraftPageById,
|
|
24
28
|
getDraftPageByPath
|
|
25
29
|
} from "./pages";
|
|
30
|
+
import {
|
|
31
|
+
siteLocales,
|
|
32
|
+
parseLocaleSlugs,
|
|
33
|
+
localizedPath,
|
|
34
|
+
localizePage,
|
|
35
|
+
localizeBlocks,
|
|
36
|
+
pruneTranslations,
|
|
37
|
+
TranslationsSchema
|
|
38
|
+
} from "../i18n/overlay";
|
|
26
39
|
export {
|
|
40
|
+
TranslationsSchema,
|
|
27
41
|
articleJsonLd,
|
|
28
42
|
assertEnv,
|
|
29
43
|
breadcrumbListJsonLd,
|
|
@@ -32,6 +46,7 @@ export {
|
|
|
32
46
|
buildRobots,
|
|
33
47
|
buildSitemap,
|
|
34
48
|
businessJsonLd,
|
|
49
|
+
expandSymbols,
|
|
35
50
|
galleryJsonLd,
|
|
36
51
|
getAdminApp,
|
|
37
52
|
getAdminAuth,
|
|
@@ -39,11 +54,20 @@ export {
|
|
|
39
54
|
getDraftPageById,
|
|
40
55
|
getDraftPageByPath,
|
|
41
56
|
getPublishedPageByPath,
|
|
57
|
+
getPublishedPageByPathExpanded,
|
|
58
|
+
getSiteCustomBlocks,
|
|
59
|
+
getSiteSymbols,
|
|
42
60
|
listPublishedPages,
|
|
61
|
+
localizeBlocks,
|
|
62
|
+
localizePage,
|
|
63
|
+
localizedPath,
|
|
43
64
|
openingHoursJsonLd,
|
|
65
|
+
parseLocaleSlugs,
|
|
44
66
|
pathFromSlugs,
|
|
45
67
|
productJsonLd,
|
|
68
|
+
pruneTranslations,
|
|
46
69
|
sanitizeRichText,
|
|
70
|
+
siteLocales,
|
|
47
71
|
siteOrigin,
|
|
48
72
|
validateEnv,
|
|
49
73
|
videoObjectJsonLd
|
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 getDraftPageById,\n getDraftPageByPath,\n type StoredPage,\n} from \"./pages\";\n"],"mappings":"AAIA,OAAO;AAEP,SAAS,aAAa,cAAc,kBAAkB;AACtD,SAAS,wBAAwB;AACjC;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,OAEK;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/server/index.ts"],"sourcesContent":["/**\n * @xiwebdesign/xicms-core/server — server-only helpers (firebase-admin, sanitization, SEO,\n * pagina-reads). NOOIT importeren in client-componenten.\n */\nimport \"server-only\";\n\nexport { getAdminApp, getAdminAuth, getAdminDb } from \"../firebase/admin\";\nexport { sanitizeRichText } from \"./sanitize\";\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;AACjC;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.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import "server-only";
|
|
2
2
|
import { type Page } from "../schema/pages";
|
|
3
|
+
import { type StoredSymbol } from "../schema/symbols";
|
|
4
|
+
import { type CustomBlock } from "../schema/customBlocks";
|
|
3
5
|
export type StoredPage = Page & {
|
|
4
6
|
id: string;
|
|
5
7
|
};
|
|
@@ -13,3 +15,22 @@ export declare function getPublishedPageByPath(siteId: string, path: string): Pr
|
|
|
13
15
|
export declare function getDraftPageById(siteId: string, pageId: string): Promise<StoredPage | null>;
|
|
14
16
|
/** Draft op pad (preview). */
|
|
15
17
|
export declare function getDraftPageByPath(siteId: string, path: string): Promise<StoredPage | null>;
|
|
18
|
+
/** Alle symbolen van een site (gevalideerd; ongeldige overgeslagen met waarschuwing). */
|
|
19
|
+
export declare function getSiteSymbols(siteId: string): Promise<StoredSymbol[]>;
|
|
20
|
+
/**
|
|
21
|
+
* Vervangt symbolRef-blokken door de blokken van hun symbool (één niveau — symbolen kunnen zelf
|
|
22
|
+
* geen symbolRef bevatten). `_key`s van de ingevoegde blokken worden geprefixt met de ref-key
|
|
23
|
+
* zodat ze uniek blijven wanneer hetzelfde symbool vaker op een pagina staat. Verweesde refs
|
|
24
|
+
* verdwijnen stil (de pagina mag nooit breken op een verwijderd symbool).
|
|
25
|
+
*/
|
|
26
|
+
export declare function expandSymbols<T extends {
|
|
27
|
+
blocks?: unknown[];
|
|
28
|
+
}>(page: T, symbols: StoredSymbol[]): T;
|
|
29
|
+
/**
|
|
30
|
+
* Gepubliceerde pagina op pad mét symbolRef-expansie (SEO: symboolinhoud zit in de SSR-HTML).
|
|
31
|
+
* Geef `locale` mee (een NIET-default taal) om de vertaal-overlay toe te passen — op de pagina
|
|
32
|
+
* én op de ingevoegde symbolen (elk symbool draagt zijn eigen `translations`).
|
|
33
|
+
*/
|
|
34
|
+
export declare function getPublishedPageByPathExpanded(siteId: string, path: string, locale?: string): Promise<StoredPage | null>;
|
|
35
|
+
/** Alle custom blocks van een site (gevalideerd) — voor SSR-registratie via XiCMSProvider. */
|
|
36
|
+
export declare function getSiteCustomBlocks(siteId: string): Promise<CustomBlock[]>;
|
package/dist/server/pages.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import "server-only";
|
|
2
2
|
import { getAdminDb } from "../firebase/admin";
|
|
3
3
|
import { PageSchema } from "../schema/pages";
|
|
4
|
+
import { SymbolSchema } from "../schema/symbols";
|
|
5
|
+
import { CustomBlockSchema } from "../schema/customBlocks";
|
|
6
|
+
import { localizePage, localizeBlocks } from "../i18n/overlay";
|
|
4
7
|
function toPath(slugs) {
|
|
5
8
|
const clean = (slugs ?? []).filter(Boolean);
|
|
6
9
|
return "/" + clean.join("/");
|
|
@@ -89,10 +92,84 @@ async function getDraftPageByPath(siteId, path) {
|
|
|
89
92
|
const d = snap.docs[0];
|
|
90
93
|
return d ? parse(d.id, d.data()) : null;
|
|
91
94
|
}
|
|
95
|
+
async function readCollection(siteId, collectionId) {
|
|
96
|
+
const adb = getAdminDb();
|
|
97
|
+
if (adb) {
|
|
98
|
+
const snap = await adb.collection(`sites/${siteId}/${collectionId}`).get();
|
|
99
|
+
return snap.docs.map((d) => ({ id: d.id, data: d.data() }));
|
|
100
|
+
}
|
|
101
|
+
const apiKey = process.env.NEXT_PUBLIC_FIREBASE_API_KEY;
|
|
102
|
+
const projectId = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID;
|
|
103
|
+
if (!apiKey || !projectId) return [];
|
|
104
|
+
const url = `https://firestore.googleapis.com/v1/projects/${projectId}/databases/(default)/documents/sites/${siteId}:runQuery?key=${apiKey}`;
|
|
105
|
+
try {
|
|
106
|
+
const res = await fetch(url, {
|
|
107
|
+
method: "POST",
|
|
108
|
+
headers: { "Content-Type": "application/json" },
|
|
109
|
+
body: JSON.stringify({ structuredQuery: { from: [{ collectionId }] } }),
|
|
110
|
+
next: { revalidate: 300 }
|
|
111
|
+
});
|
|
112
|
+
if (!res.ok) return [];
|
|
113
|
+
const rows = await res.json();
|
|
114
|
+
return rows.filter((r) => r.document).map((r) => ({ id: r.document.name.split("/").pop(), data: decodeFields(r.document.fields ?? {}) }));
|
|
115
|
+
} catch (err) {
|
|
116
|
+
console.error(`[xicms] Firestore REST-read van ${collectionId} mislukt:`, err);
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function getSiteSymbols(siteId) {
|
|
121
|
+
const rows = await readCollection(siteId, "symbols");
|
|
122
|
+
const out = [];
|
|
123
|
+
for (const r of rows) {
|
|
124
|
+
const res = SymbolSchema.safeParse(r.data);
|
|
125
|
+
if (res.success) out.push({ id: r.id, ...res.data });
|
|
126
|
+
else console.warn(`[xicms] symbool "${r.id}" ongeldig \u2014 overgeslagen.`);
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
function expandSymbols(page, symbols) {
|
|
131
|
+
const byId = new Map(symbols.map((s) => [s.id, s]));
|
|
132
|
+
const blocks = (page.blocks ?? []).flatMap((b) => {
|
|
133
|
+
const blk = b;
|
|
134
|
+
if (blk?._type !== "symbolRef") return [b];
|
|
135
|
+
const sym = blk.symbolId ? byId.get(blk.symbolId) : void 0;
|
|
136
|
+
if (!sym) return [];
|
|
137
|
+
return sym.blocks.map((sb, i) => ({ ...sb, _key: `${blk._key ?? "sym"}-${sb._key ?? i}` }));
|
|
138
|
+
});
|
|
139
|
+
return { ...page, blocks };
|
|
140
|
+
}
|
|
141
|
+
async function getPublishedPageByPathExpanded(siteId, path, locale) {
|
|
142
|
+
let page = await getPublishedPageByPath(siteId, path);
|
|
143
|
+
if (!page) return null;
|
|
144
|
+
if (locale) page = localizePage(page, locale, "");
|
|
145
|
+
if (!(page.blocks ?? []).some((b) => b._type === "symbolRef")) return page;
|
|
146
|
+
let symbols = await getSiteSymbols(siteId);
|
|
147
|
+
if (locale) {
|
|
148
|
+
symbols = symbols.map((s) => ({
|
|
149
|
+
...s,
|
|
150
|
+
blocks: localizeBlocks(s.blocks, s.translations?.[locale])
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
return expandSymbols(page, symbols);
|
|
154
|
+
}
|
|
155
|
+
async function getSiteCustomBlocks(siteId) {
|
|
156
|
+
const rows = await readCollection(siteId, "customBlocks");
|
|
157
|
+
const out = [];
|
|
158
|
+
for (const r of rows) {
|
|
159
|
+
const res = CustomBlockSchema.safeParse({ ...r.data, type: r.data.type ?? r.id });
|
|
160
|
+
if (res.success) out.push(res.data);
|
|
161
|
+
else console.warn(`[xicms] custom block "${r.id}" ongeldig \u2014 overgeslagen.`);
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
92
165
|
export {
|
|
166
|
+
expandSymbols,
|
|
93
167
|
getDraftPageById,
|
|
94
168
|
getDraftPageByPath,
|
|
95
169
|
getPublishedPageByPath,
|
|
170
|
+
getPublishedPageByPathExpanded,
|
|
171
|
+
getSiteCustomBlocks,
|
|
172
|
+
getSiteSymbols,
|
|
96
173
|
listPublishedPages,
|
|
97
174
|
pathFromSlugs
|
|
98
175
|
};
|
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\";\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"],"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) 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":[]}
|
package/dist/server/seo.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import "server-only";
|
|
|
2
2
|
import type { Metadata, MetadataRoute } from "next";
|
|
3
3
|
import type { Seo } from "../schema/seo";
|
|
4
4
|
import type { Page } from "../schema/pages";
|
|
5
|
+
import { type LocaleConfig } from "../i18n/overlay";
|
|
5
6
|
/** Absolute origin (https://domein) uit de site-config; "" als er geen domein is. */
|
|
6
7
|
export declare function siteOrigin(site: {
|
|
7
8
|
domain?: string;
|
|
@@ -18,7 +19,7 @@ export declare function buildSitemap(pages: Array<{
|
|
|
18
19
|
};
|
|
19
20
|
}>, site: {
|
|
20
21
|
domain?: string;
|
|
21
|
-
}): MetadataRoute.Sitemap;
|
|
22
|
+
}, i18n?: LocaleConfig): MetadataRoute.Sitemap;
|
|
22
23
|
/**
|
|
23
24
|
* Bouwt een Next `robots.txt`. In `mode:"demo"` wordt de hele site op noindex gezet; anders alles
|
|
24
25
|
* toegestaan behalve de beheerpaden, met verwijzing naar de sitemap. Gebruik in `app/robots.ts`.
|
|
@@ -36,8 +37,14 @@ interface SeoSiteContext {
|
|
|
36
37
|
mode?: string;
|
|
37
38
|
seoDefaults?: Seo;
|
|
38
39
|
}
|
|
39
|
-
/**
|
|
40
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Bouwt Next `Metadata` voor een pagina, met fallback op site-defaults. Geef voor een meertalige
|
|
42
|
+
* site (5.0) `i18n` mee: de canonical wijst dan naar de taalvariant en er komen
|
|
43
|
+
* hreflang-alternates voor élke taal (default zonder prefix, incl. x-default).
|
|
44
|
+
*/
|
|
45
|
+
export declare function buildMetadata(page: Pick<Page, "title" | "path" | "seo">, site: SeoSiteContext, i18n?: LocaleConfig & {
|
|
46
|
+
locale: string;
|
|
47
|
+
}): Metadata;
|
|
41
48
|
/** Bedrijfsgegevens voor gestructureerde data (schema.org LocalBusiness e.d.). */
|
|
42
49
|
export interface BusinessInfo {
|
|
43
50
|
/** schema.org-type, bv. "LocalBusiness", "LodgingBusiness", "Restaurant", "Store". */
|
package/dist/server/seo.js
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import "server-only";
|
|
2
|
+
import { localizedPath } from "../i18n/overlay";
|
|
2
3
|
function siteOrigin(site) {
|
|
3
4
|
return site.domain ? `https://${site.domain.replace(/^https?:\/\//, "").replace(/\/$/, "")}` : "";
|
|
4
5
|
}
|
|
5
|
-
function buildSitemap(pages, site) {
|
|
6
|
+
function buildSitemap(pages, site, i18n) {
|
|
6
7
|
const origin = siteOrigin(site);
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
const locales = i18n && i18n.locales.length > 1 ? i18n.locales : [void 0];
|
|
9
|
+
return pages.filter((p) => !p.seo?.noindex).flatMap(
|
|
10
|
+
(p) => locales.map((locale) => {
|
|
11
|
+
const path = locale && i18n ? localizedPath(p.path, locale, i18n.defaultLocale) : p.path;
|
|
12
|
+
return {
|
|
13
|
+
url: `${origin}${path === "/" ? "/" : path}`,
|
|
14
|
+
lastModified: p.updatedAt ? new Date(p.updatedAt) : void 0,
|
|
15
|
+
changeFrequency: p.path === "/" ? "weekly" : "monthly",
|
|
16
|
+
priority: p.path === "/" ? 1 : 0.7
|
|
17
|
+
};
|
|
18
|
+
})
|
|
19
|
+
);
|
|
13
20
|
}
|
|
14
21
|
function buildRobots(site, opts) {
|
|
15
22
|
const origin = siteOrigin(site);
|
|
@@ -21,21 +28,26 @@ function buildRobots(site, opts) {
|
|
|
21
28
|
...origin ? { sitemap: `${origin}/sitemap.xml`, host: origin } : {}
|
|
22
29
|
};
|
|
23
30
|
}
|
|
24
|
-
function buildMetadata(page, site) {
|
|
31
|
+
function buildMetadata(page, site, i18n) {
|
|
25
32
|
const seo = page.seo ?? {};
|
|
26
33
|
const d = site.seoDefaults ?? {};
|
|
27
34
|
const title = seo.title ?? page.title ?? site.name;
|
|
28
35
|
const description = seo.description ?? d.description;
|
|
29
36
|
const origin = site.domain ? `https://${site.domain.replace(/^https?:\/\//, "").replace(/\/$/, "")}` : "";
|
|
30
|
-
const
|
|
37
|
+
const rawPath = seo.canonical ?? page.path;
|
|
38
|
+
const path = i18n && !/^https?:\/\//.test(rawPath) ? localizedPath(rawPath, i18n.locale, i18n.defaultLocale) : rawPath;
|
|
31
39
|
const canonical = /^https?:\/\//.test(path) ? path : `${origin}${path.startsWith("/") ? "" : "/"}${path}`;
|
|
40
|
+
const languages = i18n && i18n.locales.length > 1 ? Object.fromEntries([
|
|
41
|
+
...i18n.locales.map((l) => [l, `${origin}${localizedPath(page.path, l, i18n.defaultLocale)}`]),
|
|
42
|
+
["x-default", `${origin}${page.path}`]
|
|
43
|
+
]) : void 0;
|
|
32
44
|
const ogImage = seo.ogImage ?? d.ogImage;
|
|
33
45
|
const images = ogImage ? [{ url: ogImage, width: 1200, height: 630 }] : void 0;
|
|
34
46
|
return {
|
|
35
47
|
title,
|
|
36
48
|
description,
|
|
37
49
|
metadataBase: origin ? new URL(origin) : void 0,
|
|
38
|
-
alternates: { canonical },
|
|
50
|
+
alternates: { canonical, ...languages ? { languages } : {} },
|
|
39
51
|
robots: seo.noindex || site.mode === "demo" ? { index: false, follow: false } : void 0,
|
|
40
52
|
openGraph: {
|
|
41
53
|
title,
|
package/dist/server/seo.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/server/seo.ts"],"sourcesContent":["import \"server-only\";\n\nimport type { Metadata, MetadataRoute } from \"next\";\nimport type { Seo } from \"../schema/seo\";\nimport type { Page } from \"../schema/pages\";\n\n/** Absolute origin (https://domein) uit de site-config; \"\" als er geen domein is. */\nexport function siteOrigin(site: { domain?: string }): string {\n return site.domain ? `https://${site.domain.replace(/^https?:\\/\\//, \"\").replace(/\\/$/, \"\")}` : \"\";\n}\n\n/**\n * Bouwt een Next `sitemap.xml` uit een lijst pagina's. Gebruik in `app/sitemap.ts`:\n * export default function sitemap() { return buildSitemap(pages, siteConfig); }\n */\nexport function buildSitemap(\n pages: Array<{ path: string; updatedAt?: string | number | Date; seo?: { noindex?: boolean } }>,\n site: { domain?: string },\n): MetadataRoute.Sitemap {\n const origin = siteOrigin(site);\n return pages\n .filter((p) => !p.seo?.noindex)\n .map((p) => ({\n url: `${origin}${p.path === \"/\" ? \"/\" : p.path}`,\n lastModified: p.updatedAt ? new Date(p.updatedAt) : undefined,\n changeFrequency: p.path === \"/\" ? (\"weekly\" as const) : (\"monthly\" as const),\n priority: p.path === \"/\" ? 1 : 0.7,\n }));\n}\n\n/**\n * Bouwt een Next `robots.txt`. In `mode:\"demo\"` wordt de hele site op noindex gezet; anders alles\n * toegestaan behalve de beheerpaden, met verwijzing naar de sitemap. Gebruik in `app/robots.ts`.\n */\nexport function buildRobots(\n site: { domain?: string; mode?: string },\n opts?: { disallow?: string[] },\n): MetadataRoute.Robots {\n const origin = siteOrigin(site);\n if (site.mode === \"demo\") {\n // Let op: disallow blokkeert alleen het CRAWLEN — de URL kan alsnog (kaal) geïndexeerd\n // raken. buildMetadata zet in demo-modus daarom óók robots.index=false op elke pagina.\n return { rules: { userAgent: \"*\", disallow: \"/\" } };\n }\n return {\n rules: { userAgent: \"*\", allow: \"/\", disallow: opts?.disallow ?? [\"/beheer\", \"/login\"] },\n ...(origin ? { sitemap: `${origin}/sitemap.xml`, host: origin } : {}),\n };\n}\n\ninterface SeoSiteContext {\n name: string;\n domain?: string;\n /** \"demo\" zet elke pagina op noindex (robots.txt-disallow alleen is geen noindex). */\n mode?: string;\n seoDefaults?: Seo;\n}\n\n/** Bouwt Next `Metadata` voor een pagina, met fallback op site-defaults. */\nexport function buildMetadata(page: Pick<Page, \"title\" | \"path\" | \"seo\">, site: SeoSiteContext): Metadata {\n const seo = page.seo ?? {};\n const d = site.seoDefaults ?? {};\n const title = seo.title ?? page.title ?? site.name;\n const description = seo.description ?? d.description;\n // Absolute canonical/OG-URL (Google prefereert absoluut) wanneer het domein bekend is.\n const origin = site.domain ? `https://${site.domain.replace(/^https?:\\/\\//, \"\").replace(/\\/$/, \"\")}` : \"\";\n const path = seo.canonical ?? page.path;\n const canonical = /^https?:\\/\\//.test(path) ? path : `${origin}${path.startsWith(\"/\") ? \"\" : \"/\"}${path}`;\n const ogImage = seo.ogImage ?? d.ogImage;\n // OG-afbeeldingen met afmetingen (1200×630) → betere previews op social.\n const images = ogImage ? [{ url: ogImage, width: 1200, height: 630 }] : undefined;\n\n return {\n title,\n description,\n metadataBase: origin ? new URL(origin) : undefined,\n alternates: { canonical },\n robots: seo.noindex || site.mode === \"demo\" ? { index: false, follow: false } : undefined,\n openGraph: {\n title,\n description,\n url: canonical,\n siteName: site.name,\n type: \"website\",\n ...(images ? { images } : {}),\n },\n twitter: {\n card: images ? \"summary_large_image\" : \"summary\",\n title,\n description,\n ...(images ? { images: [ogImage as string] } : {}),\n },\n };\n}\n\n/** Bedrijfsgegevens voor gestructureerde data (schema.org LocalBusiness e.d.). */\nexport interface BusinessInfo {\n /** schema.org-type, bv. \"LocalBusiness\", \"LodgingBusiness\", \"Restaurant\", \"Store\". */\n type?: string;\n name: string;\n description?: string;\n url?: string;\n telephone?: string;\n email?: string;\n image?: string;\n /** Indicatie prijsniveau, bv. \"€€\". */\n priceRange?: string;\n address?: {\n street?: string;\n locality?: string;\n region?: string;\n postalCode?: string;\n country?: string;\n };\n geo?: { lat: number; lng: number };\n /** schema.org openingHours, bv. [\"Mo-Fr 09:00-17:00\", \"Sa 10:00-16:00\"]. */\n openingHours?: string[];\n /** Externe profielen (social, etc.). */\n sameAs?: string[];\n}\n\n/**\n * Bouwt gestructureerde data voor een lokaal bedrijf (herbruikbaar per branche). Plaats het\n * resultaat als JSON-LD in de layout: <script type=\"application/ld+json\">JSON.stringify(...)</script>\n */\nexport function businessJsonLd(info: BusinessInfo): Record<string, unknown> {\n const out: Record<string, unknown> = {\n \"@context\": \"https://schema.org\",\n \"@type\": info.type ?? \"LocalBusiness\",\n // Zelfde relatieve @id als de contact-/openingstijden-blokken: zo voegen zoekmachines de\n // layout-brede en blok-specifieke LocalBusiness-JSON-LD samen tot één entiteit per pagina.\n \"@id\": \"#business\",\n name: info.name,\n };\n if (info.description) out.description = info.description;\n if (info.url) out.url = info.url;\n if (info.telephone) out.telephone = info.telephone;\n if (info.email) out.email = info.email;\n if (info.image) out.image = info.image;\n if (info.priceRange) out.priceRange = info.priceRange;\n if (info.address) {\n out.address = {\n \"@type\": \"PostalAddress\",\n streetAddress: info.address.street,\n addressLocality: info.address.locality,\n addressRegion: info.address.region,\n postalCode: info.address.postalCode,\n addressCountry: info.address.country,\n };\n }\n if (info.geo) out.geo = { \"@type\": \"GeoCoordinates\", latitude: info.geo.lat, longitude: info.geo.lng };\n if (info.openingHours?.length) out.openingHours = info.openingHours;\n if (info.sameAs?.length) out.sameAs = info.sameAs;\n return out;\n}\n\n/** Breadcrumb-trail (schema.org BreadcrumbList) — voor genest navigeren + rich results. */\nexport function breadcrumbListJsonLd(items: { name: string; url: string }[]): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"BreadcrumbList\",\n itemListElement: items.map((it, i) => ({\n \"@type\": \"ListItem\",\n position: i + 1,\n name: it.name,\n item: it.url,\n })),\n };\n}\n\n/** Article-JSON-LD voor nieuws/blog. */\nexport function articleJsonLd(a: {\n title: string;\n description?: string;\n author?: string;\n datePublished?: string;\n image?: string;\n url?: string;\n}): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Article\",\n headline: a.title,\n ...(a.description ? { description: a.description } : {}),\n ...(a.author ? { author: { \"@type\": \"Person\", name: a.author } } : {}),\n ...(a.datePublished ? { datePublished: a.datePublished } : {}),\n ...(a.image ? { image: a.image } : {}),\n ...(a.url ? { url: a.url } : {}),\n };\n}\n\n/** Product/Offer-JSON-LD. */\nexport function productJsonLd(p: {\n name: string;\n description?: string;\n price?: string | number;\n currency?: string;\n image?: string;\n availability?: string;\n}): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Product\",\n name: p.name,\n ...(p.description ? { description: p.description } : {}),\n ...(p.image ? { image: p.image } : {}),\n ...(p.price != null\n ? {\n offers: {\n \"@type\": \"Offer\",\n price: String(p.price),\n priceCurrency: p.currency ?? \"EUR\",\n ...(p.availability ? { availability: `https://schema.org/${p.availability}` } : {}),\n },\n }\n : {}),\n };\n}\n\n/** OpeningHoursSpecification — bv. `[{ days:[\"Mo\",\"Tu\"], opens:\"09:00\", closes:\"17:00\" }]`. */\nexport function openingHoursJsonLd(\n spec: { days: string[]; opens: string; closes: string }[],\n): Record<string, unknown>[] {\n return spec.map((s) => ({\n \"@type\": \"OpeningHoursSpecification\",\n dayOfWeek: s.days,\n opens: s.opens,\n closes: s.closes,\n }));\n}\n\n/** ImageGallery-JSON-LD voor een fotogalerij. */\nexport function galleryJsonLd(items: { url: string; caption?: string }[]): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"ImageGallery\",\n image: items.map((it) => ({ \"@type\": \"ImageObject\", contentUrl: it.url, ...(it.caption ? { caption: it.caption } : {}) })),\n };\n}\n\n/** VideoObject-JSON-LD. */\nexport function videoObjectJsonLd(v: {\n name: string;\n description?: string;\n thumbnailUrl?: string;\n contentUrl?: string;\n uploadDate?: string;\n}): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"VideoObject\",\n name: v.name,\n ...(v.description ? { description: v.description } : {}),\n ...(v.thumbnailUrl ? { thumbnailUrl: v.thumbnailUrl } : {}),\n ...(v.contentUrl ? { contentUrl: v.contentUrl } : {}),\n ...(v.uploadDate ? { uploadDate: v.uploadDate } : {}),\n };\n}\n\n/** Bouwt een JSON-LD-object voor een pagina (schema.org). */\nexport function buildJsonLd(\n page: Pick<Page, \"title\" | \"path\" | \"seo\">,\n site: SeoSiteContext,\n): Record<string, unknown> {\n const type = page.seo?.jsonLdType ?? \"WebPage\";\n const base = site.domain ? `https://${site.domain.replace(/^https?:\\/\\//, \"\")}` : \"\";\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": type,\n name: page.seo?.title ?? page.title,\n description: page.seo?.description ?? site.seoDefaults?.description,\n url: base + page.path,\n publisher: { \"@type\": \"Organization\", name: site.name },\n };\n}\n"],"mappings":"AAAA,OAAO;AAOA,SAAS,WAAW,MAAmC;AAC5D,SAAO,KAAK,SAAS,WAAW,KAAK,OAAO,QAAQ,gBAAgB,EAAE,EAAE,QAAQ,OAAO,EAAE,CAAC,KAAK;AACjG;AAMO,SAAS,aACd,OACA,MACuB;AACvB,QAAM,SAAS,WAAW,IAAI;AAC9B,SAAO,MACJ,OAAO,CAAC,MAAM,CAAC,EAAE,KAAK,OAAO,EAC7B,IAAI,CAAC,OAAO;AAAA,IACX,KAAK,GAAG,MAAM,GAAG,EAAE,SAAS,MAAM,MAAM,EAAE,IAAI;AAAA,IAC9C,cAAc,EAAE,YAAY,IAAI,KAAK,EAAE,SAAS,IAAI;AAAA,IACpD,iBAAiB,EAAE,SAAS,MAAO,WAAsB;AAAA,IACzD,UAAU,EAAE,SAAS,MAAM,IAAI;AAAA,EACjC,EAAE;AACN;AAMO,SAAS,YACd,MACA,MACsB;AACtB,QAAM,SAAS,WAAW,IAAI;AAC9B,MAAI,KAAK,SAAS,QAAQ;AAGxB,WAAO,EAAE,OAAO,EAAE,WAAW,KAAK,UAAU,IAAI,EAAE;AAAA,EACpD;AACA,SAAO;AAAA,IACL,OAAO,EAAE,WAAW,KAAK,OAAO,KAAK,UAAU,MAAM,YAAY,CAAC,WAAW,QAAQ,EAAE;AAAA,IACvF,GAAI,SAAS,EAAE,SAAS,GAAG,MAAM,gBAAgB,MAAM,OAAO,IAAI,CAAC;AAAA,EACrE;AACF;AAWO,SAAS,cAAc,MAA4C,MAAgC;AACxG,QAAM,MAAM,KAAK,OAAO,CAAC;AACzB,QAAM,IAAI,KAAK,eAAe,CAAC;AAC/B,QAAM,QAAQ,IAAI,SAAS,KAAK,SAAS,KAAK;AAC9C,QAAM,cAAc,IAAI,eAAe,EAAE;AAEzC,QAAM,SAAS,KAAK,SAAS,WAAW,KAAK,OAAO,QAAQ,gBAAgB,EAAE,EAAE,QAAQ,OAAO,EAAE,CAAC,KAAK;AACvG,QAAM,OAAO,IAAI,aAAa,KAAK;AACnC,QAAM,YAAY,eAAe,KAAK,IAAI,IAAI,OAAO,GAAG,MAAM,GAAG,KAAK,WAAW,GAAG,IAAI,KAAK,GAAG,GAAG,IAAI;AACvG,QAAM,UAAU,IAAI,WAAW,EAAE;AAEjC,QAAM,SAAS,UAAU,CAAC,EAAE,KAAK,SAAS,OAAO,MAAM,QAAQ,IAAI,CAAC,IAAI;AAExE,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,cAAc,SAAS,IAAI,IAAI,MAAM,IAAI;AAAA,IACzC,YAAY,EAAE,UAAU;AAAA,IACxB,QAAQ,IAAI,WAAW,KAAK,SAAS,SAAS,EAAE,OAAO,OAAO,QAAQ,MAAM,IAAI;AAAA,IAChF,WAAW;AAAA,MACT;AAAA,MACA;AAAA,MACA,KAAK;AAAA,MACL,UAAU,KAAK;AAAA,MACf,MAAM;AAAA,MACN,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,IAC7B;AAAA,IACA,SAAS;AAAA,MACP,MAAM,SAAS,wBAAwB;AAAA,MACvC;AAAA,MACA;AAAA,MACA,GAAI,SAAS,EAAE,QAAQ,CAAC,OAAiB,EAAE,IAAI,CAAC;AAAA,IAClD;AAAA,EACF;AACF;AAgCO,SAAS,eAAe,MAA6C;AAC1E,QAAM,MAA+B;AAAA,IACnC,YAAY;AAAA,IACZ,SAAS,KAAK,QAAQ;AAAA;AAAA;AAAA,IAGtB,OAAO;AAAA,IACP,MAAM,KAAK;AAAA,EACb;AACA,MAAI,KAAK,YAAa,KAAI,cAAc,KAAK;AAC7C,MAAI,KAAK,IAAK,KAAI,MAAM,KAAK;AAC7B,MAAI,KAAK,UAAW,KAAI,YAAY,KAAK;AACzC,MAAI,KAAK,MAAO,KAAI,QAAQ,KAAK;AACjC,MAAI,KAAK,MAAO,KAAI,QAAQ,KAAK;AACjC,MAAI,KAAK,WAAY,KAAI,aAAa,KAAK;AAC3C,MAAI,KAAK,SAAS;AAChB,QAAI,UAAU;AAAA,MACZ,SAAS;AAAA,MACT,eAAe,KAAK,QAAQ;AAAA,MAC5B,iBAAiB,KAAK,QAAQ;AAAA,MAC9B,eAAe,KAAK,QAAQ;AAAA,MAC5B,YAAY,KAAK,QAAQ;AAAA,MACzB,gBAAgB,KAAK,QAAQ;AAAA,IAC/B;AAAA,EACF;AACA,MAAI,KAAK,IAAK,KAAI,MAAM,EAAE,SAAS,kBAAkB,UAAU,KAAK,IAAI,KAAK,WAAW,KAAK,IAAI,IAAI;AACrG,MAAI,KAAK,cAAc,OAAQ,KAAI,eAAe,KAAK;AACvD,MAAI,KAAK,QAAQ,OAAQ,KAAI,SAAS,KAAK;AAC3C,SAAO;AACT;AAGO,SAAS,qBAAqB,OAAiE;AACpG,SAAO;AAAA,IACL,YAAY;AAAA,IACZ,SAAS;AAAA,IACT,iBAAiB,MAAM,IAAI,CAAC,IAAI,OAAO;AAAA,MACrC,SAAS;AAAA,MACT,UAAU,IAAI;AAAA,MACd,MAAM,GAAG;AAAA,MACT,MAAM,GAAG;AAAA,IACX,EAAE;AAAA,EACJ;AACF;AAGO,SAAS,cAAc,GAOF;AAC1B,SAAO;AAAA,IACL,YAAY;AAAA,IACZ,SAAS;AAAA,IACT,UAAU,EAAE;AAAA,IACZ,GAAI,EAAE,cAAc,EAAE,aAAa,EAAE,YAAY,IAAI,CAAC;AAAA,IACtD,GAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,UAAU,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IACpE,GAAI,EAAE,gBAAgB,EAAE,eAAe,EAAE,cAAc,IAAI,CAAC;AAAA,IAC5D,GAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAAA,IACpC,GAAI,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,IAAI,CAAC;AAAA,EAChC;AACF;AAGO,SAAS,cAAc,GAOF;AAC1B,SAAO;AAAA,IACL,YAAY;AAAA,IACZ,SAAS;AAAA,IACT,MAAM,EAAE;AAAA,IACR,GAAI,EAAE,cAAc,EAAE,aAAa,EAAE,YAAY,IAAI,CAAC;AAAA,IACtD,GAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAAA,IACpC,GAAI,EAAE,SAAS,OACX;AAAA,MACE,QAAQ;AAAA,QACN,SAAS;AAAA,QACT,OAAO,OAAO,EAAE,KAAK;AAAA,QACrB,eAAe,EAAE,YAAY;AAAA,QAC7B,GAAI,EAAE,eAAe,EAAE,cAAc,sBAAsB,EAAE,YAAY,GAAG,IAAI,CAAC;AAAA,MACnF;AAAA,IACF,IACA,CAAC;AAAA,EACP;AACF;AAGO,SAAS,mBACd,MAC2B;AAC3B,SAAO,KAAK,IAAI,CAAC,OAAO;AAAA,IACtB,SAAS;AAAA,IACT,WAAW,EAAE;AAAA,IACb,OAAO,EAAE;AAAA,IACT,QAAQ,EAAE;AAAA,EACZ,EAAE;AACJ;AAGO,SAAS,cAAc,OAAqE;AACjG,SAAO;AAAA,IACL,YAAY;AAAA,IACZ,SAAS;AAAA,IACT,OAAO,MAAM,IAAI,CAAC,QAAQ,EAAE,SAAS,eAAe,YAAY,GAAG,KAAK,GAAI,GAAG,UAAU,EAAE,SAAS,GAAG,QAAQ,IAAI,CAAC,EAAG,EAAE;AAAA,EAC3H;AACF;AAGO,SAAS,kBAAkB,GAMN;AAC1B,SAAO;AAAA,IACL,YAAY;AAAA,IACZ,SAAS;AAAA,IACT,MAAM,EAAE;AAAA,IACR,GAAI,EAAE,cAAc,EAAE,aAAa,EAAE,YAAY,IAAI,CAAC;AAAA,IACtD,GAAI,EAAE,eAAe,EAAE,cAAc,EAAE,aAAa,IAAI,CAAC;AAAA,IACzD,GAAI,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,IAAI,CAAC;AAAA,IACnD,GAAI,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,IAAI,CAAC;AAAA,EACrD;AACF;AAGO,SAAS,YACd,MACA,MACyB;AACzB,QAAM,OAAO,KAAK,KAAK,cAAc;AACrC,QAAM,OAAO,KAAK,SAAS,WAAW,KAAK,OAAO,QAAQ,gBAAgB,EAAE,CAAC,KAAK;AAClF,SAAO;AAAA,IACL,YAAY;AAAA,IACZ,SAAS;AAAA,IACT,MAAM,KAAK,KAAK,SAAS,KAAK;AAAA,IAC9B,aAAa,KAAK,KAAK,eAAe,KAAK,aAAa;AAAA,IACxD,KAAK,OAAO,KAAK;AAAA,IACjB,WAAW,EAAE,SAAS,gBAAgB,MAAM,KAAK,KAAK;AAAA,EACxD;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/server/seo.ts"],"sourcesContent":["import \"server-only\";\n\nimport type { Metadata, MetadataRoute } from \"next\";\nimport type { Seo } from \"../schema/seo\";\nimport type { Page } from \"../schema/pages\";\nimport { localizedPath, type LocaleConfig } from \"../i18n/overlay\";\n\n/** Absolute origin (https://domein) uit de site-config; \"\" als er geen domein is. */\nexport function siteOrigin(site: { domain?: string }): string {\n return site.domain ? `https://${site.domain.replace(/^https?:\\/\\//, \"\").replace(/\\/$/, \"\")}` : \"\";\n}\n\n/**\n * Bouwt een Next `sitemap.xml` uit een lijst pagina's. Gebruik in `app/sitemap.ts`:\n * export default function sitemap() { return buildSitemap(pages, siteConfig); }\n */\nexport function buildSitemap(\n pages: Array<{ path: string; updatedAt?: string | number | Date; seo?: { noindex?: boolean } }>,\n site: { domain?: string },\n i18n?: LocaleConfig,\n): MetadataRoute.Sitemap {\n const origin = siteOrigin(site);\n // Meertalige site (5.0): elke pagina één keer per taal (default zonder prefix, rest mét).\n const locales = i18n && i18n.locales.length > 1 ? i18n.locales : [undefined];\n return pages\n .filter((p) => !p.seo?.noindex)\n .flatMap((p) =>\n locales.map((locale) => {\n const path = locale && i18n ? localizedPath(p.path, locale, i18n.defaultLocale) : p.path;\n return {\n url: `${origin}${path === \"/\" ? \"/\" : path}`,\n lastModified: p.updatedAt ? new Date(p.updatedAt) : undefined,\n changeFrequency: p.path === \"/\" ? (\"weekly\" as const) : (\"monthly\" as const),\n priority: p.path === \"/\" ? 1 : 0.7,\n };\n }),\n );\n}\n\n/**\n * Bouwt een Next `robots.txt`. In `mode:\"demo\"` wordt de hele site op noindex gezet; anders alles\n * toegestaan behalve de beheerpaden, met verwijzing naar de sitemap. Gebruik in `app/robots.ts`.\n */\nexport function buildRobots(\n site: { domain?: string; mode?: string },\n opts?: { disallow?: string[] },\n): MetadataRoute.Robots {\n const origin = siteOrigin(site);\n if (site.mode === \"demo\") {\n // Let op: disallow blokkeert alleen het CRAWLEN — de URL kan alsnog (kaal) geïndexeerd\n // raken. buildMetadata zet in demo-modus daarom óók robots.index=false op elke pagina.\n return { rules: { userAgent: \"*\", disallow: \"/\" } };\n }\n return {\n rules: { userAgent: \"*\", allow: \"/\", disallow: opts?.disallow ?? [\"/beheer\", \"/login\"] },\n ...(origin ? { sitemap: `${origin}/sitemap.xml`, host: origin } : {}),\n };\n}\n\ninterface SeoSiteContext {\n name: string;\n domain?: string;\n /** \"demo\" zet elke pagina op noindex (robots.txt-disallow alleen is geen noindex). */\n mode?: string;\n seoDefaults?: Seo;\n}\n\n/**\n * Bouwt Next `Metadata` voor een pagina, met fallback op site-defaults. Geef voor een meertalige\n * site (5.0) `i18n` mee: de canonical wijst dan naar de taalvariant en er komen\n * hreflang-alternates voor élke taal (default zonder prefix, incl. x-default).\n */\nexport function buildMetadata(\n page: Pick<Page, \"title\" | \"path\" | \"seo\">,\n site: SeoSiteContext,\n i18n?: LocaleConfig & { locale: string },\n): Metadata {\n const seo = page.seo ?? {};\n const d = site.seoDefaults ?? {};\n const title = seo.title ?? page.title ?? site.name;\n const description = seo.description ?? d.description;\n // Absolute canonical/OG-URL (Google prefereert absoluut) wanneer het domein bekend is.\n const origin = site.domain ? `https://${site.domain.replace(/^https?:\\/\\//, \"\").replace(/\\/$/, \"\")}` : \"\";\n const rawPath = seo.canonical ?? page.path;\n const path = i18n && !/^https?:\\/\\//.test(rawPath) ? localizedPath(rawPath, i18n.locale, i18n.defaultLocale) : rawPath;\n const canonical = /^https?:\\/\\//.test(path) ? path : `${origin}${path.startsWith(\"/\") ? \"\" : \"/\"}${path}`;\n const languages =\n i18n && i18n.locales.length > 1\n ? Object.fromEntries([\n ...i18n.locales.map((l) => [l, `${origin}${localizedPath(page.path, l, i18n.defaultLocale)}`]),\n [\"x-default\", `${origin}${page.path}`],\n ])\n : undefined;\n const ogImage = seo.ogImage ?? d.ogImage;\n // OG-afbeeldingen met afmetingen (1200×630) → betere previews op social.\n const images = ogImage ? [{ url: ogImage, width: 1200, height: 630 }] : undefined;\n\n return {\n title,\n description,\n metadataBase: origin ? new URL(origin) : undefined,\n alternates: { canonical, ...(languages ? { languages } : {}) },\n robots: seo.noindex || site.mode === \"demo\" ? { index: false, follow: false } : undefined,\n openGraph: {\n title,\n description,\n url: canonical,\n siteName: site.name,\n type: \"website\",\n ...(images ? { images } : {}),\n },\n twitter: {\n card: images ? \"summary_large_image\" : \"summary\",\n title,\n description,\n ...(images ? { images: [ogImage as string] } : {}),\n },\n };\n}\n\n/** Bedrijfsgegevens voor gestructureerde data (schema.org LocalBusiness e.d.). */\nexport interface BusinessInfo {\n /** schema.org-type, bv. \"LocalBusiness\", \"LodgingBusiness\", \"Restaurant\", \"Store\". */\n type?: string;\n name: string;\n description?: string;\n url?: string;\n telephone?: string;\n email?: string;\n image?: string;\n /** Indicatie prijsniveau, bv. \"€€\". */\n priceRange?: string;\n address?: {\n street?: string;\n locality?: string;\n region?: string;\n postalCode?: string;\n country?: string;\n };\n geo?: { lat: number; lng: number };\n /** schema.org openingHours, bv. [\"Mo-Fr 09:00-17:00\", \"Sa 10:00-16:00\"]. */\n openingHours?: string[];\n /** Externe profielen (social, etc.). */\n sameAs?: string[];\n}\n\n/**\n * Bouwt gestructureerde data voor een lokaal bedrijf (herbruikbaar per branche). Plaats het\n * resultaat als JSON-LD in de layout: <script type=\"application/ld+json\">JSON.stringify(...)</script>\n */\nexport function businessJsonLd(info: BusinessInfo): Record<string, unknown> {\n const out: Record<string, unknown> = {\n \"@context\": \"https://schema.org\",\n \"@type\": info.type ?? \"LocalBusiness\",\n // Zelfde relatieve @id als de contact-/openingstijden-blokken: zo voegen zoekmachines de\n // layout-brede en blok-specifieke LocalBusiness-JSON-LD samen tot één entiteit per pagina.\n \"@id\": \"#business\",\n name: info.name,\n };\n if (info.description) out.description = info.description;\n if (info.url) out.url = info.url;\n if (info.telephone) out.telephone = info.telephone;\n if (info.email) out.email = info.email;\n if (info.image) out.image = info.image;\n if (info.priceRange) out.priceRange = info.priceRange;\n if (info.address) {\n out.address = {\n \"@type\": \"PostalAddress\",\n streetAddress: info.address.street,\n addressLocality: info.address.locality,\n addressRegion: info.address.region,\n postalCode: info.address.postalCode,\n addressCountry: info.address.country,\n };\n }\n if (info.geo) out.geo = { \"@type\": \"GeoCoordinates\", latitude: info.geo.lat, longitude: info.geo.lng };\n if (info.openingHours?.length) out.openingHours = info.openingHours;\n if (info.sameAs?.length) out.sameAs = info.sameAs;\n return out;\n}\n\n/** Breadcrumb-trail (schema.org BreadcrumbList) — voor genest navigeren + rich results. */\nexport function breadcrumbListJsonLd(items: { name: string; url: string }[]): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"BreadcrumbList\",\n itemListElement: items.map((it, i) => ({\n \"@type\": \"ListItem\",\n position: i + 1,\n name: it.name,\n item: it.url,\n })),\n };\n}\n\n/** Article-JSON-LD voor nieuws/blog. */\nexport function articleJsonLd(a: {\n title: string;\n description?: string;\n author?: string;\n datePublished?: string;\n image?: string;\n url?: string;\n}): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Article\",\n headline: a.title,\n ...(a.description ? { description: a.description } : {}),\n ...(a.author ? { author: { \"@type\": \"Person\", name: a.author } } : {}),\n ...(a.datePublished ? { datePublished: a.datePublished } : {}),\n ...(a.image ? { image: a.image } : {}),\n ...(a.url ? { url: a.url } : {}),\n };\n}\n\n/** Product/Offer-JSON-LD. */\nexport function productJsonLd(p: {\n name: string;\n description?: string;\n price?: string | number;\n currency?: string;\n image?: string;\n availability?: string;\n}): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Product\",\n name: p.name,\n ...(p.description ? { description: p.description } : {}),\n ...(p.image ? { image: p.image } : {}),\n ...(p.price != null\n ? {\n offers: {\n \"@type\": \"Offer\",\n price: String(p.price),\n priceCurrency: p.currency ?? \"EUR\",\n ...(p.availability ? { availability: `https://schema.org/${p.availability}` } : {}),\n },\n }\n : {}),\n };\n}\n\n/** OpeningHoursSpecification — bv. `[{ days:[\"Mo\",\"Tu\"], opens:\"09:00\", closes:\"17:00\" }]`. */\nexport function openingHoursJsonLd(\n spec: { days: string[]; opens: string; closes: string }[],\n): Record<string, unknown>[] {\n return spec.map((s) => ({\n \"@type\": \"OpeningHoursSpecification\",\n dayOfWeek: s.days,\n opens: s.opens,\n closes: s.closes,\n }));\n}\n\n/** ImageGallery-JSON-LD voor een fotogalerij. */\nexport function galleryJsonLd(items: { url: string; caption?: string }[]): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"ImageGallery\",\n image: items.map((it) => ({ \"@type\": \"ImageObject\", contentUrl: it.url, ...(it.caption ? { caption: it.caption } : {}) })),\n };\n}\n\n/** VideoObject-JSON-LD. */\nexport function videoObjectJsonLd(v: {\n name: string;\n description?: string;\n thumbnailUrl?: string;\n contentUrl?: string;\n uploadDate?: string;\n}): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"VideoObject\",\n name: v.name,\n ...(v.description ? { description: v.description } : {}),\n ...(v.thumbnailUrl ? { thumbnailUrl: v.thumbnailUrl } : {}),\n ...(v.contentUrl ? { contentUrl: v.contentUrl } : {}),\n ...(v.uploadDate ? { uploadDate: v.uploadDate } : {}),\n };\n}\n\n/** Bouwt een JSON-LD-object voor een pagina (schema.org). */\nexport function buildJsonLd(\n page: Pick<Page, \"title\" | \"path\" | \"seo\">,\n site: SeoSiteContext,\n): Record<string, unknown> {\n const type = page.seo?.jsonLdType ?? \"WebPage\";\n const base = site.domain ? `https://${site.domain.replace(/^https?:\\/\\//, \"\")}` : \"\";\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": type,\n name: page.seo?.title ?? page.title,\n description: page.seo?.description ?? site.seoDefaults?.description,\n url: base + page.path,\n publisher: { \"@type\": \"Organization\", name: site.name },\n };\n}\n"],"mappings":"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","names":[]}
|
package/dist/styles.css
CHANGED
|
@@ -241,6 +241,16 @@
|
|
|
241
241
|
font-size: 0.8rem;
|
|
242
242
|
opacity: 0.7;
|
|
243
243
|
}
|
|
244
|
+
/* Taal-switcher in de editor-balk (4.21) — compact naast het pad. */
|
|
245
|
+
.xicms-ve__locale {
|
|
246
|
+
display: inline-flex;
|
|
247
|
+
align-items: center;
|
|
248
|
+
gap: 0.35rem;
|
|
249
|
+
font-size: 0.8rem;
|
|
250
|
+
}
|
|
251
|
+
.xicms-ve__locale > :last-child {
|
|
252
|
+
min-width: 7rem;
|
|
253
|
+
}
|
|
244
254
|
.xicms-ve__status {
|
|
245
255
|
font-size: 0.8rem;
|
|
246
256
|
opacity: 0.85;
|
|
@@ -2522,3 +2532,20 @@
|
|
|
2522
2532
|
.xicms-breadcrumbs [aria-current="page"] {
|
|
2523
2533
|
font-weight: 600;
|
|
2524
2534
|
}
|
|
2535
|
+
|
|
2536
|
+
/* ── Symbolen (5.0): kader + badge in de editor-preview ── */
|
|
2537
|
+
.xicms-symbol {
|
|
2538
|
+
outline: 1px dashed color-mix(in srgb, var(--xicms-primary, #4f46e5) 45%, transparent);
|
|
2539
|
+
outline-offset: 4px;
|
|
2540
|
+
border-radius: 6px;
|
|
2541
|
+
}
|
|
2542
|
+
.xicms-symbol__badge {
|
|
2543
|
+
display: inline-block;
|
|
2544
|
+
margin: 0.25rem 0;
|
|
2545
|
+
padding: 0.15rem 0.6rem;
|
|
2546
|
+
border-radius: 999px;
|
|
2547
|
+
font-size: 0.72rem;
|
|
2548
|
+
font-weight: 600;
|
|
2549
|
+
background: color-mix(in srgb, var(--xicms-primary, #4f46e5) 12%, transparent);
|
|
2550
|
+
color: var(--xicms-primary, #4f46e5);
|
|
2551
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiwebdesign/xicms-core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.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": {
|
|
@@ -75,14 +75,14 @@
|
|
|
75
75
|
"dependencies": {
|
|
76
76
|
"@astryxdesign/core": "^0.1.1",
|
|
77
77
|
"@astryxdesign/theme-neutral": "^0.1.1",
|
|
78
|
-
"@radix-ui/react-dialog": "^1.1.
|
|
79
|
-
"@radix-ui/react-dropdown-menu": "^2.1.
|
|
80
|
-
"@radix-ui/react-label": "^2.1.
|
|
81
|
-
"@radix-ui/react-select": "^2.
|
|
82
|
-
"@radix-ui/react-switch": "^1.
|
|
83
|
-
"@radix-ui/react-tabs": "^1.1.
|
|
84
|
-
"@radix-ui/react-toast": "^1.2.
|
|
85
|
-
"@radix-ui/react-tooltip": "^1.
|
|
78
|
+
"@radix-ui/react-dialog": "^1.1.19",
|
|
79
|
+
"@radix-ui/react-dropdown-menu": "^2.1.20",
|
|
80
|
+
"@radix-ui/react-label": "^2.1.11",
|
|
81
|
+
"@radix-ui/react-select": "^2.3.3",
|
|
82
|
+
"@radix-ui/react-switch": "^1.3.3",
|
|
83
|
+
"@radix-ui/react-tabs": "^1.1.17",
|
|
84
|
+
"@radix-ui/react-toast": "^1.2.19",
|
|
85
|
+
"@radix-ui/react-tooltip": "^1.2.12",
|
|
86
86
|
"@tiptap/extension-image": "^2.11.0",
|
|
87
87
|
"@tiptap/extension-link": "^2.11.0",
|
|
88
88
|
"@tiptap/pm": "^2.11.0",
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { type CSSProperties } from "react";
|
|
2
|
-
interface EditableImageProps {
|
|
3
|
-
/** Stabiel content-id, bv. "home.hero.image". */
|
|
4
|
-
id: string;
|
|
5
|
-
/** Standaard-afbeelding (URL) zolang er geen upload is. */
|
|
6
|
-
src?: string;
|
|
7
|
-
alt?: string;
|
|
8
|
-
className?: string;
|
|
9
|
-
style?: CSSProperties;
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Bewerkbare afbeelding. In bewerk-modus kan een nieuwe afbeelding worden geüpload via dezelfde
|
|
13
|
-
* gedeelde pipeline als het beheer (`uploadImage`: automatische webp-compressie + media-registratie),
|
|
14
|
-
* zodat inline bewerken en de block-editor consistent zijn. De URL wordt als content-waarde opgeslagen.
|
|
15
|
-
*/
|
|
16
|
-
/** @deprecated Legacy inline (v1). Gebruik een image-block (inline vervangen via de mediakiezer). */
|
|
17
|
-
export declare function EditableImage({ id, src, alt, className, style }: EditableImageProps): import("react").JSX.Element;
|
|
18
|
-
export {};
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useRef, useState } from "react";
|
|
4
|
-
import { useContent } from "../content/ContentProvider";
|
|
5
|
-
import { useSiteConfig } from "../config";
|
|
6
|
-
import { uploadImage } from "../admin/lib/upload";
|
|
7
|
-
function EditableImage({ id, src, alt = "", className, style }) {
|
|
8
|
-
const { get, set, editMode, canEdit } = useContent();
|
|
9
|
-
const { siteId } = useSiteConfig();
|
|
10
|
-
const inputRef = useRef(null);
|
|
11
|
-
const [busy, setBusy] = useState(false);
|
|
12
|
-
const url = get(id, src ?? "");
|
|
13
|
-
async function handleFile(file) {
|
|
14
|
-
setBusy(true);
|
|
15
|
-
try {
|
|
16
|
-
const { url: uploaded } = await uploadImage(siteId, file);
|
|
17
|
-
await set(id, uploaded);
|
|
18
|
-
} catch (e) {
|
|
19
|
-
if (typeof window !== "undefined") window.alert(`Upload mislukt: ${e.message}`);
|
|
20
|
-
} finally {
|
|
21
|
-
setBusy(false);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
const img = url ? (
|
|
25
|
-
// eslint-disable-next-line @next/next/no-img-element
|
|
26
|
-
/* @__PURE__ */ jsx("img", { src: url, alt, className, style })
|
|
27
|
-
) : /* @__PURE__ */ jsx("div", { className, style: { background: "#e5e7eb", ...style }, "aria-label": alt });
|
|
28
|
-
if (editMode && canEdit) {
|
|
29
|
-
return /* @__PURE__ */ jsxs("div", { style: { position: "relative", display: "inline-block" }, className: "xicms-editable", children: [
|
|
30
|
-
img,
|
|
31
|
-
/* @__PURE__ */ jsx(
|
|
32
|
-
"button",
|
|
33
|
-
{
|
|
34
|
-
type: "button",
|
|
35
|
-
className: "xicms-btn",
|
|
36
|
-
style: { position: "absolute", bottom: 8, left: 8 },
|
|
37
|
-
disabled: busy,
|
|
38
|
-
onClick: () => inputRef.current?.click(),
|
|
39
|
-
children: busy ? "Uploaden\u2026" : "Afbeelding wijzigen"
|
|
40
|
-
}
|
|
41
|
-
),
|
|
42
|
-
/* @__PURE__ */ jsx(
|
|
43
|
-
"input",
|
|
44
|
-
{
|
|
45
|
-
ref: inputRef,
|
|
46
|
-
type: "file",
|
|
47
|
-
accept: "image/*",
|
|
48
|
-
hidden: true,
|
|
49
|
-
onChange: (e) => {
|
|
50
|
-
const f = e.target.files?.[0];
|
|
51
|
-
if (f) void handleFile(f);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
)
|
|
55
|
-
] });
|
|
56
|
-
}
|
|
57
|
-
return img;
|
|
58
|
-
}
|
|
59
|
-
export {
|
|
60
|
-
EditableImage
|
|
61
|
-
};
|
|
62
|
-
//# sourceMappingURL=EditableImage.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/EditableImage.tsx"],"sourcesContent":["\"use client\";\n\nimport { useRef, useState, type CSSProperties } from \"react\";\nimport { useContent } from \"../content/ContentProvider\";\nimport { useSiteConfig } from \"../config\";\nimport { uploadImage } from \"../admin/lib/upload\";\n\ninterface EditableImageProps {\n /** Stabiel content-id, bv. \"home.hero.image\". */\n id: string;\n /** Standaard-afbeelding (URL) zolang er geen upload is. */\n src?: string;\n alt?: string;\n className?: string;\n style?: CSSProperties;\n}\n\n/**\n * Bewerkbare afbeelding. In bewerk-modus kan een nieuwe afbeelding worden geüpload via dezelfde\n * gedeelde pipeline als het beheer (`uploadImage`: automatische webp-compressie + media-registratie),\n * zodat inline bewerken en de block-editor consistent zijn. De URL wordt als content-waarde opgeslagen.\n */\n/** @deprecated Legacy inline (v1). Gebruik een image-block (inline vervangen via de mediakiezer). */\nexport function EditableImage({ id, src, alt = \"\", className, style }: EditableImageProps) {\n const { get, set, editMode, canEdit } = useContent();\n const { siteId } = useSiteConfig();\n const inputRef = useRef<HTMLInputElement>(null);\n const [busy, setBusy] = useState(false);\n const url = get(id, src ?? \"\");\n\n async function handleFile(file: File) {\n setBusy(true);\n try {\n const { url: uploaded } = await uploadImage(siteId, file);\n await set(id, uploaded);\n } catch (e) {\n // eslint-disable-next-line no-alert\n if (typeof window !== \"undefined\") window.alert(`Upload mislukt: ${(e as Error).message}`);\n } finally {\n setBusy(false);\n }\n }\n\n const img = url ? (\n // eslint-disable-next-line @next/next/no-img-element\n <img src={url} alt={alt} className={className} style={style} />\n ) : (\n <div className={className} style={{ background: \"#e5e7eb\", ...style }} aria-label={alt} />\n );\n\n if (editMode && canEdit) {\n return (\n <div style={{ position: \"relative\", display: \"inline-block\" }} className=\"xicms-editable\">\n {img}\n <button\n type=\"button\"\n className=\"xicms-btn\"\n style={{ position: \"absolute\", bottom: 8, left: 8 }}\n disabled={busy}\n onClick={() => inputRef.current?.click()}\n >\n {busy ? \"Uploaden…\" : \"Afbeelding wijzigen\"}\n </button>\n <input\n ref={inputRef}\n type=\"file\"\n accept=\"image/*\"\n hidden\n onChange={(e) => {\n const f = e.target.files?.[0];\n if (f) void handleFile(f);\n }}\n />\n </div>\n );\n }\n\n return img;\n}\n"],"mappings":";AA6CI,cAOE,YAPF;AA3CJ,SAAS,QAAQ,gBAAoC;AACrD,SAAS,kBAAkB;AAC3B,SAAS,qBAAqB;AAC9B,SAAS,mBAAmB;AAkBrB,SAAS,cAAc,EAAE,IAAI,KAAK,MAAM,IAAI,WAAW,MAAM,GAAuB;AACzF,QAAM,EAAE,KAAK,KAAK,UAAU,QAAQ,IAAI,WAAW;AACnD,QAAM,EAAE,OAAO,IAAI,cAAc;AACjC,QAAM,WAAW,OAAyB,IAAI;AAC9C,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,MAAM,IAAI,IAAI,OAAO,EAAE;AAE7B,iBAAe,WAAW,MAAY;AACpC,YAAQ,IAAI;AACZ,QAAI;AACF,YAAM,EAAE,KAAK,SAAS,IAAI,MAAM,YAAY,QAAQ,IAAI;AACxD,YAAM,IAAI,IAAI,QAAQ;AAAA,IACxB,SAAS,GAAG;AAEV,UAAI,OAAO,WAAW,YAAa,QAAO,MAAM,mBAAoB,EAAY,OAAO,EAAE;AAAA,IAC3F,UAAE;AACA,cAAQ,KAAK;AAAA,IACf;AAAA,EACF;AAEA,QAAM,MAAM;AAAA;AAAA,IAEV,oBAAC,SAAI,KAAK,KAAK,KAAU,WAAsB,OAAc;AAAA,MAE7D,oBAAC,SAAI,WAAsB,OAAO,EAAE,YAAY,WAAW,GAAG,MAAM,GAAG,cAAY,KAAK;AAG1F,MAAI,YAAY,SAAS;AACvB,WACE,qBAAC,SAAI,OAAO,EAAE,UAAU,YAAY,SAAS,eAAe,GAAG,WAAU,kBACtE;AAAA;AAAA,MACD;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,WAAU;AAAA,UACV,OAAO,EAAE,UAAU,YAAY,QAAQ,GAAG,MAAM,EAAE;AAAA,UAClD,UAAU;AAAA,UACV,SAAS,MAAM,SAAS,SAAS,MAAM;AAAA,UAEtC,iBAAO,mBAAc;AAAA;AAAA,MACxB;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,KAAK;AAAA,UACL,MAAK;AAAA,UACL,QAAO;AAAA,UACP,QAAM;AAAA,UACN,UAAU,CAAC,MAAM;AACf,kBAAM,IAAI,EAAE,OAAO,QAAQ,CAAC;AAC5B,gBAAI,EAAG,MAAK,WAAW,CAAC;AAAA,UAC1B;AAAA;AAAA,MACF;AAAA,OACF;AAAA,EAEJ;AAEA,SAAO;AACT;","names":[]}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { type CSSProperties } from "react";
|
|
2
|
-
interface EditableRichTextProps {
|
|
3
|
-
id: string;
|
|
4
|
-
className?: string;
|
|
5
|
-
style?: CSSProperties;
|
|
6
|
-
children?: string;
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* Meerregelig bewerkbaar tekstblok (platte tekst, regelafbrekingen blijven behouden).
|
|
10
|
-
* In bewerk-modus een textarea; daarbuiten paragrafen. Bewust géén HTML, om XSS te vermijden.
|
|
11
|
-
*/
|
|
12
|
-
/** @deprecated Legacy inline (v1). Gebruik een richText-block + de visuele editor. */
|
|
13
|
-
export declare function EditableRichText({ id, className, style, children }: EditableRichTextProps): import("react").JSX.Element;
|
|
14
|
-
export {};
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx } from "react/jsx-runtime";
|
|
3
|
-
import { useContent } from "../content/ContentProvider";
|
|
4
|
-
function EditableRichText({ id, className, style, children }) {
|
|
5
|
-
const { get, set, editMode, canEdit } = useContent();
|
|
6
|
-
const value = get(id, children ?? "");
|
|
7
|
-
if (editMode && canEdit) {
|
|
8
|
-
return /* @__PURE__ */ jsx(
|
|
9
|
-
"textarea",
|
|
10
|
-
{
|
|
11
|
-
className: `${className ?? ""} xicms-editable`.trim(),
|
|
12
|
-
style: { width: "100%", minHeight: 120, ...style },
|
|
13
|
-
defaultValue: value,
|
|
14
|
-
"data-xicms-id": id,
|
|
15
|
-
onBlur: (e) => {
|
|
16
|
-
if (e.currentTarget.value !== value) void set(id, e.currentTarget.value);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
);
|
|
20
|
-
}
|
|
21
|
-
return /* @__PURE__ */ jsx("div", { className, style, children: value.split(/\n{2,}/).map((para, i) => /* @__PURE__ */ jsx("p", { style: { whiteSpace: "pre-wrap" }, children: para }, i)) });
|
|
22
|
-
}
|
|
23
|
-
export {
|
|
24
|
-
EditableRichText
|
|
25
|
-
};
|
|
26
|
-
//# sourceMappingURL=EditableRichText.js.map
|