@universalcms/connect 0.0.0-stage → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +779 -2
- package/dist/angular-runtime.d.mts +4 -0
- package/dist/angular-runtime.mjs +7 -0
- package/dist/angular-setup.mjs +124 -0
- package/dist/astro-runtime.d.mts +6 -0
- package/dist/astro-runtime.mjs +255 -0
- package/dist/astro-setup.mjs +215 -0
- package/dist/astro-source.mjs +451 -0
- package/dist/astro-website.d.mts +5 -0
- package/dist/astro-website.mjs +42 -0
- package/dist/cli.mjs +552 -0
- package/dist/connector-runtime.mjs +130 -0
- package/dist/content-next-setup.mjs +200 -0
- package/dist/content-registry-validation.mjs +40 -0
- package/dist/content-registry.mjs +44 -0
- package/dist/content-setup.mjs +13 -0
- package/dist/content-storefront-setup.mjs +155 -0
- package/dist/content-vite-setup.mjs +107 -0
- package/dist/core.mjs +205 -0
- package/dist/framework-ownership.mjs +68 -0
- package/dist/framework.mjs +54 -0
- package/dist/guided.mjs +335 -0
- package/dist/native-adapters.mjs +31 -0
- package/dist/native-chrome-model.d.mts +3 -0
- package/dist/native-chrome-model.mjs +14 -0
- package/dist/native-chrome-review.mjs +49 -0
- package/dist/native-chrome-source.mjs +69 -0
- package/dist/native-route-review.mjs +152 -0
- package/dist/native-routes.mjs +27 -0
- package/dist/native-website-model.d.mts +18 -0
- package/dist/native-website-model.mjs +67 -0
- package/dist/native-website-server.d.mts +1 -0
- package/dist/native-website-server.mjs +35222 -0
- package/dist/native-website-server.mjs.LEGAL.txt +56 -0
- package/dist/native-website.d.mts +3 -0
- package/dist/native-website.mjs +120 -0
- package/dist/native-website.mjs.LEGAL.txt +56 -0
- package/dist/next-publication.d.mts +12 -0
- package/dist/next-publication.mjs +107 -0
- package/dist/next-settings-layout.mjs +233 -0
- package/dist/next-source.d.mts +8 -0
- package/dist/next-source.mjs +93 -0
- package/dist/page-discovery.mjs +236 -0
- package/dist/page-mapping-review.mjs +160 -0
- package/dist/preflight-recovery.mjs +122 -0
- package/dist/preflight.mjs +138 -0
- package/dist/project-files.mjs +56 -0
- package/dist/public-bindings.d.mts +3 -0
- package/dist/public-bindings.mjs +28 -0
- package/dist/publication-browser.d.mts +5 -0
- package/dist/publication-browser.mjs +258 -0
- package/dist/publication-check.mjs +141 -0
- package/dist/publication-document.d.mts +2 -0
- package/dist/publication-document.mjs +59 -0
- package/dist/publication-next-setup.mjs +134 -0
- package/dist/publication-setup.mjs +138 -0
- package/dist/publication-verification.mjs +85 -0
- package/dist/publication.css +6 -0
- package/dist/publication.d.mts +31 -0
- package/dist/publication.mjs +320 -0
- package/dist/react-publication.d.mts +22 -0
- package/dist/react-publication.mjs +301 -0
- package/dist/react-website.d.mts +15 -0
- package/dist/react-website.mjs +735 -0
- package/dist/reviewed-commerce-ownership.mjs +26 -0
- package/dist/reviewed-commerce-settings-setup.mjs +243 -0
- package/dist/reviewed-native-ownership.mjs +118 -0
- package/dist/reviewed-storefront-source.mjs +227 -0
- package/dist/runtime-dependencies.mjs +59 -0
- package/dist/setup-environment.mjs +307 -0
- package/dist/source-bindings.mjs +77 -0
- package/dist/source-check.mjs +149 -0
- package/dist/source-collect.mjs +80 -0
- package/dist/source-config.mjs +20 -0
- package/dist/source-extract.mjs +514 -0
- package/dist/source-finalize.mjs +52 -0
- package/dist/source-routes.mjs +65 -0
- package/dist/source-setup.mjs +490 -0
- package/dist/staged-setup.mjs +287 -0
- package/dist/static-runtime.mjs +67 -0
- package/dist/static-setup.mjs +136 -0
- package/dist/svelte-runtime.d.mts +27 -0
- package/dist/svelte-runtime.mjs +40 -0
- package/dist/svelte-setup.mjs +135 -0
- package/dist/svelte-template.mjs +48 -0
- package/dist/templates.mjs +248 -0
- package/dist/vendor/analyze-project.mjs +82 -0
- package/dist/vendor/astro-compiler/LICENSE +53 -0
- package/dist/vendor/astro-compiler/astro.wasm +0 -0
- package/dist/vendor/astro-compiler/chunk-W5DTLHV4.js +1 -0
- package/dist/vendor/astro-compiler/node/index.js +1 -0
- package/dist/vendor/check-delivery.mjs +158 -0
- package/dist/vendor/dependency-diagnostics.mjs +72 -0
- package/dist/vendor/inspect-installed-dependencies.mjs +125 -0
- package/dist/vendor/project-analysis.mjs +498 -0
- package/dist/vendor/react-LICENSE.txt +21 -0
- package/dist/vendor/react-document.mjs +124 -0
- package/dist/vendor/react-dom-LICENSE.txt +21 -0
- package/dist/vendor/react-form.mjs +63 -0
- package/dist/vendor/react-safety.mjs +47 -0
- package/dist/vendor/source-manifest-provider.mjs +104 -0
- package/dist/vendor/typescript-LICENSE.txt +55 -0
- package/dist/vendor/typescript.cjs +200276 -0
- package/dist/vite-handler.mjs +54 -0
- package/dist/vite-publication-server.d.mts +3 -0
- package/dist/vite-publication-server.mjs +82 -0
- package/dist/vite-publication.d.mts +15 -0
- package/dist/vite-publication.mjs +241 -0
- package/dist/vite-source-discovery.mjs +315 -0
- package/dist/vite-source.d.mts +30 -0
- package/dist/vite-source.mjs +141 -0
- package/dist/vue-runtime.d.mts +10 -0
- package/dist/vue-runtime.mjs +50 -0
- package/dist/vue-server.d.mts +2 -0
- package/dist/vue-server.mjs +58 -0
- package/dist/vue-setup.mjs +127 -0
- package/dist/vue-source.mjs +168 -0
- package/dist/website-check.mjs +41 -0
- package/dist/website-http.mjs +149 -0
- package/dist/website-model.d.mts +20 -0
- package/dist/website-model.mjs +155 -0
- package/dist/website-next.d.mts +2 -0
- package/dist/website-next.mjs +29 -0
- package/dist/website-node.d.mts +3 -0
- package/dist/website-node.mjs +50 -0
- package/dist/website-runtime.d.mts +13 -0
- package/dist/website-runtime.mjs +396 -0
- package/dist/website-schema.mjs +22175 -0
- package/dist/website-setup.mjs +256 -0
- package/dist/website.css +49 -0
- package/dist/wizard.mjs +298 -0
- package/package.json +60 -4
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/** Private, fixed-scope website configuration and new-page delivery. No cache. */
|
|
2
|
+
import { nativePatternMatches, validateNativeRoutes } from './native-routes.mjs';
|
|
3
|
+
import { validatePublicationConfig, normalizePublicationPath, isExcludedPublicationPath, publicPublishedEntry, publicationRevision } from './publication.mjs';
|
|
4
|
+
import { websiteSections, parseWebsiteSection, parseWebsiteEntryMetadata } from './website-schema.mjs';
|
|
5
|
+
|
|
6
|
+
export { websiteSections };
|
|
7
|
+
const record = value => value !== null && typeof value === 'object' && !Array.isArray(value) && [Object.prototype, null].includes(Object.getPrototypeOf(value));
|
|
8
|
+
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
9
|
+
const keyPattern = /^cms_server\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.[A-Za-z0-9_-]{43}$/;
|
|
10
|
+
const fail = () => { throw new Error('Website configuration is unavailable.'); };
|
|
11
|
+
const integer = (value, min, max) => Number.isSafeInteger(value) && value >= min && value <= max;
|
|
12
|
+
const state = (value, version = 0, data) => ({ state: value, version, ...(data === undefined ? {} : { data }) });
|
|
13
|
+
const unavailableSnapshot = () => ({ version: 1, state: 'unavailable', sections: Object.fromEntries(websiteSections.map(section => [section, state('unavailable')])) });
|
|
14
|
+
const result = (value, extra = {}) => ({ state: value, status: value === 'missing' ? 404 : value === 'gone' ? 410 : value === 'unavailable' ? 503 : 200, ...extra });
|
|
15
|
+
const secret = /cms_server\.|sk-[A-Za-z0-9_-]{12,}|AIza[A-Za-z0-9_-]{20,}/i;
|
|
16
|
+
function canonicalLocale(value) { try { const values = Intl.getCanonicalLocales(value); if (typeof value !== 'string' || value.length > 35 || values.length !== 1) return fail(); return values[0]; } catch { return fail(); } }
|
|
17
|
+
function safeOrigin(value) {
|
|
18
|
+
if (typeof value !== 'string' || value.length > 2048 || /[\\\s]/.test(value) || secret.test(value)) return fail();
|
|
19
|
+
const url = new URL(value);
|
|
20
|
+
if (url.username || url.password || url.search || url.hash || url.pathname !== '/' || !(url.protocol === 'https:' || url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))) return fail();
|
|
21
|
+
return url.origin;
|
|
22
|
+
}
|
|
23
|
+
function publicPath(value) {
|
|
24
|
+
const path = normalizePublicationPath(value);
|
|
25
|
+
if (isExcludedPublicationPath(path) || secret.test(path)) return fail();
|
|
26
|
+
return path;
|
|
27
|
+
}
|
|
28
|
+
function publicUrl(value, websiteOrigin, { absolute = false, page = true } = {}) {
|
|
29
|
+
if (typeof value !== 'string' || value.length > 2048 || /[\\\s\u0000-\u001f\u007f]|%(?:0[0-9a-f]|1[0-9a-f]|7f|5c)/i.test(value) || secret.test(value)) return fail();
|
|
30
|
+
if (!absolute && value.startsWith('/') && !value.startsWith('//')) return publicPath(value);
|
|
31
|
+
const url = new URL(value);
|
|
32
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) return fail();
|
|
33
|
+
if (page) publicPath(url.pathname);
|
|
34
|
+
if (url.origin === websiteOrigin && !url.search && !url.hash) return absolute ? url.href : publicPath(url.pathname);
|
|
35
|
+
return url.href;
|
|
36
|
+
}
|
|
37
|
+
function cancel(response) { try { if (response?.body && !response.body.locked) void response.body.cancel().catch(() => {}); } catch { /* Cleanup only. */ } }
|
|
38
|
+
function untilAborted(pending, signal) {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const aborted = () => reject(new Error('Website configuration is unavailable.'));
|
|
41
|
+
signal.addEventListener('abort', aborted, { once: true });
|
|
42
|
+
if (signal.aborted) aborted();
|
|
43
|
+
Promise.resolve(pending).then(value => { signal.removeEventListener('abort', aborted); resolve(value); }, error => { signal.removeEventListener('abort', aborted); reject(error); });
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function publicEntry(raw, config) {
|
|
47
|
+
if (!record(raw) || !uuid.test(raw.id ?? '') || !['page', 'post'].includes(raw.kind) || typeof raw.slug !== 'string' || raw.slug.length > 160 || !/^[a-z0-9]+(?:[-/][a-z0-9]+)*$/.test(raw.slug) ||
|
|
48
|
+
typeof raw.publishedAt !== 'string' || !Number.isFinite(Date.parse(raw.publishedAt)) || Object.hasOwn(raw, 'preview') || raw.status !== undefined && raw.status !== 'published' ||
|
|
49
|
+
raw.siteId !== undefined && raw.siteId !== config.siteId || raw.environmentId !== undefined && raw.environmentId !== config.environmentId) return fail();
|
|
50
|
+
const path = publicPath(raw.path), locale = canonicalLocale(raw.locale), entry = publicPublishedEntry({ ...raw, locale });
|
|
51
|
+
const metadata = parseWebsiteEntryMetadata(raw);
|
|
52
|
+
if (metadata.canonicalUrl) metadata.canonicalUrl = publicUrl(metadata.canonicalUrl, config.websiteOrigin, { absolute: true });
|
|
53
|
+
for (const key of ['categories', 'tags']) if (metadata[key]) metadata[key] = metadata[key].map(term => ({ ...term, path: publicPath(term.path) }));
|
|
54
|
+
if (metadata.translations) metadata.translations = metadata.translations.filter(translation => translation.path).map(translation => ({ ...translation, locale: canonicalLocale(translation.locale), path: publicPath(translation.path) }));
|
|
55
|
+
return { ...entry, ...metadata, path };
|
|
56
|
+
}
|
|
57
|
+
const summary = entry => ({ id: entry.id, kind: entry.kind, slug: entry.slug, locale: entry.locale, path: entry.path, title: entry.document.title, excerpt: entry.document.excerpt, publishedAt: entry.publishedAt });
|
|
58
|
+
|
|
59
|
+
export function createWebsiteRuntime({ configuration, env = process.env, fetcher = globalThis.fetch, nativePaths = [], nativePatterns = [], nativePathPrefixes = [], installedSections = websiteSections, timeoutMs = 10_000, maxResponseBytes = 8 * 1024 * 1024, maxConcurrent = 16 } = {}) {
|
|
60
|
+
if (typeof window !== 'undefined') throw new Error('Website configuration requires a private server runtime.');
|
|
61
|
+
const config = validatePublicationConfig(configuration);
|
|
62
|
+
if (typeof fetcher !== 'function' || !Array.isArray(nativePaths) || nativePaths.length > 1000 || !integer(timeoutMs, 1, 10000) || !integer(maxResponseBytes, 1, 8 * 1024 * 1024) || !integer(maxConcurrent, 1, 16)) return fail();
|
|
63
|
+
const native = new Set(nativePaths.map(normalizePublicationPath));
|
|
64
|
+
if (!Array.isArray(nativePatterns)) return fail();
|
|
65
|
+
if (nativePatterns.length) validateNativeRoutes({nativePaths:[...native],nativePatterns},config.routes);
|
|
66
|
+
if (!Array.isArray(nativePathPrefixes) || nativePathPrefixes.length > 100 || !Array.isArray(installedSections) || installedSections.length > websiteSections.length || new Set(installedSections).size !== installedSections.length || installedSections.some(name => !websiteSections.includes(name))) return fail();
|
|
67
|
+
const prefixes = nativePathPrefixes.map(value => {
|
|
68
|
+
if (typeof value !== 'string' || !value.endsWith('/') || value === '/' || normalizePublicationPath(value.slice(0, -1)) !== value.slice(0, -1)) return fail();
|
|
69
|
+
return value;
|
|
70
|
+
});
|
|
71
|
+
const isNative = path => native.has(path) || nativePatterns.some(pattern => nativePatternMatches(pattern, path)) || prefixes.some(prefix => path.startsWith(prefix));
|
|
72
|
+
const installed = new Set(installedSections);
|
|
73
|
+
const original = new Set(config.routes.map(route => route.path));
|
|
74
|
+
if (original.has('/')) original.add('/index.html');
|
|
75
|
+
const unclaimed = path => !original.has(path) && !isNative(path) && !isExcludedPublicationPath(path);
|
|
76
|
+
let active = 0;
|
|
77
|
+
async function operation(work) {
|
|
78
|
+
if (typeof window !== 'undefined' || active >= maxConcurrent) return fail();
|
|
79
|
+
active++;
|
|
80
|
+
let controller, timer;
|
|
81
|
+
try {
|
|
82
|
+
// Read the caller's private env on every operation; rotation needs no cached client.
|
|
83
|
+
const values = typeof env === 'function' ? await env() : env;
|
|
84
|
+
if (!values || typeof values !== 'object' || Array.isArray(values) || typeof values.CMS_API_KEY !== 'string' || !keyPattern.test(values.CMS_API_KEY) ||
|
|
85
|
+
values.CMS_API_BASE_URL !== undefined && safeOrigin(values.CMS_API_BASE_URL) !== config.apiOrigin ||
|
|
86
|
+
values.CMS_SITE_ID !== undefined && String(values.CMS_SITE_ID).toLowerCase() !== config.siteId ||
|
|
87
|
+
values.CMS_ENVIRONMENT_ID !== undefined && String(values.CMS_ENVIRONMENT_ID).toLowerCase() !== config.environmentId ||
|
|
88
|
+
values.CMS_WEBSITE_ORIGIN !== undefined && safeOrigin(values.CMS_WEBSITE_ORIGIN) !== config.websiteOrigin) {
|
|
89
|
+
if (!values?.CMS_API_KEY || typeof values.CMS_API_KEY !== 'string' || !keyPattern.test(values.CMS_API_KEY)) {
|
|
90
|
+
console.warn('[UniversalCMS] Delivery check failed: CMS_API_KEY is missing or invalid in private environment.');
|
|
91
|
+
}
|
|
92
|
+
return fail();
|
|
93
|
+
}
|
|
94
|
+
const locale = values.CMS_LOCALE === undefined ? undefined : canonicalLocale(values.CMS_LOCALE);
|
|
95
|
+
const key = values.CMS_API_KEY;
|
|
96
|
+
controller = new AbortController(); timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
97
|
+
let bytes = 0, invalidAuthority = false;
|
|
98
|
+
async function read(resource, query = {}) {
|
|
99
|
+
if (controller.signal.aborted || invalidAuthority || bytes >= maxResponseBytes) return fail();
|
|
100
|
+
const url = new URL(`/api/v1/delivery/sites/${config.siteId}/${resource}`, config.apiOrigin);
|
|
101
|
+
url.search = new URLSearchParams({ ...query, environmentId: config.environmentId }).toString();
|
|
102
|
+
let response;
|
|
103
|
+
try {
|
|
104
|
+
const pending = Promise.resolve(fetcher(url, { method: 'GET', headers: { Accept: 'application/json', Authorization: `Bearer ${key}` }, credentials: 'omit', redirect: 'error', cache: 'no-store', referrerPolicy: 'no-referrer', signal: controller.signal }))
|
|
105
|
+
.then(value => { if (controller.signal.aborted) cancel(value); return value; });
|
|
106
|
+
response = await untilAborted(pending, controller.signal);
|
|
107
|
+
if (!(response instanceof Response) || response.redirected || ['opaque', 'opaqueredirect'].includes(response.type) || response.url && response.url !== url.href) return fail();
|
|
108
|
+
if (response.status === 401) {
|
|
109
|
+
invalidAuthority = true;
|
|
110
|
+
console.warn(`[UniversalCMS] Delivery API rejected CMS_API_KEY (HTTP 401 Unauthorized) for site ${config.siteId} and environment ${config.environmentId}. Verify private server key in .env.local.`);
|
|
111
|
+
}
|
|
112
|
+
if ([403, 404].includes(response.status)) return { denied: true };
|
|
113
|
+
if (response.status !== 200 || response.headers.get('content-type')?.split(';')[0]?.trim().toLowerCase() !== 'application/json' || !response.body) return fail();
|
|
114
|
+
const declared = response.headers.get('content-length');
|
|
115
|
+
if (declared !== null && (!/^\d+$/.test(declared) || Number(declared) > maxResponseBytes - bytes)) return fail();
|
|
116
|
+
const reader = response.body.getReader(), decoder = new TextDecoder('utf-8', { fatal: true });
|
|
117
|
+
let text = '';
|
|
118
|
+
try {
|
|
119
|
+
for (;;) {
|
|
120
|
+
const chunk = await untilAborted(reader.read(), controller.signal);
|
|
121
|
+
if (controller.signal.aborted) return fail();
|
|
122
|
+
if (chunk.done) break;
|
|
123
|
+
if (!(chunk.value instanceof Uint8Array) || (bytes += chunk.value.byteLength) > maxResponseBytes) return fail();
|
|
124
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
125
|
+
}
|
|
126
|
+
const envelope = JSON.parse(text + decoder.decode());
|
|
127
|
+
if (!record(envelope) || !Object.hasOwn(envelope, 'data') || Object.hasOwn(envelope, 'preview') || record(envelope.data) && Object.hasOwn(envelope.data, 'preview')) return fail();
|
|
128
|
+
return { data: envelope.data };
|
|
129
|
+
} finally { void reader.cancel().catch(() => {}); reader.releaseLock(); }
|
|
130
|
+
} finally { cancel(response); }
|
|
131
|
+
}
|
|
132
|
+
async function section(name) {
|
|
133
|
+
// An adapter cannot activate settings it does not consume merely because
|
|
134
|
+
// a replacement key grants broader access.
|
|
135
|
+
if (!installed.has(name)) return state('missing');
|
|
136
|
+
try {
|
|
137
|
+
const reply = await read(`settings/${name}`);
|
|
138
|
+
if (reply.denied) return state('denied');
|
|
139
|
+
const value = parseWebsiteSection(name, reply.data);
|
|
140
|
+
return value.version === 0 ? state('missing') : state('ready', value.version, value.data);
|
|
141
|
+
} catch { return state('unavailable'); }
|
|
142
|
+
}
|
|
143
|
+
async function inventory() {
|
|
144
|
+
const entries = [], ids = new Set(); let total, count = 0;
|
|
145
|
+
for (let offset = 0; offset < 1000; offset += 100) {
|
|
146
|
+
const reply = await read('content', { limit: '100', offset: String(offset) });
|
|
147
|
+
const page = reply.data;
|
|
148
|
+
if (reply.denied || !record(page) || !integer(page.total, 0, 1000) || !Array.isArray(page.items) || page.items.length > 100 || page.items.length > page.total || total !== undefined && page.total !== total) return fail();
|
|
149
|
+
total = page.total;
|
|
150
|
+
for (const raw of page.items) {
|
|
151
|
+
if (!record(raw) || !uuid.test(raw.id ?? '') || ids.has(raw.id) || typeof raw.kind !== 'string' || !/^[a-z][a-z0-9_-]{0,39}$/.test(raw.kind) || Object.hasOwn(raw, 'preview') || raw.status !== undefined && raw.status !== 'published' || raw.siteId !== undefined && raw.siteId !== config.siteId || raw.environmentId !== undefined && raw.environmentId !== config.environmentId) return fail();
|
|
152
|
+
ids.add(raw.id); count++;
|
|
153
|
+
if (['page', 'post'].includes(raw.kind)) {
|
|
154
|
+
if (isExcludedPublicationPath(normalizePublicationPath(raw.path))) continue;
|
|
155
|
+
const entry = publicEntry(raw, config);
|
|
156
|
+
entries.push(entry);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (count === total) return entries;
|
|
160
|
+
if (count > total || page.items.length !== 100) return fail();
|
|
161
|
+
}
|
|
162
|
+
return fail();
|
|
163
|
+
}
|
|
164
|
+
const value = await work({ read, section, inventory, locale });
|
|
165
|
+
if (invalidAuthority) return fail();
|
|
166
|
+
return value;
|
|
167
|
+
} finally { active--; clearTimeout(timer); controller?.abort(); }
|
|
168
|
+
}
|
|
169
|
+
function paths(entries) {
|
|
170
|
+
const map = new Map();
|
|
171
|
+
for (const entry of entries) {
|
|
172
|
+
if (!unclaimed(entry.path) || config.routes.some(route => route.kind === entry.kind && route.slug === entry.slug && route.locale === entry.locale)) continue;
|
|
173
|
+
if (map.has(entry.path)) return fail();
|
|
174
|
+
map.set(entry.path, entry);
|
|
175
|
+
}
|
|
176
|
+
return map;
|
|
177
|
+
}
|
|
178
|
+
function redirect(path, section) {
|
|
179
|
+
if (section.state !== 'ready') return null;
|
|
180
|
+
const mappings = new Map(section.data.entries.filter(entry => entry.enabled).map(entry => [normalizePublicationPath(entry.from), entry]));
|
|
181
|
+
const first = mappings.get(path); if (!first) return null;
|
|
182
|
+
if (first.status === 410) return result('gone', { path });
|
|
183
|
+
let current = path; const seen = new Set([path]);
|
|
184
|
+
for (let count = 0; count < 500; count++) {
|
|
185
|
+
const next = mappings.get(current); if (!next || next.status === 410) break;
|
|
186
|
+
const location = publicUrl(next.to, config.websiteOrigin);
|
|
187
|
+
if (!location.startsWith('/')) break;
|
|
188
|
+
if (seen.has(location)) return fail();
|
|
189
|
+
seen.add(location); current = location;
|
|
190
|
+
}
|
|
191
|
+
return result('redirect', { path, status: first.status, location: publicUrl(first.to, config.websiteOrigin) });
|
|
192
|
+
}
|
|
193
|
+
function archives(taxonomies, entries, current, dynamic) {
|
|
194
|
+
if (taxonomies.state !== 'ready') return null;
|
|
195
|
+
const found = [];
|
|
196
|
+
for (const [kind, key, pattern] of [['category', 'categories', taxonomies.data.categoryPath], ['tag', 'tags', taxonomies.data.tagPath]]) {
|
|
197
|
+
for (const term of taxonomies.data[key]) {
|
|
198
|
+
const path = publicPath(pattern.replaceAll('{slug}', term.slug));
|
|
199
|
+
if (!unclaimed(path) || path !== current) continue;
|
|
200
|
+
if (dynamic.has(path)) return fail();
|
|
201
|
+
found.push(result('archive', { path, kind, title: term.name, description: term.description ?? '', entries: entries.filter(entry => entry[key]?.some(value => value.id === term.id) || entry.document.taxonomy?.[key]?.includes(term.id)).filter(entry => dynamic.has(entry.path) || config.routes.some(route => route.kind === entry.kind && route.slug === entry.slug && route.locale === entry.locale)).map(entry => ({ ...entry, path: config.routes.find(route => route.kind === entry.kind && route.slug === entry.slug && route.locale === entry.locale)?.path ?? entry.path })) }));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (found.length > 1) return fail();
|
|
205
|
+
return found[0] ?? null;
|
|
206
|
+
}
|
|
207
|
+
function navigationTarget(value) {
|
|
208
|
+
try {
|
|
209
|
+
const url=new URL(value,config.websiteOrigin);
|
|
210
|
+
if(url.origin!==config.websiteOrigin)return {external:true};
|
|
211
|
+
const path=normalizePublicationPath(url.pathname);
|
|
212
|
+
return {path,native:isNative(path)||isExcludedPublicationPath(path)};
|
|
213
|
+
}catch{return null;}
|
|
214
|
+
}
|
|
215
|
+
async function publishedNavigation(sections,inventory) {
|
|
216
|
+
const navigation=sections.navigation;
|
|
217
|
+
if(navigation.state!=='ready')return;
|
|
218
|
+
const data=navigation.data;
|
|
219
|
+
const links=[...data.header.flatMap(link=>[link,...(link.children??[])]),...data.footer,...(data.footerGroups??[]).flatMap(group=>group.links)];
|
|
220
|
+
// External links, reviewed native routes and intentionally empty menus do
|
|
221
|
+
// not need a content read. Only CMS-controlled destinations are filtered.
|
|
222
|
+
const needsInventory=links.some(link=>{const target=link.url&&navigationTarget(link.url);return target&&!target.external&&!target.native;});
|
|
223
|
+
if(!links.length)return;
|
|
224
|
+
try {
|
|
225
|
+
const {languages,taxonomies,redirects}=sections;
|
|
226
|
+
if(needsInventory&&[languages,taxonomies,redirects].some(section=>section.state==='unavailable'))return fail();
|
|
227
|
+
const entries=needsInventory?(await inventory()).filter(entry=>languages.state!=='ready'||languages.data.enabled.some(locale=>canonicalLocale(locale)===entry.locale)):[];
|
|
228
|
+
const dynamic=paths(entries),available=new Set(dynamic.keys());
|
|
229
|
+
for(const route of config.routes){
|
|
230
|
+
const matches=entries.filter(entry=>entry.kind===route.kind&&entry.slug===route.slug&&entry.locale===route.locale);
|
|
231
|
+
if(matches.length>1)return fail();
|
|
232
|
+
if(matches.length===1){available.add(route.path);if(route.path==='/')available.add('/index.html');}
|
|
233
|
+
}
|
|
234
|
+
// A collision has no valid rendered destination, even if one individual
|
|
235
|
+
// content or taxonomy record otherwise appears published.
|
|
236
|
+
if(needsInventory&&taxonomies.state==='ready'){
|
|
237
|
+
const archivePaths=new Map();
|
|
238
|
+
for(const [terms,pattern] of [[taxonomies.data.categories,taxonomies.data.categoryPath],[taxonomies.data.tags,taxonomies.data.tagPath]])for(const term of terms){
|
|
239
|
+
const path=publicPath(pattern.replaceAll('{slug}',term.slug));
|
|
240
|
+
if(unclaimed(path))archivePaths.set(path,(archivePaths.get(path)??0)+1);
|
|
241
|
+
}
|
|
242
|
+
for(const [path,count] of archivePaths)if(count===1&&!dynamic.has(path))available.add(path);else available.delete(path);
|
|
243
|
+
}
|
|
244
|
+
const keep=(value,seen=new Set())=>{
|
|
245
|
+
const target=navigationTarget(value);
|
|
246
|
+
if(!target)return false;
|
|
247
|
+
if(target.external||target.native)return true;
|
|
248
|
+
if(seen.has(target.path)||seen.size>=500)return false;
|
|
249
|
+
seen.add(target.path);
|
|
250
|
+
const redirected=redirect(target.path,redirects);
|
|
251
|
+
if(redirected?.state==='gone')return false;
|
|
252
|
+
if(redirected?.state==='redirect')return keep(redirected.location,seen);
|
|
253
|
+
return available.has(target.path);
|
|
254
|
+
};
|
|
255
|
+
const header=data.header.flatMap(link=>{
|
|
256
|
+
const children=link.children?.filter(child=>keep(child.url)),allowed=link.url!==undefined&&keep(link.url);
|
|
257
|
+
if(!allowed&&!children?.length)return [];
|
|
258
|
+
return [{label:link.label,...(allowed?{url:link.url}:{}),...(children===undefined?{}:{children})}];
|
|
259
|
+
});
|
|
260
|
+
sections.navigation=state('ready',navigation.version,{...data,header,footer:data.footer.filter(link=>keep(link.url)),...(data.footerGroups===undefined?{}:{footerGroups:data.footerGroups.map(group=>({...group,links:group.links.filter(link=>keep(link.url))})).filter(group=>group.links.length)})});
|
|
261
|
+
}catch{sections.navigation=state('unavailable');}
|
|
262
|
+
}
|
|
263
|
+
const runtime = {
|
|
264
|
+
async settings() {
|
|
265
|
+
try {
|
|
266
|
+
return await operation(async ({ section, inventory }) => {
|
|
267
|
+
const sections = {}; let cursor = 0;
|
|
268
|
+
await Promise.all(Array.from({ length: 4 }, async () => {
|
|
269
|
+
while (cursor < websiteSections.length) { const name = websiteSections[cursor++]; sections[name] = await section(name); }
|
|
270
|
+
}));
|
|
271
|
+
await publishedNavigation(sections,inventory);
|
|
272
|
+
return { version: 1, state: Object.values(sections).some(value => value.state === 'unavailable') ? 'unavailable' : 'ready', sections };
|
|
273
|
+
});
|
|
274
|
+
} catch { return unavailableSnapshot(); }
|
|
275
|
+
},
|
|
276
|
+
async resolve(pathname) {
|
|
277
|
+
let path;
|
|
278
|
+
try { path = normalizePublicationPath(pathname); if (isNative(path) || isExcludedPublicationPath(path)) return result('unmanaged'); } catch { return result('missing'); }
|
|
279
|
+
try {
|
|
280
|
+
return await operation(async ({ section, inventory }) => {
|
|
281
|
+
const redirects = await section('redirects');
|
|
282
|
+
// A failed authorized redirect read must not expose the old content.
|
|
283
|
+
if (redirects.state === 'unavailable') return result('unavailable');
|
|
284
|
+
const redirected = redirect(path, redirects); if (redirected) return redirected;
|
|
285
|
+
if (original.has(path)) return result('unmanaged');
|
|
286
|
+
const languages = await section('languages');
|
|
287
|
+
if (languages.state === 'unavailable') return result('unavailable');
|
|
288
|
+
const entries = (await inventory()).filter(entry => languages.state !== 'ready' || languages.data.enabled.some(locale => canonicalLocale(locale) === entry.locale)), dynamic = paths(entries);
|
|
289
|
+
const taxonomies = await section('taxonomies');
|
|
290
|
+
if (taxonomies.state === 'unavailable') return result('unavailable');
|
|
291
|
+
const archive = archives(taxonomies, entries, path, dynamic); if (archive) return archive;
|
|
292
|
+
const entry = dynamic.get(path);
|
|
293
|
+
if (!entry) return result('missing');
|
|
294
|
+
return result('published', { path, entry, revision: publicationRevision(entry), related: [...dynamic.values()].filter(other => other.id !== entry.id && other.kind === entry.kind && other.locale === entry.locale).slice(0, 6).map(summary) });
|
|
295
|
+
});
|
|
296
|
+
} catch { return result('unavailable'); }
|
|
297
|
+
},
|
|
298
|
+
async page(pathname) {
|
|
299
|
+
let path;
|
|
300
|
+
try { path = normalizePublicationPath(pathname); if (isNative(path) || isExcludedPublicationPath(path)) return result('unmanaged'); } catch { return result('missing'); }
|
|
301
|
+
const route = config.routes.find(route => route.path === (path === '/index.html' ? '/' : path));
|
|
302
|
+
if (!route) return runtime.resolve(path);
|
|
303
|
+
try {
|
|
304
|
+
return await operation(async ({ section, inventory }) => {
|
|
305
|
+
const redirects = await section('redirects');
|
|
306
|
+
if (redirects.state === 'unavailable') return result('unavailable');
|
|
307
|
+
const redirected = redirect(path, redirects); if (redirected) return redirected;
|
|
308
|
+
const entries = await inventory(), matches = entries.filter(entry => entry.kind === route.kind && entry.slug === route.slug && entry.locale === route.locale);
|
|
309
|
+
if (matches.length > 1) return result('unavailable');
|
|
310
|
+
if (!matches.length) return result('missing');
|
|
311
|
+
const entry = { ...matches[0], path: route.path };
|
|
312
|
+
return result('published', { path: route.path, entry, revision: publicationRevision(entry), related: [...paths(entries).values()].filter(other => other.kind === entry.kind && other.locale === entry.locale).slice(0, 6).map(summary) });
|
|
313
|
+
});
|
|
314
|
+
} catch { return result('unavailable'); }
|
|
315
|
+
},
|
|
316
|
+
async sitemap() {
|
|
317
|
+
try {
|
|
318
|
+
return await operation(async ({ section, inventory }) => {
|
|
319
|
+
const setting = await section('seo');
|
|
320
|
+
if (setting.state !== 'ready') return { state: setting.state };
|
|
321
|
+
const seo = setting.data, canonicalBase = seo.canonicalBase || config.websiteOrigin;
|
|
322
|
+
publicUrl(canonicalBase, config.websiteOrigin, { absolute: true });
|
|
323
|
+
if (!seo.sitemapEnabled || !seo.indexable) return { state: 'ready', enabled: false, canonicalBase, entries: [] };
|
|
324
|
+
const languageSetting = await section('languages');
|
|
325
|
+
if (languageSetting.state === 'unavailable') return { state: 'unavailable' };
|
|
326
|
+
const locales = languageSetting.state === 'ready' ? languageSetting.data.enabled.map(canonicalLocale) : null;
|
|
327
|
+
const entries = await inventory(), dynamic = paths(entries), mapped = new Map();
|
|
328
|
+
for (const entry of entries) {
|
|
329
|
+
if (locales && !locales.includes(entry.locale) || entry.seo?.robots.index === false || entry.seo?.canonical && !['', 'self'].includes(entry.seo.canonical)) continue;
|
|
330
|
+
const path = config.routes.find(route => route.kind === entry.kind && route.slug === entry.slug && route.locale === entry.locale)?.path ?? (dynamic.has(entry.path) ? entry.path : null);
|
|
331
|
+
if (!path || isNative(path)) continue;
|
|
332
|
+
if (mapped.has(path)) return fail();
|
|
333
|
+
mapped.set(path, { url: absolutePath(canonicalBase, path), path, locale: entry.locale, lastModified: entry.publishedAt });
|
|
334
|
+
}
|
|
335
|
+
return { state: 'ready', enabled: true, canonicalBase, entries: [...mapped.values()] };
|
|
336
|
+
});
|
|
337
|
+
} catch { return { state: 'unavailable' }; }
|
|
338
|
+
},
|
|
339
|
+
async robots() {
|
|
340
|
+
try {
|
|
341
|
+
return await operation(async ({ section }) => {
|
|
342
|
+
const setting = await section('seo');
|
|
343
|
+
if (setting.state !== 'ready') return { state: setting.state };
|
|
344
|
+
const seo = setting.data;
|
|
345
|
+
if (seo.robotsTxt.trim()) return { state: 'ready', body: `${seo.robotsTxt.trim()}\n` };
|
|
346
|
+
const lines = ['User-agent: *', seo.indexable ? 'Allow: /' : 'Disallow: /', ...seo.robotsDisallow.map(path => `Disallow: ${path}`)];
|
|
347
|
+
if (seo.indexable && seo.sitemapEnabled) lines.push('', `Sitemap: ${absolutePath(seo.canonicalBase || config.websiteOrigin, '/sitemap.xml')}`);
|
|
348
|
+
return { state: 'ready', body: `${lines.join('\n')}\n` };
|
|
349
|
+
});
|
|
350
|
+
} catch { return { state: 'unavailable' }; }
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
return runtime;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function absolutePath(base, pathname) {
|
|
357
|
+
const url = new URL(base);
|
|
358
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) return fail();
|
|
359
|
+
url.pathname = `${url.pathname.replace(/\/+$/, '')}${pathname}`; url.search = ''; url.hash = '';
|
|
360
|
+
return url.href;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Plain public metadata only. Adapters escape attributes/JSON when serializing. */
|
|
364
|
+
export function websiteMetadata(entry, snapshot, pathname, websiteOrigin) {
|
|
365
|
+
const path = publicPath(pathname), base = websiteOrigin === undefined ? undefined : safeOrigin(websiteOrigin);
|
|
366
|
+
const get = name => snapshot?.version === 1 && snapshot.sections?.[name]?.state === 'ready' ? parseWebsiteSection(name, { section: name, version: snapshot.sections[name].version, data: snapshot.sections[name].data }).data : null;
|
|
367
|
+
const seo = get('seo'), templates = get('templates'), authors = get('authors');
|
|
368
|
+
const metadata = parseWebsiteEntryMetadata(entry ?? {}), published = entry?.document;
|
|
369
|
+
const editorial = seo ? metadata.seo : undefined;
|
|
370
|
+
let title = editorial?.title || published?.title || '';
|
|
371
|
+
if (typeof title !== 'string' || title.length > 500) return fail();
|
|
372
|
+
if (seo?.titleTemplate) title = seo.titleTemplate.replace(/%s|%title%|\{title\}/g, () => title);
|
|
373
|
+
const description = editorial?.description || published?.excerpt || seo?.description || '';
|
|
374
|
+
if (typeof description !== 'string' || description.length > 2000) return fail();
|
|
375
|
+
let canonical;
|
|
376
|
+
if (seo) {
|
|
377
|
+
if (editorial?.canonical && !['self', ''].includes(editorial.canonical)) canonical = publicUrl(editorial.canonical, base, { absolute: true });
|
|
378
|
+
else if (seo.canonicalBase || base) canonical = absolutePath(seo.canonicalBase || base, path);
|
|
379
|
+
}
|
|
380
|
+
const robots = { index: seo ? seo.indexable && (editorial?.robots.index ?? true) : false, follow: seo ? seo.followLinks && (editorial?.robots.follow ?? true) : true };
|
|
381
|
+
const output = { title, description, robots, ...(canonical ? { canonical } : {}) };
|
|
382
|
+
if (!seo) return output;
|
|
383
|
+
const og = editorial?.openGraph;
|
|
384
|
+
const image = og?.imageUrl || editorial?.featuredImage.url || seo.socialImageUrl;
|
|
385
|
+
output.openGraph = { title: og?.title || title, description: og?.description || description, type: og?.type || (entry?.kind === 'post' ? 'article' : 'website'), ...(canonical ? { url: canonical } : {}), ...(image ? { image: publicUrl(image, base, { absolute: true, page: false }), imageAlt: og?.imageAlt || editorial?.featuredImage.alt || '' } : {}) };
|
|
386
|
+
output.twitter = { card: editorial?.twitterCard || 'summary_large_image', ...(seo.twitterHandle ? { site: seo.twitterHandle } : {}) };
|
|
387
|
+
const languages = get('languages');
|
|
388
|
+
if (languages && metadata.translations) output.alternates = metadata.translations.filter(value => languages.enabled.some(locale => canonicalLocale(locale) === canonicalLocale(value.locale))).map(value => ({ locale: canonicalLocale(value.locale), url: seo.canonicalBase || base ? absolutePath(seo.canonicalBase || base, publicPath(value.path)) : publicPath(value.path) }));
|
|
389
|
+
const schemaType = editorial && editorial.schema.type !== 'none' ? editorial.schema.type : templates?.[entry?.kind]?.schemaType;
|
|
390
|
+
if (schemaType === 'custom' && editorial?.schema.json) output.jsonLd = editorial.schema.json;
|
|
391
|
+
else if (schemaType && schemaType !== 'none') {
|
|
392
|
+
output.jsonLd = { '@context': 'https://schema.org', '@type': schemaType, name: title, description, ...(canonical ? { url: canonical } : {}), ...(image ? { image } : {}) };
|
|
393
|
+
if (authors && metadata.author) output.jsonLd.author = { '@type': 'Person', name: metadata.author.name, ...(metadata.author.url ? { url: metadata.author.url } : {}) };
|
|
394
|
+
}
|
|
395
|
+
return output;
|
|
396
|
+
}
|