@universalcms/connect 0.1.0 → 0.13.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.
Files changed (130) hide show
  1. package/README.md +699 -37
  2. package/dist/angular-runtime.d.mts +4 -0
  3. package/dist/angular-runtime.mjs +7 -0
  4. package/dist/angular-setup.mjs +124 -0
  5. package/dist/astro-runtime.d.mts +6 -0
  6. package/dist/astro-runtime.mjs +255 -0
  7. package/dist/astro-setup.mjs +215 -0
  8. package/dist/astro-source.mjs +451 -0
  9. package/dist/astro-website.d.mts +5 -0
  10. package/dist/astro-website.mjs +42 -0
  11. package/dist/cli.mjs +418 -15
  12. package/dist/connector-runtime.mjs +130 -0
  13. package/dist/content-next-setup.mjs +207 -0
  14. package/dist/content-registry-validation.mjs +40 -0
  15. package/dist/content-registry.mjs +44 -0
  16. package/dist/content-setup.mjs +13 -0
  17. package/dist/content-storefront-setup.mjs +155 -0
  18. package/dist/content-vite-setup.mjs +107 -0
  19. package/dist/core.mjs +32 -51
  20. package/dist/framework-ownership.mjs +68 -0
  21. package/dist/framework.mjs +54 -0
  22. package/dist/guided.mjs +514 -0
  23. package/dist/native-adapters.mjs +31 -0
  24. package/dist/native-chrome-model.d.mts +3 -0
  25. package/dist/native-chrome-model.mjs +14 -0
  26. package/dist/native-chrome-review.mjs +49 -0
  27. package/dist/native-chrome-source.mjs +69 -0
  28. package/dist/native-route-review.mjs +152 -0
  29. package/dist/native-routes.mjs +27 -0
  30. package/dist/native-website-model.d.mts +18 -0
  31. package/dist/native-website-model.mjs +67 -0
  32. package/dist/native-website-server.d.mts +1 -0
  33. package/dist/native-website-server.mjs +35222 -0
  34. package/dist/native-website-server.mjs.LEGAL.txt +56 -0
  35. package/dist/native-website.d.mts +3 -0
  36. package/dist/native-website.mjs +120 -0
  37. package/dist/native-website.mjs.LEGAL.txt +56 -0
  38. package/dist/next-publication.d.mts +12 -0
  39. package/dist/next-publication.mjs +107 -0
  40. package/dist/next-settings-layout.mjs +233 -0
  41. package/dist/next-source.d.mts +8 -0
  42. package/dist/next-source.mjs +93 -0
  43. package/dist/page-discovery.mjs +236 -0
  44. package/dist/page-mapping-review.mjs +160 -0
  45. package/dist/preflight-recovery.mjs +130 -0
  46. package/dist/preflight.mjs +138 -0
  47. package/dist/project-files.mjs +56 -0
  48. package/dist/public-bindings.d.mts +3 -0
  49. package/dist/public-bindings.mjs +28 -0
  50. package/dist/publication-browser.d.mts +5 -0
  51. package/dist/publication-browser.mjs +258 -0
  52. package/dist/publication-check.mjs +141 -0
  53. package/dist/publication-document.d.mts +2 -0
  54. package/dist/publication-document.mjs +59 -0
  55. package/dist/publication-next-setup.mjs +134 -0
  56. package/dist/publication-setup.mjs +138 -0
  57. package/dist/publication-verification.mjs +85 -0
  58. package/dist/publication.css +6 -0
  59. package/dist/publication.d.mts +31 -0
  60. package/dist/publication.mjs +320 -0
  61. package/dist/react-publication.d.mts +22 -0
  62. package/dist/react-publication.mjs +301 -0
  63. package/dist/react-website.d.mts +15 -0
  64. package/dist/react-website.mjs +735 -0
  65. package/dist/reviewed-commerce-ownership.mjs +26 -0
  66. package/dist/reviewed-commerce-settings-setup.mjs +243 -0
  67. package/dist/reviewed-native-ownership.mjs +118 -0
  68. package/dist/reviewed-storefront-source.mjs +227 -0
  69. package/dist/runtime-dependencies.mjs +59 -0
  70. package/dist/settings-only-setup.mjs +100 -0
  71. package/dist/settings-only.d.mts +4 -0
  72. package/dist/settings-only.mjs +49 -0
  73. package/dist/setup-environment.mjs +70 -0
  74. package/dist/source-bindings.mjs +77 -0
  75. package/dist/source-check.mjs +149 -0
  76. package/dist/source-collect.mjs +80 -0
  77. package/dist/source-config.mjs +20 -0
  78. package/dist/source-extract.mjs +514 -0
  79. package/dist/source-finalize.mjs +52 -0
  80. package/dist/source-routes.mjs +65 -0
  81. package/dist/source-setup.mjs +490 -0
  82. package/dist/staged-setup.mjs +287 -0
  83. package/dist/static-runtime.mjs +67 -0
  84. package/dist/static-setup.mjs +136 -0
  85. package/dist/svelte-runtime.d.mts +27 -0
  86. package/dist/svelte-runtime.mjs +40 -0
  87. package/dist/svelte-setup.mjs +135 -0
  88. package/dist/svelte-template.mjs +48 -0
  89. package/dist/templates.mjs +167 -3
  90. package/dist/vendor/astro-compiler/LICENSE +53 -0
  91. package/dist/vendor/astro-compiler/astro.wasm +0 -0
  92. package/dist/vendor/astro-compiler/chunk-W5DTLHV4.js +1 -0
  93. package/dist/vendor/astro-compiler/node/index.js +1 -0
  94. package/dist/vendor/project-analysis.mjs +58 -4
  95. package/dist/vendor/react-LICENSE.txt +21 -0
  96. package/dist/vendor/react-document.mjs +124 -0
  97. package/dist/vendor/react-dom-LICENSE.txt +21 -0
  98. package/dist/vendor/react-form.mjs +63 -0
  99. package/dist/vendor/react-safety.mjs +47 -0
  100. package/dist/vendor/source-manifest-provider.mjs +104 -0
  101. package/dist/vendor/typescript-LICENSE.txt +55 -0
  102. package/dist/vendor/typescript.cjs +200276 -0
  103. package/dist/vite-publication-server.d.mts +3 -0
  104. package/dist/vite-publication-server.mjs +82 -0
  105. package/dist/vite-publication.d.mts +15 -0
  106. package/dist/vite-publication.mjs +241 -0
  107. package/dist/vite-source-discovery.mjs +315 -0
  108. package/dist/vite-source.d.mts +30 -0
  109. package/dist/vite-source.mjs +141 -0
  110. package/dist/vue-runtime.d.mts +10 -0
  111. package/dist/vue-runtime.mjs +50 -0
  112. package/dist/vue-server.d.mts +2 -0
  113. package/dist/vue-server.mjs +58 -0
  114. package/dist/vue-setup.mjs +127 -0
  115. package/dist/vue-source.mjs +168 -0
  116. package/dist/website-check.mjs +41 -0
  117. package/dist/website-http.mjs +149 -0
  118. package/dist/website-model.d.mts +20 -0
  119. package/dist/website-model.mjs +155 -0
  120. package/dist/website-next.d.mts +2 -0
  121. package/dist/website-next.mjs +29 -0
  122. package/dist/website-node.d.mts +3 -0
  123. package/dist/website-node.mjs +50 -0
  124. package/dist/website-runtime.d.mts +13 -0
  125. package/dist/website-runtime.mjs +404 -0
  126. package/dist/website-schema.mjs +22175 -0
  127. package/dist/website-setup.mjs +256 -0
  128. package/dist/website.css +49 -0
  129. package/dist/wizard.mjs +237 -38
  130. package/package.json +44 -7
@@ -0,0 +1,735 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import React, { createContext, useContext, useEffect, useMemo, useRef, useState, useCallback } from 'react';
4
+ import { PublishedDocumentProvider, PublicationMetadataOwner, CmsPublishedDocument, validateEntry } from './react-publication.mjs';
5
+ import { emptyWebsiteSettings, parseWebsiteSettings, sectionData, record, text, publicHref, navigationLinks, languageLinks, experienceCopy, consentPolicy, parseConsent, createConsent, scriptDescriptors, pagePresentation, websiteJsonLd, readWebsiteJson } from './website-model.mjs';
6
+ const Context = createContext(null);
7
+ // Only CmsWebsitePage declares a managed page failure. A settings-only provider
8
+ // may legitimately receive a 404 for a native route and must keep its own title.
9
+ const PageAvailabilityContext = createContext(undefined);
10
+ const initial = (value) => { try {
11
+ return parseWebsiteSettings(value);
12
+ }
13
+ catch {
14
+ return emptyWebsiteSettings();
15
+ } };
16
+ /** Only public same-origin projections are read here. Server credentials never
17
+ * enter this provider, browser storage or request headers. */
18
+ export function WebsiteProvider({ children, initialSettings, entry, archive, scope }) {
19
+ const parent = useContext(Context), pageAvailability = useContext(PageAvailabilityContext), resolvedScope = scope ?? parent?.scope ?? 'website';
20
+ const safeArchive = useMemo(() => { try {
21
+ return archive === undefined ? undefined : archiveMetadata(archive);
22
+ }
23
+ catch {
24
+ return undefined;
25
+ } }, [archive]);
26
+ // A native layout and its page share one settings/consent owner. Installing a
27
+ // second effects owner would execute scripts twice and race over SEO tags.
28
+ if (parent?.scope === resolvedScope)
29
+ return _jsx(PublicationMetadataOwner, { children: _jsx(InheritedWebsiteProvider, { parent: parent, entry: entry, archive: safeArchive, pageAvailability: pageAvailability, children: children }) });
30
+ return _jsx(PublicationMetadataOwner, { children: _jsx(ScopedWebsiteProvider, { scope: resolvedScope, initialSettings: initialSettings, entry: entry, archive: safeArchive, pageAvailability: pageAvailability, children: children }, resolvedScope) });
31
+ }
32
+ function InheritedWebsiteProvider({ parent, entry, archive, pageAvailability, children }) {
33
+ const adoptPage = parent.adoptPage;
34
+ useEffect(() => adoptPage(entry, archive, pageAvailability === 'unavailable'), [adoptPage, entry, archive, pageAvailability]);
35
+ const current = entry && parent.entry?.id === entry.id && parent.entry.locale === entry.locale && parent.entry.version >= entry.version ? parent.entry : entry;
36
+ return _jsx(Context.Provider, { value: { ...parent, entry: current, archive: archive && parent.archive?.path === archive.path ? parent.archive : archive, related: current?.id === parent.entry?.id ? parent.related : [] }, children: children });
37
+ }
38
+ function ScopedWebsiteProvider({ children, initialSettings, entry, archive, pageAvailability, scope }) {
39
+ const [settings, setSettings] = useState(() => initial(initialSettings));
40
+ const [settingsChecked, setSettingsChecked] = useState(false);
41
+ const [consent, setConsent] = useState(null);
42
+ const [preferencesOpen, setPreferencesOpen] = useState(false);
43
+ const [loadedEntry, setLoadedEntry] = useState(entry), [loadedArchive, setLoadedArchive] = useState(archive), [related, setRelated] = useState([]);
44
+ const [pageUnavailable, setPageUnavailable] = useState(pageAvailability === 'unavailable');
45
+ const adoptedPage = useRef(undefined);
46
+ const adoptPage = useCallback((next, nextArchive, unavailable = false) => {
47
+ const owner = Symbol('page');
48
+ adoptedPage.current = owner;
49
+ setLoadedEntry(next);
50
+ setLoadedArchive(next ? undefined : nextArchive);
51
+ setPageUnavailable(unavailable);
52
+ setRelated([]);
53
+ window.dispatchEvent(new Event('universalcms:page-change'));
54
+ return () => { if (adoptedPage.current === owner) {
55
+ adoptedPage.current = undefined;
56
+ setLoadedEntry(undefined);
57
+ setLoadedArchive(undefined);
58
+ setPageUnavailable(false);
59
+ setRelated([]);
60
+ } };
61
+ }, []);
62
+ useEffect(() => { if (pageAvailability !== undefined)
63
+ setPageUnavailable(pageAvailability === 'unavailable'); }, [pageAvailability]);
64
+ const generation = useRef(0), storageFailed = useRef(false);
65
+ const policy = consentPolicy(settings), policyKey = policy ? `${policy.version}:${policy.maxAgeDays}` : '';
66
+ const storageKey = `universalcms:consent:${scope.slice(0, 200)}`;
67
+ const denialMarker = `\n[universalcms-consent-denied:${encodeURIComponent(scope.slice(0, 200))}]`;
68
+ useEffect(() => {
69
+ const documentRoot = document.documentElement;
70
+ let disposed = false, controller;
71
+ const active = () => !disposed && document.documentElement === documentRoot;
72
+ const refresh = () => {
73
+ if (!active())
74
+ return;
75
+ const current = ++generation.current;
76
+ controller?.abort();
77
+ const request = new AbortController();
78
+ controller = request;
79
+ const timer = window.setTimeout(() => request.abort(), 10000);
80
+ void (async () => {
81
+ try {
82
+ const response = await fetch('/api/universalcms/settings', { headers: { Accept: 'application/json' }, credentials: 'omit', cache: 'no-store', redirect: 'error', referrerPolicy: 'no-referrer', signal: request.signal });
83
+ const next = parseWebsiteSettings(await readWebsiteJson(response, request.signal));
84
+ if (active() && current === generation.current) {
85
+ setSettings(next);
86
+ setSettingsChecked(true);
87
+ }
88
+ }
89
+ catch {
90
+ if (active() && current === generation.current) {
91
+ setSettings(emptyWebsiteSettings());
92
+ setSettingsChecked(false);
93
+ setConsent(null);
94
+ }
95
+ }
96
+ finally {
97
+ window.clearTimeout(timer);
98
+ }
99
+ })();
100
+ };
101
+ const visible = () => { if (document.visibilityState === 'visible')
102
+ refresh(); };
103
+ refresh();
104
+ const interval = window.setInterval(refresh, 15000);
105
+ window.addEventListener('focus', refresh);
106
+ document.addEventListener('visibilitychange', visible);
107
+ window.addEventListener('universalcms:page-change', refresh);
108
+ return () => { disposed = true; generation.current++; controller?.abort(); window.clearInterval(interval); window.removeEventListener('focus', refresh); document.removeEventListener('visibilitychange', visible); window.removeEventListener('universalcms:page-change', refresh); };
109
+ }, []);
110
+ useEffect(() => {
111
+ if (entry || archive) {
112
+ setLoadedEntry(entry);
113
+ setLoadedArchive(entry ? undefined : archive);
114
+ }
115
+ const documentRoot = document.documentElement;
116
+ let disposed = false, serial = 0, controller, observedPath = window.location.pathname;
117
+ const active = () => !disposed && document.documentElement === documentRoot;
118
+ const refresh = () => {
119
+ if (!active())
120
+ return;
121
+ const current = ++serial;
122
+ controller?.abort();
123
+ const request = new AbortController();
124
+ controller = request;
125
+ const path = window.location.pathname, timer = window.setTimeout(() => request.abort(), 10000);
126
+ void (async () => {
127
+ try {
128
+ const response = await fetch(`/api/universalcms/page?path=${encodeURIComponent(path)}`, { headers: { Accept: 'application/json' }, credentials: 'omit', cache: 'no-store', redirect: 'error', referrerPolicy: 'no-referrer', signal: request.signal });
129
+ const next = validatedPage(await readWebsiteJson(response, request.signal));
130
+ if (next.path && next.path !== path)
131
+ throw Error();
132
+ if (active() && current === serial && window.location.pathname === path) {
133
+ setLoadedEntry(next.state === 'published' ? next.entry : undefined);
134
+ setLoadedArchive(next.state === 'archive' ? archiveMetadata(next) : undefined);
135
+ setRelated(Array.isArray(next.related) ? next.related.slice(0, 6) : []);
136
+ }
137
+ }
138
+ catch {
139
+ if (active() && current === serial && window.location.pathname === path) {
140
+ setLoadedEntry(undefined);
141
+ setLoadedArchive(undefined);
142
+ setRelated([]);
143
+ }
144
+ }
145
+ finally {
146
+ window.clearTimeout(timer);
147
+ }
148
+ })();
149
+ };
150
+ const changed = () => { observedPath = window.location.pathname; refresh(); };
151
+ // Native client routers may preserve their layout on navigation. Refresh only
152
+ // when the URL changes; this interval never polls the network at this rate.
153
+ const locationTimer = window.setInterval(() => { if (active() && observedPath !== window.location.pathname) {
154
+ setLoadedEntry(undefined);
155
+ setLoadedArchive(undefined);
156
+ setRelated([]);
157
+ window.dispatchEvent(new Event('universalcms:page-change'));
158
+ } }, 500);
159
+ refresh();
160
+ const interval = window.setInterval(refresh, 15000);
161
+ window.addEventListener('focus', refresh);
162
+ window.addEventListener('universalcms:page-change', changed);
163
+ return () => { disposed = true; serial++; controller?.abort(); window.clearInterval(interval); window.clearInterval(locationTimer); window.removeEventListener('focus', refresh); window.removeEventListener('universalcms:page-change', changed); };
164
+ }, [entry, archive]);
165
+ useEffect(() => {
166
+ const documentRoot = document.documentElement;
167
+ const restore = () => {
168
+ if (document.documentElement !== documentRoot)
169
+ return;
170
+ if (storageFailed.current || !policy || window.name.includes(denialMarker)) {
171
+ setConsent(null);
172
+ return;
173
+ }
174
+ try {
175
+ setConsent(parseConsent(window.localStorage.getItem(storageKey), policy));
176
+ }
177
+ catch {
178
+ storageFailed.current = true;
179
+ setConsent(null);
180
+ }
181
+ };
182
+ const storage = (event) => { if (!event.key || event.key === storageKey)
183
+ restore(); };
184
+ restore();
185
+ const timer = window.setInterval(restore, 1000);
186
+ window.addEventListener('storage', storage);
187
+ window.addEventListener('focus', restore);
188
+ return () => { window.clearInterval(timer); window.removeEventListener('storage', storage); window.removeEventListener('focus', restore); };
189
+ }, [storageKey, policyKey, denialMarker]);
190
+ const saveConsent = (choices) => {
191
+ const next = createConsent(policy, choices);
192
+ if (!next)
193
+ return;
194
+ // Failed persistence cannot grant optional execution, including next load.
195
+ try {
196
+ const serialized = JSON.stringify(next);
197
+ window.localStorage.setItem(storageKey, serialized);
198
+ if (window.localStorage.getItem(storageKey) !== serialized)
199
+ throw Error();
200
+ window.name = window.name.replaceAll(denialMarker, '');
201
+ storageFailed.current = false;
202
+ setConsent(next);
203
+ setPreferencesOpen(false);
204
+ }
205
+ catch {
206
+ // A denial-only marker survives a same-tab navigation even when browser
207
+ // storage refuses a write. Preserve unrelated window.name text; this is
208
+ // never a source of permission and contains no credential or visitor ID.
209
+ try {
210
+ window.localStorage.removeItem(storageKey);
211
+ }
212
+ catch { /* denied storage */ }
213
+ if (!window.name.includes(denialMarker))
214
+ window.name += denialMarker;
215
+ storageFailed.current = true;
216
+ setConsent(null);
217
+ }
218
+ };
219
+ const value = { settings, settingsChecked, entry: loadedEntry, archive: loadedArchive, pageUnavailable, related, scope, consent, saveConsent, preferencesOpen, setPreferencesOpen, adoptPage };
220
+ return _jsxs(Context.Provider, { value: value, children: [_jsx(WebsiteEffects, {}), children] });
221
+ }
222
+ export function useWebsiteSettings() {
223
+ const context = useContext(Context);
224
+ if (!context)
225
+ throw Error('Website settings require WebsiteProvider');
226
+ return context;
227
+ }
228
+ function WebsiteEffects() {
229
+ const { settings, settingsChecked, entry, archive, pageUnavailable, consent } = useWebsiteSettings();
230
+ const documentRoot = useRef(undefined), archiveManaged = useRef(false);
231
+ const branding = sectionData(settings, 'branding'), seo = sectionData(settings, 'seo'), installed = useRef(new Map()), cleanupTimer = useRef(undefined);
232
+ // Validate the browser's current projection before executing any script. A
233
+ // failed settings read can then withdraw execution with one reload, without
234
+ // replaying the server snapshot and reloading forever while offline.
235
+ const scripts = JSON.stringify(settingsChecked ? scriptDescriptors(settings, consent, typeof navigator !== 'undefined' && (navigator.doNotTrack === '1' || navigator.globalPrivacyControl === true)) : []);
236
+ useEffect(() => {
237
+ documentRoot.current ??= document.documentElement;
238
+ // The publication guard can replace the document before an aborted fetch
239
+ // settles. That departing React tree must not reload or modify the new page.
240
+ if (document.documentElement !== documentRoot.current)
241
+ return;
242
+ const descriptors = JSON.parse(scripts), desired = new Map(descriptors.map(item => [item.src, JSON.stringify(item)]));
243
+ const removed = [...installed.current].filter(([src, node]) => desired.get(src) !== node.dataset.universalcmsDescriptor);
244
+ for (const [src, node] of removed) {
245
+ node.remove();
246
+ installed.current.delete(src);
247
+ }
248
+ // Executed JavaScript cannot be recalled by removing its DOM element. The
249
+ // persisted denial/current policy is checked before anything runs on reload.
250
+ if (removed.length) {
251
+ window.location.reload();
252
+ return;
253
+ }
254
+ for (const descriptor of descriptors) {
255
+ if (installed.current.has(descriptor.src))
256
+ continue;
257
+ prepareAnalytics(descriptor);
258
+ const element = document.createElement('script');
259
+ element.src = descriptor.src;
260
+ element.async = true;
261
+ element.referrerPolicy = 'no-referrer';
262
+ element.dataset.universalcmsSetting = descriptor.category;
263
+ element.dataset.universalcmsDescriptor = JSON.stringify(descriptor);
264
+ for (const [name, value] of Object.entries(descriptor.attributes))
265
+ if (name === 'data-domain')
266
+ element.setAttribute(name, value);
267
+ (descriptor.placement === 'body' ? document.body : document.head).append(element);
268
+ installed.current.set(descriptor.src, element);
269
+ }
270
+ }, [scripts]);
271
+ useEffect(() => {
272
+ // React's development effect replay must not execute external code twice.
273
+ if (cleanupTimer.current !== undefined)
274
+ window.clearTimeout(cleanupTimer.current);
275
+ return () => {
276
+ cleanupTimer.current = window.setTimeout(() => {
277
+ for (const node of installed.current.values())
278
+ node.remove();
279
+ installed.current.clear();
280
+ }, 0);
281
+ };
282
+ }, []);
283
+ useEffect(() => {
284
+ if (document.documentElement !== documentRoot.current)
285
+ return;
286
+ const favicon = publicHref(branding?.faviconUrl, { relative: false });
287
+ document.querySelectorAll('[data-universalcms-setting="branding"]').forEach(node => node.remove());
288
+ if (!favicon)
289
+ return;
290
+ const element = document.createElement('link');
291
+ element.rel = 'icon';
292
+ element.href = favicon;
293
+ element.dataset.universalcmsSetting = 'branding';
294
+ document.head.append(element);
295
+ return () => element.remove();
296
+ }, [branding?.faviconUrl]);
297
+ useEffect(() => {
298
+ if (document.documentElement !== documentRoot.current)
299
+ return;
300
+ // Only delivered translations change language; settings alone never imply a
301
+ // page was translated or invent a locale-prefixed URL.
302
+ if (!entry?.locale || !sectionData(settings, 'languages'))
303
+ return;
304
+ const before = document.documentElement.lang;
305
+ document.documentElement.lang = entry.locale;
306
+ return () => { if (document.documentElement === documentRoot.current && document.documentElement.lang === entry.locale)
307
+ document.documentElement.lang = before; };
308
+ }, [entry?.locale, settings.sections.languages]);
309
+ useEffect(() => {
310
+ if (document.documentElement !== documentRoot.current)
311
+ return;
312
+ // Next owns server metadata outside this React subtree. After a fresh access
313
+ // denial, request its fallback metadata instead of restoring saved CMS SEO
314
+ // as if it were native. The marker is emitted only by our SEO adapter; remove
315
+ // it first so repeated effects cannot trigger multiple reloads.
316
+ const serverSeo = document.head.querySelector('meta[name="universalcms-seo"][content="managed"]');
317
+ const seoRemoved = ['denied', 'missing'].includes(settings.sections.seo.state);
318
+ if (!seo && seoRemoved && settingsChecked && serverSeo) {
319
+ serverSeo.remove();
320
+ window.location.reload();
321
+ return;
322
+ }
323
+ // Archives have presentation metadata, not a fabricated published entry.
324
+ // Only a currently verified archive with current taxonomy access can supply it.
325
+ const currentArchive = !pageUnavailable && archive?.path === window.location.pathname && sectionData(settings, 'taxonomies') ? archive : undefined;
326
+ if (currentArchive)
327
+ archiveManaged.current = true;
328
+ document.querySelectorAll('[data-universalcms-setting="archive-fallback"]').forEach(node => node.remove());
329
+ if (pageUnavailable || (!entry && !currentArchive) || !seo) {
330
+ const loadingTitle = document.title;
331
+ document.querySelectorAll('[data-universalcms-setting="seo"]').forEach(node => node.remove());
332
+ if (entry && !pageUnavailable) {
333
+ archiveManaged.current = false;
334
+ document.title = entry.document.title;
335
+ return;
336
+ }
337
+ // Keep a plain title while the initial public page read settles, even if
338
+ // the server's managed <title> was removed with its unverified SEO tags.
339
+ if (!pageUnavailable && !archiveManaged.current && !archive) {
340
+ if (loadingTitle)
341
+ document.title = loadingTitle;
342
+ return;
343
+ }
344
+ document.title = currentArchive?.title || 'Page unavailable';
345
+ const nodes = [['description', currentArchive?.description || ''], ['robots', 'noindex, follow']].map(([name, content]) => {
346
+ const node = document.createElement('meta');
347
+ node.name = name;
348
+ node.content = content;
349
+ node.dataset.universalcmsSetting = 'archive-fallback';
350
+ document.head.append(node);
351
+ return node;
352
+ });
353
+ return () => { for (const node of nodes)
354
+ node.remove(); };
355
+ }
356
+ if (entry)
357
+ archiveManaged.current = false;
358
+ const subject = entry ? { title: entry.document.title, description: entry.document.excerpt, path: entry.path } : { title: currentArchive.title, description: currentArchive.description, path: currentArchive.path };
359
+ const priorTitle = document.title, raw = record(entry?.seo), base = text(raw.title, 120) || subject.title;
360
+ const title = text(seo.titleTemplate, 500).replace(/%s|%title%|\{title\}/g, () => base) || base;
361
+ const description = text(raw.description, 320) || subject.description || text(seo.description, 500);
362
+ const robots = `${seo.indexable === true && record(raw.robots).index !== false ? 'index' : 'noindex'}, ${seo.followLinks !== false && record(raw.robots).follow !== false ? 'follow' : 'nofollow'}`;
363
+ let canonical = publicHref(raw.canonical, { relative: false });
364
+ if (!canonical && publicHref(subject.path)?.startsWith('/')) {
365
+ const base = publicHref(seo.canonicalBase, { relative: false }) || window.location.origin;
366
+ const url = new URL(base);
367
+ url.pathname = `${url.pathname.replace(/\/+$/, '')}${subject.path}`;
368
+ url.search = '';
369
+ url.hash = '';
370
+ canonical = url.href;
371
+ }
372
+ const image = publicHref(record(raw.openGraph).imageUrl || record(raw.featuredImage).url || seo.socialImageUrl, { relative: false });
373
+ const saved = [], created = [];
374
+ const meta = (name, content, property = false) => {
375
+ const attribute = property ? 'property' : 'name';
376
+ let node = document.head.querySelector(`meta[${attribute}="${name}"]`);
377
+ if (node && !node.hasAttribute('data-universalcms-setting'))
378
+ saved.push({ node, content: node.getAttribute('content') });
379
+ else {
380
+ node?.remove();
381
+ node = document.createElement('meta');
382
+ node.setAttribute(attribute, name);
383
+ node.dataset.universalcmsSetting = 'seo';
384
+ document.head.append(node);
385
+ created.push(node);
386
+ }
387
+ node.setAttribute('content', content);
388
+ };
389
+ document.title = title;
390
+ meta('description', description);
391
+ meta('robots', robots);
392
+ meta('og:title', text(record(raw.openGraph).title, 120) || title, true);
393
+ meta('og:description', text(record(raw.openGraph).description, 320) || description, true);
394
+ if (image)
395
+ meta('og:image', image, true);
396
+ if (canonical)
397
+ meta('og:url', canonical, true);
398
+ meta('og:type', ['website', 'article', 'product', 'profile'].includes(record(raw.openGraph).type) ? raw.openGraph.type : entry?.kind === 'post' ? 'article' : 'website', true);
399
+ meta('twitter:card', raw.twitterCard === 'summary' ? 'summary' : 'summary_large_image');
400
+ if (/^@?[A-Za-z0-9_]{1,15}$/.test(seo.twitterHandle ?? ''))
401
+ meta('twitter:site', seo.twitterHandle);
402
+ document.head.querySelectorAll('script[type="application/ld+json"][data-universalcms-setting="seo"],link[rel="alternate"][data-universalcms-setting="seo"]').forEach(node => node.remove());
403
+ const jsonLd = entry ? websiteJsonLd(settings, entry, { title, description, canonical, image }) : null;
404
+ if (jsonLd) {
405
+ const node = document.createElement('script');
406
+ node.type = 'application/ld+json';
407
+ node.dataset.universalcmsSetting = 'seo';
408
+ node.textContent = JSON.stringify(jsonLd).replace(/</g, '\\u003c');
409
+ document.head.append(node);
410
+ created.push(node);
411
+ }
412
+ for (const alternate of entry ? languageLinks(settings, entry) : []) {
413
+ if (alternate.current)
414
+ continue;
415
+ const node = document.createElement('link');
416
+ node.rel = 'alternate';
417
+ node.hreflang = alternate.locale;
418
+ node.href = new URL(alternate.path, seo.canonicalBase || window.location.origin).href;
419
+ node.dataset.universalcmsSetting = 'seo';
420
+ document.head.append(node);
421
+ created.push(node);
422
+ }
423
+ if (canonical) {
424
+ const existing = document.head.querySelector('link[rel="canonical"]');
425
+ if (existing && !existing.hasAttribute('data-universalcms-setting'))
426
+ saved.push({ node: existing, content: existing.getAttribute('href') });
427
+ else
428
+ existing?.remove();
429
+ if (existing && !existing.hasAttribute('data-universalcms-setting'))
430
+ existing.href = canonical;
431
+ else {
432
+ const node = document.createElement('link');
433
+ node.rel = 'canonical';
434
+ node.href = canonical;
435
+ node.dataset.universalcmsSetting = 'seo';
436
+ document.head.append(node);
437
+ created.push(node);
438
+ }
439
+ }
440
+ return () => { if (document.documentElement !== documentRoot.current)
441
+ return; if (document.title === title)
442
+ document.title = priorTitle; for (const { node, content } of saved) {
443
+ const attribute = node.tagName === 'LINK' ? 'href' : 'content';
444
+ if (content === null)
445
+ node.removeAttribute(attribute);
446
+ else
447
+ node.setAttribute(attribute, content);
448
+ } for (const node of created)
449
+ node.remove(); };
450
+ }, [entry, archive, pageUnavailable, seo, settingsChecked, settings.sections.templates, settings.sections.authors, settings.sections.languages, settings.sections.taxonomies]);
451
+ return null;
452
+ }
453
+ function prepareAnalytics(descriptor) {
454
+ const target = window;
455
+ if (descriptor.provider === 'google') {
456
+ target.dataLayer ??= [];
457
+ target.gtag ??= function () { target.dataLayer.push(arguments); };
458
+ target.gtag('js', new Date());
459
+ target.gtag('config', descriptor.id, { anonymize_ip: true });
460
+ }
461
+ else if (descriptor.provider === 'clarity') {
462
+ target.clarity ??= Object.assign((...args) => { target.clarity.q.push(args); }, { q: [] });
463
+ }
464
+ else if (descriptor.provider === 'matomo') {
465
+ target._paq ??= [];
466
+ target._paq.push(['setTrackerUrl', descriptor.tracker], ['setSiteId', descriptor.id], ['trackPageView'], ['enableLinkTracking']);
467
+ }
468
+ }
469
+ function SafeLink({ url, children, ...props }) {
470
+ const href = publicHref(url, { mail: true });
471
+ if (!href)
472
+ return null;
473
+ return _jsx("a", { href: href, ...props, rel: /^https?:/i.test(href) ? 'noopener noreferrer' : undefined, children: children });
474
+ }
475
+ export function CmsBranding({ homePath = '/', fallback = null }) {
476
+ const { settings } = useWebsiteSettings(), branding = sectionData(settings, 'branding'), [failed, setFailed] = useState('');
477
+ if (!branding)
478
+ return fallback;
479
+ const logo = publicHref(branding.logoUrl, { relative: false }), description = text(branding.description, 2000), label = text(branding.logoAlt, 500);
480
+ if ((!logo || failed === logo) && !description)
481
+ return fallback;
482
+ return _jsxs("div", { className: "cms-website-branding", children: [logo && failed !== logo ? _jsxs(SafeLink, { url: homePath, children: [_jsx("img", { src: logo, alt: label || 'Website home', referrerPolicy: "no-referrer", onError: () => setFailed(logo) }), " "] }) : fallback, description && _jsx("p", { children: description })] });
483
+ }
484
+ export function CmsNavigation({ location = 'header', originalLinks = [], excludeUrls = [], onNavigate, className }) {
485
+ const { settings } = useWebsiteSettings(), links = navigationLinks(settings, location, originalLinks, excludeUrls), navigation = sectionData(settings, 'navigation');
486
+ const used = new Set([...excludeUrls, ...links.flatMap(item => [item.url, ...(item.children?.map(child => child.url) ?? [])])].map(value => publicHref(value)).filter(Boolean));
487
+ const groups = location === 'footer' && Array.isArray(navigation?.footerGroups) ? navigation.footerGroups.slice(0, 8).flatMap((value) => {
488
+ const group = record(value), items = (Array.isArray(group.links) ? group.links : []).slice(0, 20).flatMap((value) => {
489
+ const row = record(value), url = publicHref(row.url), label = text(row.label, 80);
490
+ if (!url || !label || used.has(url))
491
+ return [];
492
+ used.add(url);
493
+ return [{ url, label }];
494
+ });
495
+ return items.length ? [{ title: text(group.title, 80), links: items }] : [];
496
+ }) : [];
497
+ const footerText = location === 'footer' ? text(navigation?.footerText, 1000) : '';
498
+ if (!links.length && !groups.length && !footerText)
499
+ return null;
500
+ return _jsxs("div", { className: `cms-website-navigation cms-website-navigation-${location}${className ? ` ${className}` : ''}`, onClick: event => { if (event.target.closest('a'))
501
+ onNavigate?.(); }, children: [!!links.length && _jsx("nav", { "aria-label": `CMS ${location} links`, children: _jsx("ul", { children: links.map((item, index) => _jsx("li", { children: item.children?.length ? _jsxs("details", { children: [_jsx("summary", { children: item.label }), _jsxs("ul", { children: [item.url && _jsx("li", { children: _jsx(SafeLink, { url: item.url, children: item.label }) }), item.children.map((child, i) => _jsx("li", { children: _jsx(SafeLink, { url: child.url, children: child.label }) }, `${i}:${child.url}`))] })] }) : item.url && _jsx(SafeLink, { url: item.url, children: item.label }) }, `${index}:${item.label}`)) }) }), !!groups.length && _jsx("div", { className: "cms-website-footer-groups", children: groups.map((value, index) => { const group = record(value); return _jsxs("section", { children: [_jsx("h2", { children: text(group.title, 80) }), _jsx("ul", { children: (Array.isArray(group.links) ? group.links : []).slice(0, 20).map((value, i) => { const item = record(value); return _jsx("li", { children: _jsx(SafeLink, { url: text(item.url), children: text(item.label, 80) }) }, i); }) })] }, index); }) }), footerText && _jsx("p", { children: footerText })] });
502
+ }
503
+ export function CmsSocialLinks() {
504
+ const { settings } = useWebsiteSettings(), social = sectionData(settings, 'social');
505
+ if (!social || !Array.isArray(social.profiles) || !social.profiles.length)
506
+ return null;
507
+ const alignment = ['left', 'center', 'right'].includes(social.alignment) ? social.alignment : 'left';
508
+ return _jsx("nav", { className: `cms-website-social cms-website-align-${alignment}`, "aria-label": "Social profiles", children: social.profiles.slice(0, 20).map((value, index) => { const profile = record(value), href = publicHref(profile.url, { mail: String(profile.network).toLowerCase() === 'email' }); return href ? _jsxs(SafeLink, { url: href, children: [publicHref(profile.iconUrl, { relative: false }) && _jsx("img", { src: profile.iconUrl, alt: "", loading: "lazy", referrerPolicy: "no-referrer" }), text(profile.label, 80) || text(profile.network, 40)] }, index) : null; }) });
509
+ }
510
+ export function CmsLanguages() {
511
+ const { settings, entry } = useWebsiteSettings(), links = languageLinks(settings, entry);
512
+ if (links.length < 2)
513
+ return null;
514
+ return _jsx("nav", { className: "cms-website-languages", "aria-label": "Available page languages", children: links.map(item => _jsx(SafeLink, { url: item.path, hrefLang: item.locale, "aria-current": item.current ? 'page' : undefined, children: item.locale }, item.locale)) });
515
+ }
516
+ export function CmsExperience({ notFound = false }) {
517
+ const { settings, entry, consent, saveConsent, preferencesOpen, setPreferencesOpen } = useWebsiteSettings(), experience = experienceCopy(settings, entry?.locale), policy = consentPolicy(settings);
518
+ const [choices, setChoices] = useState({ analytics: false, marketing: false });
519
+ const panel = useRef(null), restoreFocus = useRef(null);
520
+ useEffect(() => { if (preferencesOpen) {
521
+ setChoices({ analytics: consent?.analytics === true, marketing: consent?.marketing === true });
522
+ restoreFocus.current = document.activeElement;
523
+ panel.current?.focus();
524
+ }
525
+ else {
526
+ restoreFocus.current?.focus();
527
+ restoreFocus.current = null;
528
+ } }, [preferencesOpen]);
529
+ const label = (name, fallback) => text(experience?.[name], 80) || fallback;
530
+ const position = ['bottom-left', 'bottom-center', 'bottom-right'].includes(experience?.cookiePosition) ? experience.cookiePosition : 'bottom-center';
531
+ if (notFound)
532
+ return _jsxs("main", { className: "cms-website-error", children: [_jsx("h1", { children: text(experience?.notFoundTitle, 500) || 'Page not found' }), _jsx("p", { children: text(experience?.notFoundMessage, 2000) || 'This page is not available.' }), publicHref(record(experience?.notFoundAction).url) && _jsx(SafeLink, { url: experience.notFoundAction.url, children: text(experience.notFoundAction.label, 80) || 'Go back' })] });
533
+ return _jsxs(_Fragment, { children: [policy && _jsx("button", { className: "cms-website-preferences", type: "button", onClick: () => setPreferencesOpen(true), children: label('cookiePreferencesLabel', 'Cookie preferences') }), policy && (!consent || preferencesOpen) && _jsxs("section", { ref: panel, tabIndex: -1, className: `cms-website-consent cms-website-${position}`, "aria-label": label('cookieTitle', 'Cookie preferences'), onKeyDown: event => { if (event.key === 'Escape' && consent)
534
+ setPreferencesOpen(false); }, children: [_jsx("h2", { children: label('cookieTitle', 'Cookie preferences') }), _jsx("p", { children: text(experience?.cookieNotice, 2000) || 'Choose whether to allow optional analytics and marketing cookies.' }), publicHref(experience?.privacyUrl) && _jsx(SafeLink, { url: experience.privacyUrl, children: label('privacyLabel', 'Privacy policy') }), preferencesOpen && _jsxs("fieldset", { children: [_jsx("legend", { children: "Optional cookies" }), _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: choices.analytics, onChange: event => setChoices(value => ({ ...value, analytics: event.target.checked })) }), "Analytics"] }), _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: choices.marketing, onChange: event => setChoices(value => ({ ...value, marketing: event.target.checked })) }), "Marketing"] })] }), _jsxs("div", { className: "cms-website-consent-actions", children: [_jsx("button", { type: "button", onClick: () => saveConsent({ analytics: false, marketing: false }), children: label('cookieRejectLabel', 'Reject optional') }), _jsx("button", { type: "button", onClick: () => saveConsent({ analytics: true, marketing: true }), children: label('cookieAcceptLabel', 'Accept optional') }), preferencesOpen ? _jsx("button", { type: "button", onClick: () => saveConsent(choices), children: label('cookieSaveLabel', 'Save preferences') }) : _jsx("button", { type: "button", onClick: () => setPreferencesOpen(true), children: label('cookiePreferencesLabel', 'Preferences') })] })] })] });
535
+ }
536
+ /** Add settings-owned chrome without replacing or walking the native tree. */
537
+ export function CmsWebsite({ children, initialSettings, entry, archive, scope, originalPaths: _originalPaths, applyTemplate = true, chrome = true }) {
538
+ const content = applyTemplate ? _jsx(NativePresentation, { children: children }) : children;
539
+ return _jsx(WebsiteProvider, { initialSettings: initialSettings, entry: entry, archive: archive, scope: scope, children: chrome ? _jsx(WebsiteChrome, { children: content }) : content });
540
+ }
541
+ function WebsiteChrome({ children }) {
542
+ const { settings } = useWebsiteSettings(), branding = sectionData(settings, 'branding');
543
+ const accent = /^#[0-9a-f]{6}$/i.test(branding?.accentColor ?? '') ? branding.accentColor : undefined;
544
+ return _jsxs("div", { className: "cms-website", style: accent ? { '--cms-website-accent': accent } : undefined, children: [_jsxs("header", { className: "cms-website-header", children: [_jsx(CmsBranding, {}), _jsx(CmsNavigation, {}), _jsx(CmsLanguages, {})] }), children, _jsxs("footer", { className: "cms-website-footer", children: [_jsx(CmsNavigation, { location: "footer" }), _jsx(CmsSocialLinks, {}), _jsx(CmsExperience, {})] })] });
545
+ }
546
+ function NativePresentation({ children }) {
547
+ const { settings, entry, related } = useWebsiteSettings(), root = useRef(null);
548
+ const presentation = pagePresentation(settings, entry), [headings, setHeadings] = useState([]);
549
+ useEffect(() => {
550
+ if (!root.current || !entry || !presentation.showTableOfContents) {
551
+ setHeadings([]);
552
+ return;
553
+ }
554
+ const container = root.current, added = new Map();
555
+ let serial = 0;
556
+ const options = { childList: true, subtree: true, characterData: true };
557
+ // Settings and the native document may finish loading in either order.
558
+ // Observe only content changes, so assigning IDs does not trigger a loop.
559
+ const observer = new MutationObserver(() => scan());
560
+ const scan = () => {
561
+ observer.disconnect();
562
+ observer.observe(container, options);
563
+ const nativeHeadings = new Set(container.querySelectorAll('h2,h3,h4'));
564
+ for (const slot of container.querySelectorAll('slot'))
565
+ for (const element of slot.assignedElements({ flatten: true })) {
566
+ observer.observe(element, options);
567
+ if (element.matches('h2,h3,h4'))
568
+ nativeHeadings.add(element);
569
+ for (const heading of element.querySelectorAll('h2,h3,h4'))
570
+ nativeHeadings.add(heading);
571
+ }
572
+ for (const [heading, id] of added)
573
+ if (!nativeHeadings.has(heading)) {
574
+ if (heading.id === id)
575
+ heading.removeAttribute('id');
576
+ added.delete(heading);
577
+ }
578
+ const rows = [...nativeHeadings].slice(0, 100).map(heading => {
579
+ let id = heading.id;
580
+ if (!id) {
581
+ do {
582
+ id = `cms-website-native-${entry.id}-${serial++}`;
583
+ } while ([...nativeHeadings].some(other => other.id === id));
584
+ heading.id = id;
585
+ added.set(heading, id);
586
+ }
587
+ return { id, title: heading.textContent ?? '' };
588
+ });
589
+ setHeadings(previous => JSON.stringify(previous) === JSON.stringify(rows) ? previous : rows);
590
+ };
591
+ scan();
592
+ container.addEventListener('slotchange', scan);
593
+ return () => { observer.disconnect(); container.removeEventListener('slotchange', scan); for (const [heading, id] of added)
594
+ if (heading.id === id)
595
+ heading.removeAttribute('id'); };
596
+ }, [entry, presentation.showTableOfContents]);
597
+ const template = !!entry && !!sectionData(settings, 'templates');
598
+ return _jsxs("div", { className: template ? `cms-website-native cms-website-layout-${presentation.layout}` : undefined, children: [entry && presentation.showBreadcrumbs && _jsxs("nav", { className: "cms-website-supplement", "aria-label": "Breadcrumbs", children: [_jsx(SafeLink, { url: "/", children: "Home" }), _jsx("span", { "aria-hidden": "true", children: " / " }), _jsx("span", { "aria-current": "page", children: entry.document.title })] }), entry && presentation.showDate && Number.isFinite(Date.parse(entry.publishedAt)) && _jsx("time", { className: "cms-website-supplement", dateTime: entry.publishedAt, children: new Date(entry.publishedAt).toLocaleDateString(entry.locale) }), presentation.showFeaturedImage && presentation.featuredImage.url && _jsx("img", { className: "cms-website-featured", src: presentation.featuredImage.url, alt: presentation.featuredImage.alt, referrerPolicy: "no-referrer" }), !!headings.length && _jsxs("nav", { className: "cms-website-toc cms-website-supplement", "aria-label": "Table of contents", children: [_jsx("h2", { children: "On this page" }), _jsx("ul", { children: headings.map(item => _jsx("li", { children: _jsx(SafeLink, { url: `${typeof window === 'undefined' ? '' : window.location.pathname}#${encodeURIComponent(item.id)}`, children: item.title }) }, item.id)) })] }), _jsx("div", { ref: root, children: children }), presentation.author && _jsxs("aside", { className: "cms-website-author cms-website-supplement", "aria-label": "About the author", children: [presentation.author.avatarUrl && _jsx("img", { src: presentation.author.avatarUrl, alt: "", loading: "lazy", referrerPolicy: "no-referrer" }), _jsxs("div", { children: [_jsx("h2", { children: presentation.author.url ? _jsx(SafeLink, { url: presentation.author.url, children: presentation.author.name }) : presentation.author.name }), _jsx("p", { children: presentation.author.jobTitle }), _jsx("p", { children: presentation.author.bio })] })] }), (presentation.categories.length > 0 || presentation.tags.length > 0) && _jsx("nav", { className: "cms-website-terms cms-website-supplement", "aria-label": "Categories and tags", children: [...presentation.categories, ...presentation.tags].map((term, index) => _jsx(SafeLink, { url: term.path, children: term.name }, `${term.id}:${index}`)) }), entry && presentation.showShare && _jsx(SharePage, { title: entry.document.title, path: entry.path || '/' }), presentation.showRelated && related.length > 0 && _jsxs("aside", { className: "cms-website-related cms-website-supplement", children: [_jsx("h2", { children: "Related content" }), _jsx("div", { className: "cms-website-cards", children: related.slice(0, 6).map((value, index) => { const row = record(value); return publicHref(row.path)?.startsWith('/') ? _jsxs("article", { children: [_jsx("h3", { children: _jsx(SafeLink, { url: row.path, children: text(row.title, 200) }) }), _jsx("p", { children: text(row.excerpt, 500) })] }, index) : null; }) })] })] });
599
+ }
600
+ function archiveMetadata(value) {
601
+ const row = record(value), path = publicHref(row.path);
602
+ if (!['category', 'tag'].includes(row.kind) || !path?.startsWith('/') || /[?#]/.test(path) ||
603
+ typeof row.title !== 'string' || !row.title.trim() || row.title.length > 200 ||
604
+ typeof row.description !== 'string' || row.description.length > 1000)
605
+ throw Error('Archive metadata unavailable');
606
+ return { kind: row.kind, path, title: row.title, description: row.description };
607
+ }
608
+ function validatedPage(value) {
609
+ const result = record(value);
610
+ if (result.state === 'archive' && ['category', 'tag'].includes(result.kind) && publicHref(result.path)?.startsWith('/') && Array.isArray(result.entries) && result.entries.length <= 1000) {
611
+ return { state: 'archive', ...archiveMetadata(result), entries: result.entries.map((value) => publicEntry(value)) };
612
+ }
613
+ if (result.state !== 'published')
614
+ throw Error('Published page unavailable');
615
+ const entry = publicEntry(result.entry), revision = `${entry.id}:${entry.version}${entry.revisionId ? `:${entry.revisionId}` : ''}`;
616
+ if (result.revision !== undefined && result.revision !== revision)
617
+ throw Error('Published page unavailable');
618
+ return { ...result, entry };
619
+ }
620
+ function publicEntry(value) {
621
+ const raw = record(value), entry = validateEntry(value, { kind: text(raw.kind, 40), slug: text(raw.slug, 160), locale: text(raw.locale, 35) });
622
+ // Extras are already projected by the private runtime. Each presentation helper
623
+ // independently validates its URLs and only consumes a ready selected section.
624
+ return { ...entry, ...Object.fromEntries(['path', 'seo', 'author', 'categories', 'tags', 'translations', 'canonicalUrl', 'publishedAt'].map(name => [name, raw[name]])) };
625
+ }
626
+ export function CmsWebsitePage({ scope, pathname, initialEntry, initialResult, initialSettings, chrome = true }) {
627
+ const path = pathname ?? (typeof window === 'undefined' ? '/' : window.location.pathname);
628
+ return _jsx(WebsitePageLoader, { scope: scope, pathname: path, initialEntry: initialEntry, initialResult: initialResult, initialSettings: initialSettings, chrome: chrome }, `${scope}:${path}`);
629
+ }
630
+ function WebsitePageLoader({ scope, pathname, initialEntry, initialResult, initialSettings, chrome }) {
631
+ const [page, setPage] = useState(() => { try {
632
+ return initialResult ? validatedPage(initialResult) : initialEntry ? validatedPage({ state: 'published', entry: initialEntry }) : null;
633
+ }
634
+ catch {
635
+ return null;
636
+ } });
637
+ const [failure, setFailure] = useState(null), generation = useRef(0);
638
+ useEffect(() => {
639
+ const documentRoot = document.documentElement;
640
+ let controller, disposed = false;
641
+ const active = () => !disposed && document.documentElement === documentRoot;
642
+ const refresh = () => {
643
+ if (!active())
644
+ return;
645
+ const current = ++generation.current;
646
+ controller?.abort();
647
+ const request = new AbortController();
648
+ controller = request;
649
+ const timer = window.setTimeout(() => request.abort(), 10000);
650
+ void (async () => {
651
+ try {
652
+ const response = await fetch(`/api/universalcms/page?path=${encodeURIComponent(pathname)}`, { headers: { Accept: 'application/json' }, credentials: 'omit', cache: 'no-store', redirect: 'error', referrerPolicy: 'no-referrer', signal: request.signal });
653
+ if (response.status === 404)
654
+ throw 404;
655
+ const next = validatedPage(await readWebsiteJson(response, request.signal));
656
+ if (next.path && next.path !== pathname)
657
+ throw 503;
658
+ if (active() && current === generation.current) {
659
+ setPage(next);
660
+ setFailure(null);
661
+ }
662
+ }
663
+ catch (error) {
664
+ if (active() && current === generation.current) {
665
+ setPage(null);
666
+ setFailure(error === 404 ? 404 : 503);
667
+ }
668
+ }
669
+ finally {
670
+ window.clearTimeout(timer);
671
+ }
672
+ })();
673
+ };
674
+ const visible = () => { if (document.visibilityState === 'visible')
675
+ refresh(); };
676
+ refresh();
677
+ const interval = window.setInterval(refresh, 15000);
678
+ window.addEventListener('focus', refresh);
679
+ document.addEventListener('visibilitychange', visible);
680
+ return () => { disposed = true; generation.current++; controller?.abort(); window.clearInterval(interval); window.removeEventListener('focus', refresh); document.removeEventListener('visibilitychange', visible); };
681
+ }, [pathname]);
682
+ const entry = page?.state === 'published' ? page.entry : undefined;
683
+ const archive = useMemo(() => page?.state === 'archive' ? archiveMetadata(page) : undefined, [page]);
684
+ return _jsx(PageAvailabilityContext.Provider, { value: failure ? 'unavailable' : page ? 'ready' : 'loading', children: _jsx(CmsWebsite, { scope: scope, entry: entry, archive: archive, initialSettings: initialSettings, applyTemplate: false, chrome: chrome, children: failure === 404 ? _jsx(CmsExperience, { notFound: true }) : failure ? _jsxs("main", { className: "cms-website-error", role: "alert", children: [_jsx("h1", { children: "Page temporarily unavailable" }), _jsx("p", { children: "Please try again later." })] }) : !page ? _jsx("main", { className: "cms-website-error", role: "status", "aria-busy": "true", children: "Loading published content\u2026" }) : page.state === 'archive' ? _jsx(ArchivePage, { page: page }) : entry && _jsx(PublishedPage, { entry: entry, pathname: pathname, related: page.related }) }) });
685
+ }
686
+ function ArchivePage({ page }) {
687
+ const { settings } = useWebsiteSettings();
688
+ if (!sectionData(settings, 'taxonomies'))
689
+ return _jsx(CmsExperience, { notFound: true });
690
+ return _jsxs("main", { className: "cms-website-page", children: [_jsx("h1", { children: text(page.title, 200) }), _jsx("p", { children: text(page.description, 1000) }), _jsx("div", { className: "cms-website-cards", children: page.entries.map((entry) => _jsxs("article", { children: [_jsx("h2", { children: _jsx(SafeLink, { url: entry.path, children: entry.document.title }) }), _jsx("p", { children: entry.document.excerpt })] }, entry.id)) })] });
691
+ }
692
+ function PublishedPage({ entry, pathname, related }) {
693
+ const { settings } = useWebsiteSettings(), presentation = pagePresentation(settings, entry), content = useRef(null);
694
+ const [headings, setHeadings] = useState([]);
695
+ useEffect(() => {
696
+ const root = content.current;
697
+ if (!root || !presentation.showTableOfContents) {
698
+ setHeadings([]);
699
+ return;
700
+ }
701
+ const added = [], rows = [];
702
+ root.querySelectorAll('h2,h3,h4').forEach((heading, index) => { let id = heading.id; if (!id) {
703
+ id = `cms-website-heading-${entry.id}-${index}`;
704
+ heading.id = id;
705
+ added.push(heading);
706
+ } rows.push({ id, title: heading.textContent ?? '' }); });
707
+ setHeadings(rows);
708
+ return () => { for (const heading of added)
709
+ heading.removeAttribute('id'); };
710
+ }, [entry, presentation.showTableOfContents]);
711
+ const route = useMemo(() => ({ identity: { kind: entry.kind, slug: entry.slug, locale: entry.locale }, baseline: entry.document, bindings: {} }), [entry]);
712
+ const publishedDate = Number.isFinite(Date.parse(entry.publishedAt)) ? new Date(entry.publishedAt).toLocaleDateString(entry.locale) : '';
713
+ return _jsxs("main", { className: `cms-website-page cms-website-layout-${presentation.layout}`, children: [presentation.showBreadcrumbs && _jsxs("nav", { "aria-label": "Breadcrumbs", children: [_jsx(SafeLink, { url: "/", children: "Home" }), _jsx("span", { "aria-hidden": "true", children: " / " }), _jsx("span", { "aria-current": "page", children: entry.document.title })] }), _jsx("h1", { children: entry.document.title }), entry.document.excerpt && _jsx("p", { className: "cms-website-excerpt", children: entry.document.excerpt }), presentation.showDate && publishedDate && _jsx("time", { dateTime: entry.publishedAt, children: publishedDate }), presentation.showFeaturedImage && presentation.featuredImage.url && _jsx("img", { className: "cms-website-featured", src: presentation.featuredImage.url, alt: presentation.featuredImage.alt, referrerPolicy: "no-referrer" }), !!headings.length && _jsxs("nav", { className: "cms-website-toc", "aria-label": "Table of contents", children: [_jsx("h2", { children: "On this page" }), _jsx("ul", { children: headings.map(item => _jsx("li", { children: _jsx(SafeLink, { url: `${pathname}#${encodeURIComponent(item.id)}`, children: item.title }) }, item.id)) })] }), _jsx("div", { ref: content, children: _jsx(PublishedDocumentProvider, { initialEntry: entry, pathname: pathname, route: route, children: _jsx(CmsPublishedDocument, {}) }) }), presentation.author && _jsxs("aside", { className: "cms-website-author", "aria-label": "About the author", children: [presentation.author.avatarUrl && _jsx("img", { src: presentation.author.avatarUrl, alt: "", loading: "lazy", referrerPolicy: "no-referrer" }), _jsxs("div", { children: [_jsx("h2", { children: presentation.author.url ? _jsx(SafeLink, { url: presentation.author.url, children: presentation.author.name }) : presentation.author.name }), _jsx("p", { children: presentation.author.jobTitle }), _jsx("p", { children: presentation.author.bio })] })] }), (presentation.categories.length > 0 || presentation.tags.length > 0) && _jsx("nav", { className: "cms-website-terms", "aria-label": "Categories and tags", children: [...presentation.categories, ...presentation.tags].map((term, index) => _jsx(SafeLink, { url: term.path, children: term.name }, `${term.id}:${index}`)) }), presentation.showShare && _jsx(SharePage, { title: entry.document.title, path: entry.path || pathname }), presentation.showRelated && Array.isArray(related) && related.length > 0 && _jsxs("aside", { className: "cms-website-related", children: [_jsx("h2", { children: "Related content" }), _jsx("div", { className: "cms-website-cards", children: related.slice(0, 6).map((value, index) => { const row = record(value); return publicHref(row.path)?.startsWith('/') ? _jsxs("article", { children: [_jsx("h3", { children: _jsx(SafeLink, { url: row.path, children: text(row.title, 200) }) }), _jsx("p", { children: text(row.excerpt, 500) })] }, index) : null; }) })] })] });
714
+ }
715
+ function SharePage({ title, path }) {
716
+ const [status, setStatus] = useState('');
717
+ const share = async () => {
718
+ const safe = publicHref(path);
719
+ if (!safe || !safe.startsWith('/'))
720
+ return;
721
+ const url = new URL(safe, window.location.origin).href;
722
+ try {
723
+ if (navigator.share)
724
+ await navigator.share({ title, url });
725
+ else {
726
+ await navigator.clipboard.writeText(url);
727
+ setStatus('Link copied');
728
+ }
729
+ }
730
+ catch {
731
+ setStatus('Sharing was canceled or unavailable.');
732
+ }
733
+ };
734
+ return _jsxs("div", { className: "cms-website-share", children: [_jsx("button", { type: "button", onClick: () => { void share(); }, children: "Share page" }), _jsx("span", { role: "status", children: status })] });
735
+ }