@zeropress/build-core 0.6.13 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +145 -43
- package/package.json +8 -7
- package/src/api/build-site.js +969 -405
- package/src/assets/asset-processor.js +0 -13
- package/src/index.d.ts +43 -11
- package/src/render/content-renderer.js +98 -168
- package/src/render/control-flow-renderer.js +5 -2
- package/src/render/variable-resolver.js +6 -1
- package/src/render/zeropress-engine.js +3 -1
- package/src/theme/format-theme-validation.js +54 -0
- package/src/theme/load-theme-dir.js +134 -68
package/src/api/build-site.js
CHANGED
|
@@ -4,24 +4,29 @@ import { isSafeSlugSegment, normalizeStoredSlug } from '@zeropress/slug-policy';
|
|
|
4
4
|
import { validateThemeFiles } from '@zeropress/theme-validator';
|
|
5
5
|
import { AssetProcessor } from '../assets/asset-processor.js';
|
|
6
6
|
import { renderDocument, renderDocumentContent } from '../render/content-renderer.js';
|
|
7
|
+
import { formatThemeValidationFailure } from '../theme/format-theme-validation.js';
|
|
7
8
|
import { ZeroPressEngine } from '../render/zeropress-engine.js';
|
|
8
9
|
|
|
9
10
|
const DEFAULT_OPTIONS = {
|
|
10
11
|
assetHashing: true,
|
|
11
|
-
generateSpecialFiles: true,
|
|
12
12
|
generateFeed: true,
|
|
13
13
|
generateRobotsTxt: true,
|
|
14
|
-
writeManifest: false,
|
|
15
14
|
};
|
|
16
15
|
|
|
17
16
|
const DEFAULT_POSTS_PER_PAGE = 10;
|
|
18
|
-
const DEFAULT_DATETIME_DISPLAY = 'static';
|
|
19
17
|
const DEFAULT_DATE_STYLE = 'medium';
|
|
20
18
|
const DEFAULT_TIME_STYLE = 'none';
|
|
21
19
|
const DEFAULT_TIMEZONE = 'UTC';
|
|
22
20
|
const DEFAULT_LOCALE = 'en-US';
|
|
23
|
-
const
|
|
21
|
+
const DEFAULT_COMMENTS_PROVIDER = 'zeropress';
|
|
22
|
+
const DEFAULT_COMMENTS_PER_PAGE = 50;
|
|
23
|
+
const DEFAULT_COMMENTS_ORDER = 'desc';
|
|
24
|
+
const DEFAULT_COMMENTS_THREADING_ENABLED = true;
|
|
25
|
+
const DEFAULT_COMMENTS_THREADING_MAX_DEPTH = 2;
|
|
24
26
|
const DATETIME_STYLES = new Set(['none', 'short', 'medium', 'long', 'full']);
|
|
27
|
+
const COMMENTS_PROVIDERS = new Set(['zeropress', 'wordpress']);
|
|
28
|
+
const COMMENTS_ORDERS = new Set(['asc', 'desc']);
|
|
29
|
+
const DISABLED_COMMENTS_CONTEXT = Object.freeze({ enabled: false });
|
|
25
30
|
const DEFAULT_PERMALINKS = Object.freeze({
|
|
26
31
|
output_style: 'directory',
|
|
27
32
|
posts: '/posts/:slug/',
|
|
@@ -43,9 +48,12 @@ const SEARCH_ADAPTER_OUTPUT_PATH = '_zeropress/search.js';
|
|
|
43
48
|
const SEARCH_PAGEFIND_ADAPTER_OUTPUT_PATH = '_zeropress/search_pagefind.js';
|
|
44
49
|
const OUTPUT_PATH_CONTROL_CHAR_PATTERN = /[\u0000-\u001F\u007F]/;
|
|
45
50
|
const SAFE_MEDIA_PROTOCOLS = new Set(['http:', 'https:']);
|
|
46
|
-
const SAFE_LINK_PROTOCOLS = new Set(['http:', 'https:'
|
|
51
|
+
const SAFE_LINK_PROTOCOLS = new Set(['http:', 'https:']);
|
|
47
52
|
const MEDIA_DELIVERY_MODES = new Set(['none', 'media_domain']);
|
|
48
53
|
const DISCOVERABILITY_VALUES = new Set(['default', 'noindex', 'delist']);
|
|
54
|
+
const CUSTOM_HTML_SLOT_MAX_CODE_POINTS = 65_536;
|
|
55
|
+
const HEAD_CLOSING_TAG_PATTERN = /<\/head\s*>/i;
|
|
56
|
+
const BODY_CLOSING_TAG_PATTERN = /<\/body\s*>/i;
|
|
49
57
|
const RESPONSIVE_IMAGE_WIDTHS = [320, 480, 768, 1024, 1280, 1600, 1920];
|
|
50
58
|
const RESPONSIVE_IMAGE_EXTENSIONS = new Set(['jpg', 'jpeg', 'png', 'webp', 'avif']);
|
|
51
59
|
const SEARCH_FIELD_WEIGHTS = Object.freeze({
|
|
@@ -57,6 +65,7 @@ const SEARCH_FIELD_WEIGHTS = Object.freeze({
|
|
|
57
65
|
content_text: 1,
|
|
58
66
|
});
|
|
59
67
|
const SEARCH_RECENCY_BOOST_MAX = 0.15;
|
|
68
|
+
const RFC3339_TIMESTAMP_PATTERN = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:)(60|[0-5]\d)(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
|
|
60
69
|
export async function buildSite(input) {
|
|
61
70
|
const options = { ...DEFAULT_OPTIONS, ...(input.options || {}) };
|
|
62
71
|
const state = await createBuildState(input, options);
|
|
@@ -100,32 +109,32 @@ export async function buildSite(input) {
|
|
|
100
109
|
await writeOutput(state.writer, state.summaries, assetOutput.path, assetOutput.content, assetOutput.contentType);
|
|
101
110
|
}
|
|
102
111
|
|
|
103
|
-
if (shouldGenerateSearchArtifacts(state
|
|
112
|
+
if (shouldGenerateSearchArtifacts(state)) {
|
|
104
113
|
await writeOutput(state.writer, state.summaries, SEARCH_INDEX_OUTPUT_PATH, buildSearchIndexJson(state), 'application/json');
|
|
105
|
-
await writeOutput(state.writer, state.summaries, SEARCH_ADAPTER_OUTPUT_PATH, buildSearchAdapterJs(), 'application/javascript');
|
|
114
|
+
await writeOutput(state.writer, state.summaries, SEARCH_ADAPTER_OUTPUT_PATH, buildSearchAdapterJs(state.previewData.site.locale), 'application/javascript');
|
|
106
115
|
await writeOutput(state.writer, state.summaries, SEARCH_PAGEFIND_ADAPTER_OUTPUT_PATH, buildSearchPagefindAdapterJs(), 'application/javascript');
|
|
107
116
|
}
|
|
108
117
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
await writeOutput(state.writer, state.summaries, 'feed.xml', buildFeedXml(state.previewData.site, state.emitted, state.generatedAt), 'application/rss+xml');
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
if (shouldGenerateRobotsTxt(options)) {
|
|
124
|
-
await writeOutput(state.writer, state.summaries, 'robots.txt', buildRobotsTxt(state.previewData.site), 'text/plain');
|
|
118
|
+
await maybeRenderNotFoundPage(state);
|
|
119
|
+
if (hasCanonicalSiteUrl(state.previewData.site.url)) {
|
|
120
|
+
await writeOutput(
|
|
121
|
+
state.writer,
|
|
122
|
+
state.summaries,
|
|
123
|
+
'sitemap.xml',
|
|
124
|
+
buildSitemapXml(state.previewData.site, state.emitted, options.sitemapStylesheetHref),
|
|
125
|
+
'application/xml',
|
|
126
|
+
);
|
|
127
|
+
if (shouldGenerateFeed(state)) {
|
|
128
|
+
await writeOutput(state.writer, state.summaries, 'feed.xml', buildFeedXml(state.previewData.site, state.emitted, state.feedGeneratedAt), 'application/rss+xml');
|
|
125
129
|
}
|
|
126
130
|
}
|
|
131
|
+
if (shouldGenerateRobotsTxt(options)) {
|
|
132
|
+
await writeOutput(state.writer, state.summaries, 'robots.txt', buildRobotsTxt(state.previewData.site), 'text/plain');
|
|
133
|
+
}
|
|
127
134
|
|
|
128
|
-
return
|
|
135
|
+
return {
|
|
136
|
+
files: state.summaries,
|
|
137
|
+
};
|
|
129
138
|
}
|
|
130
139
|
|
|
131
140
|
async function createBuildState(input, options) {
|
|
@@ -140,12 +149,12 @@ async function createBuildState(input, options) {
|
|
|
140
149
|
const assetProcessor = new AssetProcessor();
|
|
141
150
|
const summaries = [];
|
|
142
151
|
const previewData = normalizePreviewData(input.previewData, options);
|
|
143
|
-
const renderData = createRenderData(previewData, themePackage
|
|
152
|
+
const renderData = createRenderData(previewData, themePackage, options);
|
|
144
153
|
|
|
145
154
|
engine.initialize(themePackage);
|
|
146
155
|
|
|
147
|
-
const assetOutputs =
|
|
148
|
-
const customCssAsset =
|
|
156
|
+
const assetOutputs = buildAssetOutputs(themePackage.assets, assetProcessor, options);
|
|
157
|
+
const customCssAsset = buildCustomCssAsset(previewData.custom_css, assetProcessor, options);
|
|
149
158
|
if (customCssAsset) {
|
|
150
159
|
assetOutputs.push(customCssAsset);
|
|
151
160
|
}
|
|
@@ -168,7 +177,7 @@ async function createBuildState(input, options) {
|
|
|
168
177
|
favicon: previewData.site.favicon,
|
|
169
178
|
exposeGenerator: previewData.site.expose_generator !== false,
|
|
170
179
|
options,
|
|
171
|
-
|
|
180
|
+
feedGeneratedAt: toDate(previewData.generated_at),
|
|
172
181
|
emitted: {
|
|
173
182
|
frontPage: null,
|
|
174
183
|
indexRoutes: [],
|
|
@@ -193,30 +202,9 @@ function assertBuildPreviewData(previewData) {
|
|
|
193
202
|
}
|
|
194
203
|
}
|
|
195
204
|
|
|
196
|
-
async function finalizeBuildResult(writer, summaries, options) {
|
|
197
|
-
let manifest;
|
|
198
|
-
if (options.writeManifest) {
|
|
199
|
-
manifest = {
|
|
200
|
-
generatedAt: formatUtcIsoSeconds(new Date()),
|
|
201
|
-
files: summaries.map((file) => ({ ...file })),
|
|
202
|
-
};
|
|
203
|
-
await writeOutput(
|
|
204
|
-
writer,
|
|
205
|
-
summaries,
|
|
206
|
-
'build-manifest.json',
|
|
207
|
-
JSON.stringify(manifest, null, 2),
|
|
208
|
-
'application/json',
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return {
|
|
213
|
-
files: summaries,
|
|
214
|
-
manifest,
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
|
|
218
205
|
async function renderRoute(state, templateName, route) {
|
|
219
206
|
const currentUrl = routePathToPublicUrl(route.path, state.previewData.site.permalinks.output_style);
|
|
207
|
+
const outputPath = routePathToOutputPath(route.path, state.previewData.site.permalinks.output_style);
|
|
220
208
|
const routeContext = buildRouteContext(route.route_type || templateName, currentUrl, {
|
|
221
209
|
isFrontPage: route.is_front_page === true,
|
|
222
210
|
isPostIndex: route.is_post_index === true,
|
|
@@ -242,8 +230,11 @@ async function renderRoute(state, templateName, route) {
|
|
|
242
230
|
createRenderContext(state.previewData.site, currentUrl),
|
|
243
231
|
);
|
|
244
232
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
245
|
-
html = injectSiteCustomizations(html, state
|
|
246
|
-
|
|
233
|
+
html = injectSiteCustomizations(html, state, {
|
|
234
|
+
route: currentUrl,
|
|
235
|
+
outputPath,
|
|
236
|
+
});
|
|
237
|
+
await writeOutput(state.writer, state.summaries, outputPath, html, 'text/html');
|
|
247
238
|
recordRouteEmission(state, templateName, route, currentUrl);
|
|
248
239
|
}
|
|
249
240
|
|
|
@@ -288,10 +279,17 @@ async function renderFrontPage(state, route) {
|
|
|
288
279
|
robotsNoindex: shouldNoindexDocument(page),
|
|
289
280
|
}),
|
|
290
281
|
},
|
|
291
|
-
createRenderContext(
|
|
282
|
+
createRenderContext(
|
|
283
|
+
state.previewData.site,
|
|
284
|
+
currentUrl,
|
|
285
|
+
getTargetCommentsContext(state, 'page', state.renderData.pageReferencePathByPage.get(route.page)),
|
|
286
|
+
),
|
|
292
287
|
);
|
|
293
288
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
294
|
-
html = injectSiteCustomizations(html, state
|
|
289
|
+
html = injectSiteCustomizations(html, state, {
|
|
290
|
+
route: currentUrl,
|
|
291
|
+
outputPath: 'index.html',
|
|
292
|
+
});
|
|
295
293
|
await writeOutput(state.writer, state.summaries, 'index.html', html, 'text/html');
|
|
296
294
|
state.emitted.frontPage = {
|
|
297
295
|
url: currentUrl,
|
|
@@ -323,7 +321,10 @@ async function renderFrontPage(state, route) {
|
|
|
323
321
|
createRenderContext(state.previewData.site, currentUrl),
|
|
324
322
|
);
|
|
325
323
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
326
|
-
html = injectSiteCustomizations(html, state
|
|
324
|
+
html = injectSiteCustomizations(html, state, {
|
|
325
|
+
route: currentUrl,
|
|
326
|
+
outputPath: 'index.html',
|
|
327
|
+
});
|
|
327
328
|
await writeOutput(state.writer, state.summaries, 'index.html', html, 'text/html');
|
|
328
329
|
state.emitted.frontPage = {
|
|
329
330
|
url: currentUrl,
|
|
@@ -335,6 +336,7 @@ async function renderFrontPage(state, route) {
|
|
|
335
336
|
|
|
336
337
|
async function renderPost(state, post) {
|
|
337
338
|
const currentUrl = post.url;
|
|
339
|
+
const outputPath = routePathToOutputPath(post.url, state.previewData.site.permalinks.output_style);
|
|
338
340
|
let html = await state.engine.render(
|
|
339
341
|
'post',
|
|
340
342
|
{
|
|
@@ -355,11 +357,18 @@ async function renderPost(state, post) {
|
|
|
355
357
|
robotsNoindex: shouldNoindexDocument(post),
|
|
356
358
|
}),
|
|
357
359
|
},
|
|
358
|
-
createRenderContext(
|
|
360
|
+
createRenderContext(
|
|
361
|
+
state.previewData.site,
|
|
362
|
+
currentUrl,
|
|
363
|
+
getTargetCommentsContext(state, 'post', post.slug),
|
|
364
|
+
),
|
|
359
365
|
);
|
|
360
366
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
361
|
-
html = injectSiteCustomizations(html, state
|
|
362
|
-
|
|
367
|
+
html = injectSiteCustomizations(html, state, {
|
|
368
|
+
route: currentUrl,
|
|
369
|
+
outputPath,
|
|
370
|
+
});
|
|
371
|
+
await writeOutput(state.writer, state.summaries, outputPath, html, 'text/html');
|
|
363
372
|
if (!isDelistedDocument(post)) {
|
|
364
373
|
state.emitted.posts.push({
|
|
365
374
|
url: currentUrl,
|
|
@@ -395,10 +404,17 @@ async function renderPage(state, page) {
|
|
|
395
404
|
robotsNoindex: shouldNoindexDocument(page),
|
|
396
405
|
}),
|
|
397
406
|
},
|
|
398
|
-
createRenderContext(
|
|
407
|
+
createRenderContext(
|
|
408
|
+
state.previewData.site,
|
|
409
|
+
currentUrl,
|
|
410
|
+
getTargetCommentsContext(state, 'page', state.renderData.pageReferencePathByPage.get(page)),
|
|
411
|
+
),
|
|
399
412
|
);
|
|
400
413
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
401
|
-
html = injectSiteCustomizations(html, state
|
|
414
|
+
html = injectSiteCustomizations(html, state, {
|
|
415
|
+
route: currentUrl,
|
|
416
|
+
outputPath,
|
|
417
|
+
});
|
|
402
418
|
await writeOutput(state.writer, state.summaries, outputPath, html, 'text/html');
|
|
403
419
|
state.emitted.pages.push({
|
|
404
420
|
url: currentUrl,
|
|
@@ -425,53 +441,72 @@ async function maybeRenderNotFoundPage(state) {
|
|
|
425
441
|
route: buildRouteContext('not_found', '/404.html'),
|
|
426
442
|
meta: buildPageMeta(state.previewData.site, {
|
|
427
443
|
currentUrl: '/404.html',
|
|
428
|
-
title: state.previewData.site.title,
|
|
429
|
-
|
|
430
|
-
|
|
444
|
+
title: buildDocumentTitle('Page Not Found', state.previewData.site.title),
|
|
445
|
+
robotsNoindex: true,
|
|
446
|
+
includeRichMetadata: false,
|
|
447
|
+
includeFeedLink: false,
|
|
431
448
|
}),
|
|
432
449
|
},
|
|
433
450
|
createRenderContext(state.previewData.site, '/404.html'),
|
|
434
451
|
);
|
|
435
452
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
436
|
-
html = injectSiteCustomizations(html, state
|
|
453
|
+
html = injectSiteCustomizations(html, state, {
|
|
454
|
+
route: '/404.html',
|
|
455
|
+
outputPath: '404.html',
|
|
456
|
+
});
|
|
437
457
|
await writeOutput(state.writer, state.summaries, '404.html', html, 'text/html');
|
|
438
458
|
}
|
|
439
459
|
|
|
440
460
|
function normalizePreviewData(previewData, options = {}) {
|
|
441
|
-
const
|
|
461
|
+
const media_origin = normalizeMediaOrigin(previewData.site.media_origin);
|
|
462
|
+
const mediaDeliveryMode = MEDIA_DELIVERY_MODES.has(previewData.site.media_delivery_mode)
|
|
463
|
+
? previewData.site.media_delivery_mode
|
|
464
|
+
: 'none';
|
|
465
|
+
if (mediaDeliveryMode === 'media_domain' && !media_origin) {
|
|
466
|
+
throw new Error('Invalid preview-data: site.media_origin must be a non-empty HTTP(S) origin when site.media_delivery_mode is "media_domain".');
|
|
467
|
+
}
|
|
468
|
+
const {
|
|
469
|
+
search: siteSearch,
|
|
470
|
+
feed: siteFeed,
|
|
471
|
+
archive: siteArchive,
|
|
472
|
+
comments: siteComments,
|
|
473
|
+
robots: siteRobots,
|
|
474
|
+
...siteFields
|
|
475
|
+
} = previewData.site;
|
|
476
|
+
const normalizedComments = normalizeSiteComments(siteComments);
|
|
442
477
|
const normalizedSite = {
|
|
443
|
-
...
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
: 'none',
|
|
478
|
+
...siteFields,
|
|
479
|
+
url: normalizeSiteOrigin(previewData.site.url),
|
|
480
|
+
media_origin,
|
|
481
|
+
media_delivery_mode: mediaDeliveryMode,
|
|
448
482
|
favicon: previewData.site.favicon
|
|
449
|
-
? normalizeSiteFavicon(previewData.site.favicon,
|
|
483
|
+
? normalizeSiteFavicon(previewData.site.favicon, media_origin)
|
|
450
484
|
: normalizeSiteFavicon(options.favicon, ''),
|
|
451
|
-
logo: normalizeSiteLogo(previewData.site.logo,
|
|
485
|
+
logo: normalizeSiteLogo(previewData.site.logo, media_origin),
|
|
452
486
|
newsletter: normalizeSiteNewsletter(previewData.site.newsletter),
|
|
453
487
|
posts_per_page: Number.isInteger(previewData.site.posts_per_page) && previewData.site.posts_per_page > 0
|
|
454
488
|
? previewData.site.posts_per_page
|
|
455
489
|
: DEFAULT_POSTS_PER_PAGE,
|
|
456
|
-
datetime_display: DATETIME_DISPLAY_MODES.has(previewData.site.datetime_display)
|
|
457
|
-
? previewData.site.datetime_display
|
|
458
|
-
: DEFAULT_DATETIME_DISPLAY,
|
|
459
490
|
date_style: DATETIME_STYLES.has(previewData.site.date_style)
|
|
460
491
|
? previewData.site.date_style
|
|
461
492
|
: DEFAULT_DATE_STYLE,
|
|
462
493
|
time_style: DATETIME_STYLES.has(previewData.site.time_style)
|
|
463
494
|
? previewData.site.time_style
|
|
464
495
|
: DEFAULT_TIME_STYLE,
|
|
465
|
-
timezone:
|
|
466
|
-
locale: normalizeLocale(previewData.site.locale
|
|
467
|
-
disallow_comments: previewData.site.disallow_comments === true,
|
|
496
|
+
timezone: normalizeTimezone(previewData.site.timezone),
|
|
497
|
+
locale: normalizeLocale(previewData.site.locale),
|
|
468
498
|
expose_generator: previewData.site.expose_generator !== false,
|
|
469
|
-
search:
|
|
470
|
-
|
|
499
|
+
search: normalizeRequestedFeatureState(siteSearch),
|
|
500
|
+
feed: normalizeRequestedFeatureState(siteFeed),
|
|
501
|
+
archive: normalizeRequestedFeatureState(siteArchive),
|
|
502
|
+
robots: {
|
|
503
|
+
allow_indexing: siteRobots?.allow_indexing !== false,
|
|
504
|
+
},
|
|
471
505
|
permalinks: normalizePermalinks(previewData.site.permalinks),
|
|
472
506
|
front_page: normalizeFrontPage(previewData.site.front_page),
|
|
473
507
|
post_index: normalizePostIndex(previewData.site.post_index),
|
|
474
508
|
footer: normalizeSiteFooter(previewData.site.footer),
|
|
509
|
+
...(normalizedComments ? { comments: normalizedComments } : {}),
|
|
475
510
|
};
|
|
476
511
|
const media = normalizeContentMedia(previewData.content.media, normalizedSite);
|
|
477
512
|
const mediaRegistry = buildMediaRegistry(media);
|
|
@@ -479,15 +514,15 @@ function normalizePreviewData(previewData, options = {}) {
|
|
|
479
514
|
return {
|
|
480
515
|
...previewData,
|
|
481
516
|
site: normalizedSite,
|
|
482
|
-
menus:
|
|
517
|
+
menus: normalizeMenus(previewData.menus),
|
|
483
518
|
collections: normalizeCollections(previewData.collections),
|
|
484
|
-
widgets: normalizeWidgetAreas(previewData.widgets, normalizedSite.
|
|
519
|
+
widgets: normalizeWidgetAreas(previewData.widgets, normalizedSite.media_origin),
|
|
485
520
|
custom_css: normalizeCustomCss(previewData.custom_css),
|
|
486
521
|
custom_html: normalizeCustomHtml(previewData.custom_html),
|
|
487
522
|
content: {
|
|
488
523
|
...previewData.content,
|
|
489
524
|
authors: previewData.content.authors.map((author) => {
|
|
490
|
-
const avatar = normalizeMediaField(author.avatar, normalizedSite.
|
|
525
|
+
const avatar = normalizeMediaField(author.avatar, normalizedSite.media_origin);
|
|
491
526
|
const avatarMedia = deriveManagedMedia(avatar, mediaRegistry, normalizedSite);
|
|
492
527
|
return {
|
|
493
528
|
...author,
|
|
@@ -497,27 +532,35 @@ function normalizePreviewData(previewData, options = {}) {
|
|
|
497
532
|
}),
|
|
498
533
|
posts: previewData.content.posts
|
|
499
534
|
.map((post) => {
|
|
500
|
-
const
|
|
535
|
+
const { comments, ...postFields } = post;
|
|
536
|
+
const normalizedPostComments = normalizeTargetComments(comments);
|
|
537
|
+
const featuredImage = normalizeMediaField(post.featured_image, normalizedSite.media_origin);
|
|
501
538
|
const featuredMedia = deriveManagedMedia(featuredImage, mediaRegistry, normalizedSite);
|
|
502
539
|
return {
|
|
503
|
-
...
|
|
540
|
+
...postFields,
|
|
504
541
|
published_at_iso: normalizeIsoTimestamp(post.published_at_iso),
|
|
505
542
|
updated_at_iso: normalizeIsoTimestamp(post.updated_at_iso),
|
|
543
|
+
allow_comments: post.allow_comments === true,
|
|
506
544
|
discoverability: normalizeDiscoverability(post.discoverability),
|
|
507
545
|
featured_image: featuredImage,
|
|
508
546
|
...(featuredMedia ? { featured_media: featuredMedia } : {}),
|
|
547
|
+
...(normalizedPostComments ? { comments: normalizedPostComments } : {}),
|
|
509
548
|
};
|
|
510
549
|
})
|
|
511
550
|
.sort((left, right) => toDate(right.published_at_iso).getTime() - toDate(left.published_at_iso).getTime()),
|
|
512
551
|
pages: previewData.content.pages.map((page) => {
|
|
513
|
-
const
|
|
552
|
+
const { comments, ...pageFields } = page;
|
|
553
|
+
const normalizedPageComments = normalizeTargetComments(comments);
|
|
554
|
+
const featuredImage = normalizeMediaField(page.featured_image, normalizedSite.media_origin);
|
|
514
555
|
const featuredMedia = deriveManagedMedia(featuredImage, mediaRegistry, normalizedSite);
|
|
515
556
|
return {
|
|
516
|
-
...
|
|
557
|
+
...pageFields,
|
|
517
558
|
...(page.updated_at_iso ? { updated_at_iso: normalizeIsoTimestamp(page.updated_at_iso) } : {}),
|
|
559
|
+
allow_comments: page.allow_comments === true,
|
|
518
560
|
discoverability: normalizeDiscoverability(page.discoverability),
|
|
519
561
|
featured_image: featuredImage,
|
|
520
562
|
...(featuredMedia ? { featured_media: featuredMedia } : {}),
|
|
563
|
+
...(normalizedPageComments ? { comments: normalizedPageComments } : {}),
|
|
521
564
|
};
|
|
522
565
|
}),
|
|
523
566
|
categories: [...previewData.content.categories],
|
|
@@ -535,6 +578,22 @@ function normalizeRecordMap(value) {
|
|
|
535
578
|
return { ...value };
|
|
536
579
|
}
|
|
537
580
|
|
|
581
|
+
function normalizeMenus(menus) {
|
|
582
|
+
const normalized = normalizeRecordMap(menus);
|
|
583
|
+
return Object.fromEntries(Object.entries(normalized).map(([menuId, menu]) => [
|
|
584
|
+
menuId,
|
|
585
|
+
{
|
|
586
|
+
...menu,
|
|
587
|
+
items: Array.isArray(menu?.items)
|
|
588
|
+
? menu.items.map((item) => ({
|
|
589
|
+
...item,
|
|
590
|
+
url: normalizeNavigationUrl(item?.url),
|
|
591
|
+
}))
|
|
592
|
+
: [],
|
|
593
|
+
},
|
|
594
|
+
]));
|
|
595
|
+
}
|
|
596
|
+
|
|
538
597
|
function normalizeDiscoverability(value) {
|
|
539
598
|
return DISCOVERABILITY_VALUES.has(value) ? value : 'default';
|
|
540
599
|
}
|
|
@@ -557,7 +616,7 @@ function normalizeContentMedia(mediaItems, site) {
|
|
|
557
616
|
if (!item || typeof item !== 'object') {
|
|
558
617
|
return null;
|
|
559
618
|
}
|
|
560
|
-
const src = normalizeMediaField(item.src, site.
|
|
619
|
+
const src = normalizeMediaField(item.src, site.media_origin);
|
|
561
620
|
const width = Number.isInteger(item.width) && item.width > 0 ? item.width : 0;
|
|
562
621
|
const height = Number.isInteger(item.height) && item.height > 0 ? item.height : 0;
|
|
563
622
|
if (!src || !width || !height) {
|
|
@@ -603,8 +662,8 @@ function buildResponsiveImageSrcset(media, site) {
|
|
|
603
662
|
return '';
|
|
604
663
|
}
|
|
605
664
|
|
|
606
|
-
const
|
|
607
|
-
if (!
|
|
665
|
+
const media_origin = normalizeMediaOrigin(site.media_origin);
|
|
666
|
+
if (!media_origin || !isUrlAtMediaOrigin(media.src, media_origin) || !isResponsiveRasterImage(media.src)) {
|
|
608
667
|
return '';
|
|
609
668
|
}
|
|
610
669
|
|
|
@@ -631,12 +690,10 @@ function buildResponsiveImageVariantUrl(src, width) {
|
|
|
631
690
|
}
|
|
632
691
|
}
|
|
633
692
|
|
|
634
|
-
function
|
|
693
|
+
function isUrlAtMediaOrigin(src, media_origin) {
|
|
635
694
|
try {
|
|
636
695
|
const sourceUrl = new URL(src);
|
|
637
|
-
|
|
638
|
-
const basePath = baseUrl.pathname.endsWith('/') ? baseUrl.pathname : `${baseUrl.pathname}/`;
|
|
639
|
-
return sourceUrl.origin === baseUrl.origin && sourceUrl.pathname.startsWith(basePath);
|
|
696
|
+
return sourceUrl.origin === media_origin;
|
|
640
697
|
} catch {
|
|
641
698
|
return false;
|
|
642
699
|
}
|
|
@@ -671,7 +728,7 @@ function normalizeCollections(collections) {
|
|
|
671
728
|
);
|
|
672
729
|
}
|
|
673
730
|
|
|
674
|
-
function normalizeWidgetAreas(widget_areas,
|
|
731
|
+
function normalizeWidgetAreas(widget_areas, media_origin) {
|
|
675
732
|
if (!widget_areas || typeof widget_areas !== 'object') {
|
|
676
733
|
return {};
|
|
677
734
|
}
|
|
@@ -683,7 +740,7 @@ function normalizeWidgetAreas(widget_areas, media_base_url) {
|
|
|
683
740
|
...widgetArea,
|
|
684
741
|
name: normalizeNonEmptyString(widgetArea?.name, widgetAreaId),
|
|
685
742
|
items: Array.isArray(widgetArea?.items)
|
|
686
|
-
? widgetArea.items.map((item) => normalizeWidgetItem(item,
|
|
743
|
+
? widgetArea.items.map((item) => normalizeWidgetItem(item, media_origin))
|
|
687
744
|
: [],
|
|
688
745
|
},
|
|
689
746
|
]),
|
|
@@ -700,7 +757,7 @@ function normalizeSiteFooter(footer) {
|
|
|
700
757
|
};
|
|
701
758
|
}
|
|
702
759
|
|
|
703
|
-
function normalizeWidgetItem(item,
|
|
760
|
+
function normalizeWidgetItem(item, media_origin) {
|
|
704
761
|
const normalizedItem = {
|
|
705
762
|
...item,
|
|
706
763
|
title: typeof item?.title === 'string' ? item.title.trim() : '',
|
|
@@ -711,7 +768,7 @@ function normalizeWidgetItem(item, media_base_url) {
|
|
|
711
768
|
...normalizedItem,
|
|
712
769
|
settings: {
|
|
713
770
|
...item.settings,
|
|
714
|
-
avatar: normalizeMediaField(item.settings.avatar,
|
|
771
|
+
avatar: normalizeMediaField(item.settings.avatar, media_origin),
|
|
715
772
|
},
|
|
716
773
|
};
|
|
717
774
|
}
|
|
@@ -720,49 +777,57 @@ function normalizeWidgetItem(item, media_base_url) {
|
|
|
720
777
|
}
|
|
721
778
|
|
|
722
779
|
function normalizeCustomCss(customCss) {
|
|
723
|
-
const content =
|
|
724
|
-
return content ? { content } : undefined;
|
|
780
|
+
const content = customCss?.content;
|
|
781
|
+
return typeof content === 'string' && content.trim() ? { content } : undefined;
|
|
725
782
|
}
|
|
726
783
|
|
|
727
784
|
function normalizeCustomHtml(customHtml) {
|
|
728
|
-
if (!customHtml || typeof customHtml !== 'object') {
|
|
785
|
+
if (!customHtml || typeof customHtml !== 'object' || Array.isArray(customHtml)) {
|
|
729
786
|
return undefined;
|
|
730
787
|
}
|
|
731
788
|
|
|
732
|
-
const headEnd =
|
|
733
|
-
const bodyEnd =
|
|
789
|
+
const headEnd = normalizeCustomHtmlSlot(customHtml.head_end);
|
|
790
|
+
const bodyEnd = normalizeCustomHtmlSlot(customHtml.body_end);
|
|
734
791
|
if (!headEnd && !bodyEnd) {
|
|
735
792
|
return undefined;
|
|
736
793
|
}
|
|
737
794
|
|
|
738
795
|
return {
|
|
739
|
-
...(headEnd ? { head_end:
|
|
740
|
-
...(bodyEnd ? { body_end:
|
|
796
|
+
...(headEnd ? { head_end: headEnd } : {}),
|
|
797
|
+
...(bodyEnd ? { body_end: bodyEnd } : {}),
|
|
741
798
|
};
|
|
742
799
|
}
|
|
743
800
|
|
|
744
|
-
function
|
|
801
|
+
function normalizeCustomHtmlSlot(value) {
|
|
802
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
803
|
+
return '';
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
return [...value].length <= CUSTOM_HTML_SLOT_MAX_CODE_POINTS ? value : '';
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function normalizeSiteFavicon(favicon, media_origin) {
|
|
745
810
|
if (!favicon || typeof favicon !== 'object') {
|
|
746
811
|
return undefined;
|
|
747
812
|
}
|
|
748
813
|
|
|
749
814
|
const normalized = {};
|
|
750
|
-
for (const key of ['icon', 'svg', 'png', 'apple_touch_icon']) {
|
|
815
|
+
for (const key of ['icon', 'icon_dark', 'svg', 'png', 'apple_touch_icon']) {
|
|
751
816
|
const value = normalizeOptionalString(favicon[key]);
|
|
752
817
|
if (value) {
|
|
753
|
-
normalized[key] = normalizeMediaField(value,
|
|
818
|
+
normalized[key] = normalizeMediaField(value, media_origin);
|
|
754
819
|
}
|
|
755
820
|
}
|
|
756
821
|
|
|
757
822
|
return Object.keys(normalized).length ? normalized : undefined;
|
|
758
823
|
}
|
|
759
824
|
|
|
760
|
-
function normalizeSiteLogo(logo,
|
|
825
|
+
function normalizeSiteLogo(logo, media_origin) {
|
|
761
826
|
if (!logo || typeof logo !== 'object') {
|
|
762
827
|
return undefined;
|
|
763
828
|
}
|
|
764
829
|
|
|
765
|
-
const src = normalizeMediaField(logo.src,
|
|
830
|
+
const src = normalizeMediaField(logo.src, media_origin);
|
|
766
831
|
if (!src) {
|
|
767
832
|
return undefined;
|
|
768
833
|
}
|
|
@@ -782,8 +847,8 @@ function normalizeSiteNewsletter(newsletter) {
|
|
|
782
847
|
const title = normalizeOptionalString(newsletter.title);
|
|
783
848
|
const description = normalizeOptionalString(newsletter.description);
|
|
784
849
|
const buttonLabel = normalizeOptionalString(newsletter.button_label);
|
|
785
|
-
const signupUrl =
|
|
786
|
-
const embedUrl =
|
|
850
|
+
const signupUrl = normalizeNavigationUrl(newsletter.signup_url);
|
|
851
|
+
const embedUrl = normalizeNavigationUrl(newsletter.embed_url);
|
|
787
852
|
|
|
788
853
|
return {
|
|
789
854
|
enabled: newsletter.enabled === true,
|
|
@@ -795,6 +860,66 @@ function normalizeSiteNewsletter(newsletter) {
|
|
|
795
860
|
};
|
|
796
861
|
}
|
|
797
862
|
|
|
863
|
+
function normalizeSiteComments(comments) {
|
|
864
|
+
if (!comments || typeof comments !== 'object' || Array.isArray(comments)) {
|
|
865
|
+
return undefined;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const apiBaseUrl = normalizeCommentsApiBaseUrl(comments.api_base_url);
|
|
869
|
+
if (!apiBaseUrl) {
|
|
870
|
+
return undefined;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
const threading = comments.threading && typeof comments.threading === 'object' && !Array.isArray(comments.threading)
|
|
874
|
+
? comments.threading
|
|
875
|
+
: {};
|
|
876
|
+
|
|
877
|
+
return {
|
|
878
|
+
enabled: comments.enabled === true,
|
|
879
|
+
provider: COMMENTS_PROVIDERS.has(comments.provider)
|
|
880
|
+
? comments.provider
|
|
881
|
+
: DEFAULT_COMMENTS_PROVIDER,
|
|
882
|
+
api_base_url: apiBaseUrl,
|
|
883
|
+
per_page: Number.isInteger(comments.per_page) && comments.per_page >= 1 && comments.per_page <= 100
|
|
884
|
+
? comments.per_page
|
|
885
|
+
: DEFAULT_COMMENTS_PER_PAGE,
|
|
886
|
+
order: COMMENTS_ORDERS.has(comments.order)
|
|
887
|
+
? comments.order
|
|
888
|
+
: DEFAULT_COMMENTS_ORDER,
|
|
889
|
+
threading: {
|
|
890
|
+
enabled: typeof threading.enabled === 'boolean'
|
|
891
|
+
? threading.enabled
|
|
892
|
+
: DEFAULT_COMMENTS_THREADING_ENABLED,
|
|
893
|
+
max_depth: Number.isInteger(threading.max_depth) && threading.max_depth >= 2 && threading.max_depth <= 10
|
|
894
|
+
? threading.max_depth
|
|
895
|
+
: DEFAULT_COMMENTS_THREADING_MAX_DEPTH,
|
|
896
|
+
},
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function normalizeRequestedFeatureState(value) {
|
|
901
|
+
return {
|
|
902
|
+
enabled: value?.enabled !== false,
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function normalizeCommentsApiBaseUrl(value) {
|
|
907
|
+
const normalized = normalizeOptionalString(value);
|
|
908
|
+
if (!normalized || normalized === '/') {
|
|
909
|
+
return normalized;
|
|
910
|
+
}
|
|
911
|
+
return normalized.replace(/\/+$/u, '');
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function normalizeTargetComments(comments) {
|
|
915
|
+
if (!comments || typeof comments !== 'object' || Array.isArray(comments)) {
|
|
916
|
+
return undefined;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
const requestToken = preserveOpaqueNonBlankString(comments.request_token);
|
|
920
|
+
return requestToken ? { request_token: requestToken } : undefined;
|
|
921
|
+
}
|
|
922
|
+
|
|
798
923
|
function normalizePermalinks(permalinks) {
|
|
799
924
|
const source = permalinks && typeof permalinks === 'object' ? permalinks : {};
|
|
800
925
|
const outputStyle = typeof source.output_style === 'string' && PERMALINK_OUTPUT_STYLES.has(source.output_style)
|
|
@@ -821,7 +946,7 @@ function normalizeFrontPage(frontPage) {
|
|
|
821
946
|
|
|
822
947
|
return {
|
|
823
948
|
type,
|
|
824
|
-
...(type === 'page' ? {
|
|
949
|
+
...(type === 'page' ? { page_path: normalizePageReferencePath(frontPage.page_path) } : {}),
|
|
825
950
|
...(type === 'standalone_html' ? { html: normalizeOptionalRawString(frontPage.html) } : {}),
|
|
826
951
|
};
|
|
827
952
|
}
|
|
@@ -838,11 +963,31 @@ function normalizePostIndex(post_index) {
|
|
|
838
963
|
};
|
|
839
964
|
}
|
|
840
965
|
|
|
841
|
-
function createRenderData(previewData,
|
|
966
|
+
function createRenderData(previewData, themePackage = {}, options = {}) {
|
|
967
|
+
const themeMetadata = themePackage.metadata || {};
|
|
842
968
|
const themeSupportsComments = themeMetadata?.features?.comments === true;
|
|
843
969
|
const themeSupportsPostIndex = themeMetadata?.features?.post_index !== false;
|
|
844
970
|
const themeSupportsSearch = themeMetadata?.features?.search === true;
|
|
845
|
-
|
|
971
|
+
const outputStyle = previewData.site.permalinks.output_style;
|
|
972
|
+
const requestedComments = previewData.site.comments;
|
|
973
|
+
previewData.site.search = {
|
|
974
|
+
enabled: previewData.site.search.enabled === true && themeSupportsSearch,
|
|
975
|
+
};
|
|
976
|
+
previewData.site.feed = previewData.site.feed.enabled === true
|
|
977
|
+
&& hasCanonicalSiteUrl(previewData.site.url)
|
|
978
|
+
&& options.generateFeed !== false
|
|
979
|
+
? { enabled: true, url: '/feed.xml' }
|
|
980
|
+
: { enabled: false };
|
|
981
|
+
previewData.site.archive = previewData.site.archive.enabled === true
|
|
982
|
+
&& themePackage.templates?.has('archive') === true
|
|
983
|
+
? {
|
|
984
|
+
enabled: true,
|
|
985
|
+
url: outputStyle === 'html-extension' ? '/archive' : '/archive/',
|
|
986
|
+
}
|
|
987
|
+
: { enabled: false };
|
|
988
|
+
previewData.site.comments = requestedComments?.enabled === true && themeSupportsComments
|
|
989
|
+
? { ...requestedComments, enabled: true }
|
|
990
|
+
: { enabled: false };
|
|
846
991
|
const authorsById = new Map(previewData.content.authors.map((author) => [author.id, author]));
|
|
847
992
|
const categoriesBySlug = new Map(previewData.content.categories.map((category) => [category.slug, category]));
|
|
848
993
|
const tagsBySlug = new Map(previewData.content.tags.map((tag) => [tag.slug, tag]));
|
|
@@ -864,7 +1009,38 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
864
1009
|
}
|
|
865
1010
|
}
|
|
866
1011
|
|
|
867
|
-
const
|
|
1012
|
+
const rawPageReferencePaths = new Map(previewData.content.pages.map((page) => [
|
|
1013
|
+
page,
|
|
1014
|
+
resolveEffectivePageReferencePath(previewData.site, page),
|
|
1015
|
+
]));
|
|
1016
|
+
assertUniquePageReferencePaths(rawPageReferencePaths);
|
|
1017
|
+
const commentsByTarget = {
|
|
1018
|
+
posts: new Map(previewData.content.posts.map((post) => [
|
|
1019
|
+
post.slug,
|
|
1020
|
+
buildTargetCommentsContext({
|
|
1021
|
+
site: previewData.site,
|
|
1022
|
+
target: post,
|
|
1023
|
+
targetType: 'post',
|
|
1024
|
+
themeSupportsComments,
|
|
1025
|
+
}),
|
|
1026
|
+
])),
|
|
1027
|
+
pages: new Map(previewData.content.pages.map((page) => [
|
|
1028
|
+
rawPageReferencePaths.get(page),
|
|
1029
|
+
buildTargetCommentsContext({
|
|
1030
|
+
site: previewData.site,
|
|
1031
|
+
target: page,
|
|
1032
|
+
targetType: 'page',
|
|
1033
|
+
themeSupportsComments,
|
|
1034
|
+
}),
|
|
1035
|
+
])),
|
|
1036
|
+
};
|
|
1037
|
+
for (const target of previewData.content.posts) {
|
|
1038
|
+
delete target.comments;
|
|
1039
|
+
}
|
|
1040
|
+
for (const target of previewData.content.pages) {
|
|
1041
|
+
delete target.comments;
|
|
1042
|
+
}
|
|
1043
|
+
const preparedPosts = previewData.content.posts.map((post) => preparePost(post, previewData.site, authorsById, categoriesBySlug, tagsBySlug));
|
|
868
1044
|
const discoverablePreparedPosts = preparedPosts.filter((post) => !isDelistedDocument(post));
|
|
869
1045
|
const adjacentPostsBySlug = new Map(
|
|
870
1046
|
discoverablePreparedPosts.map((post, index) => [post.slug, {
|
|
@@ -878,30 +1054,50 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
878
1054
|
next: adjacentPostsBySlug.get(post.slug)?.next || null,
|
|
879
1055
|
}));
|
|
880
1056
|
const pages = previewData.content.pages.map((page) => preparePage(page, previewData.site));
|
|
1057
|
+
const pageReferencePathByPage = new Map(pages.map((page, index) => [
|
|
1058
|
+
page,
|
|
1059
|
+
rawPageReferencePaths.get(previewData.content.pages[index]),
|
|
1060
|
+
]));
|
|
881
1061
|
const postBySlug = new Map(posts.map((post) => [post.slug, post]));
|
|
882
|
-
const
|
|
1062
|
+
const pageByPath = new Map(pages.map((page) => [pageReferencePathByPage.get(page), page]));
|
|
883
1063
|
const frontPage = previewData.site.front_page;
|
|
884
|
-
const
|
|
885
|
-
|
|
1064
|
+
const collectionTargetByItem = new WeakMap();
|
|
1065
|
+
const collections = resolveCollections(
|
|
1066
|
+
previewData.collections,
|
|
1067
|
+
postBySlug,
|
|
1068
|
+
pageByPath,
|
|
1069
|
+
frontPage,
|
|
1070
|
+
pageReferencePathByPage,
|
|
1071
|
+
collectionTargetByItem,
|
|
1072
|
+
);
|
|
1073
|
+
attachCollectionCursors(collections, collectionTargetByItem);
|
|
886
1074
|
const post_index = previewData.site.post_index;
|
|
887
1075
|
const effectivePostIndexEnabled = post_index.enabled !== false && themeSupportsPostIndex;
|
|
888
1076
|
const effectivePostIndexPaginate = effectivePostIndexEnabled && post_index.paginate !== false;
|
|
889
1077
|
const post_indexBasePath = normalizeRoutePath(post_index.path || DEFAULT_POST_INDEX.path);
|
|
1078
|
+
previewData.site.post_index = {
|
|
1079
|
+
...post_index,
|
|
1080
|
+
enabled: effectivePostIndexEnabled,
|
|
1081
|
+
paginate: effectivePostIndexPaginate,
|
|
1082
|
+
};
|
|
890
1083
|
|
|
891
1084
|
if (frontPage.type !== 'theme_index' && effectivePostIndexEnabled && post_indexBasePath === '/') {
|
|
892
1085
|
throw new Error('Invalid front page configuration: site.front_page occupies "/" so site.post_index.path must not be "/". Set site.post_index.path to a non-root path or disable site.post_index.');
|
|
893
1086
|
}
|
|
894
1087
|
|
|
895
|
-
const frontPageRoute = buildFrontPageRoute(frontPage,
|
|
896
|
-
const
|
|
897
|
-
const preparedPages =
|
|
898
|
-
? pages.filter((page) => page
|
|
1088
|
+
const frontPageRoute = buildFrontPageRoute(frontPage, pageByPath, effectivePostIndexEnabled, post_indexBasePath);
|
|
1089
|
+
const frontPagePage = frontPageRoute?.front_page_type === 'page' ? frontPageRoute.page : null;
|
|
1090
|
+
const preparedPages = frontPagePage
|
|
1091
|
+
? pages.filter((page) => page !== frontPagePage)
|
|
899
1092
|
: pages;
|
|
900
1093
|
|
|
901
1094
|
return {
|
|
902
1095
|
posts,
|
|
903
1096
|
pages: preparedPages,
|
|
904
1097
|
postBySlug,
|
|
1098
|
+
pageByPath,
|
|
1099
|
+
pageReferencePathByPage,
|
|
1100
|
+
commentsByTarget,
|
|
905
1101
|
collections,
|
|
906
1102
|
taxonomies: buildGlobalTaxonomies(previewData, categoryCountBySlug, tagCountBySlug),
|
|
907
1103
|
frontPageRoute,
|
|
@@ -915,20 +1111,22 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
915
1111
|
postBySlug,
|
|
916
1112
|
frontPage,
|
|
917
1113
|
}),
|
|
918
|
-
archiveRoutes:
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1114
|
+
archiveRoutes: previewData.site.archive.enabled
|
|
1115
|
+
? buildPaginatedCollection({
|
|
1116
|
+
items: discoverableSourcePosts,
|
|
1117
|
+
posts_per_page: previewData.site.posts_per_page,
|
|
1118
|
+
basePath: '/archive/',
|
|
1119
|
+
outputStyle: previewData.site.permalinks.output_style,
|
|
1120
|
+
}).map((entry) => ({
|
|
1121
|
+
path: entry.path,
|
|
1122
|
+
page: entry.page,
|
|
1123
|
+
totalPages: entry.totalPages,
|
|
1124
|
+
archive: {
|
|
1125
|
+
groups: buildArchiveGroups(entry.items, postBySlug, previewData.site),
|
|
1126
|
+
},
|
|
1127
|
+
pagination: buildStructuredPagination(entry.paginationData),
|
|
1128
|
+
}))
|
|
1129
|
+
: [],
|
|
932
1130
|
categoryRoutes: buildTaxonomyRoutes({
|
|
933
1131
|
items: previewData.content.categories,
|
|
934
1132
|
postsBySlug: categoryPostsBySlug,
|
|
@@ -954,6 +1152,55 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
954
1152
|
};
|
|
955
1153
|
}
|
|
956
1154
|
|
|
1155
|
+
export function buildTargetCommentsContext({ site, target, targetType, themeSupportsComments }) {
|
|
1156
|
+
const targetPublicId = target?.public_id;
|
|
1157
|
+
const comments = site?.comments;
|
|
1158
|
+
|
|
1159
|
+
if (
|
|
1160
|
+
themeSupportsComments !== true ||
|
|
1161
|
+
comments?.enabled !== true ||
|
|
1162
|
+
target?.allow_comments !== true ||
|
|
1163
|
+
!Number.isInteger(targetPublicId) ||
|
|
1164
|
+
targetPublicId <= 0
|
|
1165
|
+
) {
|
|
1166
|
+
return DISABLED_COMMENTS_CONTEXT;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
const common = {
|
|
1170
|
+
enabled: true,
|
|
1171
|
+
target_type: targetType,
|
|
1172
|
+
target_public_id: targetPublicId,
|
|
1173
|
+
provider: comments.provider,
|
|
1174
|
+
api_base_url: comments.api_base_url,
|
|
1175
|
+
per_page: comments.per_page,
|
|
1176
|
+
order: comments.order,
|
|
1177
|
+
threading: {
|
|
1178
|
+
enabled: comments.threading.enabled,
|
|
1179
|
+
max_depth: comments.threading.max_depth,
|
|
1180
|
+
},
|
|
1181
|
+
};
|
|
1182
|
+
|
|
1183
|
+
if (comments.provider === 'wordpress') {
|
|
1184
|
+
return common;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
if (comments.provider !== 'zeropress') {
|
|
1188
|
+
return DISABLED_COMMENTS_CONTEXT;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
const requestToken = preserveOpaqueNonBlankString(target?.comments?.request_token);
|
|
1192
|
+
return requestToken
|
|
1193
|
+
? { ...common, request_token: requestToken }
|
|
1194
|
+
: DISABLED_COMMENTS_CONTEXT;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
function getTargetCommentsContext(state, targetType, slug) {
|
|
1198
|
+
const targetMap = targetType === 'post'
|
|
1199
|
+
? state.renderData.commentsByTarget.posts
|
|
1200
|
+
: state.renderData.commentsByTarget.pages;
|
|
1201
|
+
return targetMap.get(slug) || DISABLED_COMMENTS_CONTEXT;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
957
1204
|
function buildGlobalTaxonomies(previewData, categoryCountBySlug, tagCountBySlug) {
|
|
958
1205
|
return {
|
|
959
1206
|
categories: buildGlobalTaxonomyItems(previewData.site, 'categories', previewData.content.categories, categoryCountBySlug),
|
|
@@ -961,14 +1208,22 @@ function buildGlobalTaxonomies(previewData, categoryCountBySlug, tagCountBySlug)
|
|
|
961
1208
|
};
|
|
962
1209
|
}
|
|
963
1210
|
|
|
964
|
-
function resolveCollections(collections, postBySlug,
|
|
1211
|
+
function resolveCollections(collections, postBySlug, pageByPath, frontPage, pageReferencePathByPage, collectionTargetByItem) {
|
|
965
1212
|
if (!collections || typeof collections !== 'object') {
|
|
966
1213
|
return {};
|
|
967
1214
|
}
|
|
968
1215
|
|
|
969
1216
|
return Object.fromEntries(
|
|
970
1217
|
Object.entries(collections).map(([collectionId, collection]) => {
|
|
971
|
-
const items = resolveCollectionItems(
|
|
1218
|
+
const items = resolveCollectionItems(
|
|
1219
|
+
collectionId,
|
|
1220
|
+
collection?.items,
|
|
1221
|
+
postBySlug,
|
|
1222
|
+
pageByPath,
|
|
1223
|
+
frontPage,
|
|
1224
|
+
pageReferencePathByPage,
|
|
1225
|
+
collectionTargetByItem,
|
|
1226
|
+
);
|
|
972
1227
|
return [
|
|
973
1228
|
collectionId,
|
|
974
1229
|
{
|
|
@@ -983,44 +1238,58 @@ function resolveCollections(collections, postBySlug, pageBySlug, frontPage) {
|
|
|
983
1238
|
);
|
|
984
1239
|
}
|
|
985
1240
|
|
|
986
|
-
function resolveCollectionItems(collectionId, items, postBySlug,
|
|
1241
|
+
function resolveCollectionItems(collectionId, items, postBySlug, pageByPath, frontPage, pageReferencePathByPage, collectionTargetByItem) {
|
|
987
1242
|
if (!Array.isArray(items)) {
|
|
988
1243
|
return [];
|
|
989
1244
|
}
|
|
990
1245
|
|
|
991
|
-
return items.map((item, index) => resolveCollectionItem(
|
|
1246
|
+
return items.map((item, index) => resolveCollectionItem(
|
|
1247
|
+
collectionId,
|
|
1248
|
+
item,
|
|
1249
|
+
index,
|
|
1250
|
+
postBySlug,
|
|
1251
|
+
pageByPath,
|
|
1252
|
+
frontPage,
|
|
1253
|
+
pageReferencePathByPage,
|
|
1254
|
+
collectionTargetByItem,
|
|
1255
|
+
));
|
|
992
1256
|
}
|
|
993
1257
|
|
|
994
|
-
function resolveCollectionItem(collectionId, item, index, postBySlug,
|
|
1258
|
+
function resolveCollectionItem(collectionId, item, index, postBySlug, pageByPath, frontPage, pageReferencePathByPage, collectionTargetByItem) {
|
|
995
1259
|
if (item?.type === 'post') {
|
|
996
1260
|
const post = postBySlug.get(item.slug);
|
|
997
1261
|
if (!post) {
|
|
998
1262
|
throw new Error(`Invalid collection "${collectionId}": item ${index + 1} references missing post slug "${item.slug}".`);
|
|
999
1263
|
}
|
|
1000
|
-
|
|
1264
|
+
const resolved = {
|
|
1001
1265
|
type: 'post',
|
|
1002
1266
|
meta: post.meta,
|
|
1003
1267
|
...buildStructuredPostSummary(post),
|
|
1004
1268
|
};
|
|
1269
|
+
collectionTargetByItem.set(resolved, post);
|
|
1270
|
+
return resolved;
|
|
1005
1271
|
}
|
|
1006
1272
|
|
|
1007
1273
|
if (item?.type === 'page') {
|
|
1008
|
-
const
|
|
1274
|
+
const pagePath = normalizePageReferencePath(item.path);
|
|
1275
|
+
const page = pageByPath.get(pagePath);
|
|
1009
1276
|
if (!page) {
|
|
1010
|
-
throw new Error(`Invalid collection "${collectionId}": item ${index + 1} references missing page
|
|
1277
|
+
throw new Error(`Invalid collection "${collectionId}": item ${index + 1} references missing page path "${item.path}".`);
|
|
1011
1278
|
}
|
|
1012
|
-
|
|
1279
|
+
const resolved = buildCollectionPageSummary(page, frontPage, pageReferencePathByPage.get(page));
|
|
1280
|
+
collectionTargetByItem.set(resolved, page);
|
|
1281
|
+
return resolved;
|
|
1013
1282
|
}
|
|
1014
1283
|
|
|
1015
1284
|
throw new Error(`Invalid collection "${collectionId}": item ${index + 1} has unsupported type "${item?.type}".`);
|
|
1016
1285
|
}
|
|
1017
1286
|
|
|
1018
|
-
function buildCollectionPageSummary(page, frontPage) {
|
|
1287
|
+
function buildCollectionPageSummary(page, frontPage, pageReferencePath) {
|
|
1019
1288
|
return {
|
|
1020
1289
|
type: 'page',
|
|
1021
1290
|
title: page.title,
|
|
1022
1291
|
slug: page.slug,
|
|
1023
|
-
url: frontPage?.type === 'page' && frontPage.
|
|
1292
|
+
url: frontPage?.type === 'page' && frontPage.page_path === pageReferencePath ? '/' : page.url,
|
|
1024
1293
|
excerpt: page.excerpt || '',
|
|
1025
1294
|
featured_image: page.featured_image || '',
|
|
1026
1295
|
updated_at: page.updated_at || '',
|
|
@@ -1031,19 +1300,12 @@ function buildCollectionPageSummary(page, frontPage) {
|
|
|
1031
1300
|
};
|
|
1032
1301
|
}
|
|
1033
1302
|
|
|
1034
|
-
function attachCollectionCursors(
|
|
1035
|
-
const postTargets = new Map(posts.map((post) => [post.slug, post]));
|
|
1036
|
-
const pageTargets = new Map(pages.map((page) => [page.slug, page]));
|
|
1037
|
-
|
|
1303
|
+
function attachCollectionCursors(collections, collectionTargetByItem) {
|
|
1038
1304
|
for (const [collectionId, collection] of Object.entries(collections || {})) {
|
|
1039
1305
|
const items = Array.isArray(collection.items) ? collection.items : [];
|
|
1040
1306
|
|
|
1041
1307
|
items.forEach((item, index) => {
|
|
1042
|
-
const target = item
|
|
1043
|
-
? postTargets.get(item.slug)
|
|
1044
|
-
: item.type === 'page'
|
|
1045
|
-
? pageTargets.get(item.slug)
|
|
1046
|
-
: null;
|
|
1308
|
+
const target = collectionTargetByItem.get(item);
|
|
1047
1309
|
|
|
1048
1310
|
if (!target) {
|
|
1049
1311
|
return;
|
|
@@ -1094,7 +1356,7 @@ function buildCollectionCursorItemSummary(item) {
|
|
|
1094
1356
|
};
|
|
1095
1357
|
}
|
|
1096
1358
|
|
|
1097
|
-
function buildFrontPageRoute(frontPage,
|
|
1359
|
+
function buildFrontPageRoute(frontPage, pageByPath, effectivePostIndexEnabled, post_indexBasePath) {
|
|
1098
1360
|
if (frontPage.type === 'theme_index') {
|
|
1099
1361
|
if (effectivePostIndexEnabled && post_indexBasePath === '/') {
|
|
1100
1362
|
return null;
|
|
@@ -1109,9 +1371,9 @@ function buildFrontPageRoute(frontPage, pages, effectivePostIndexEnabled, post_i
|
|
|
1109
1371
|
}
|
|
1110
1372
|
|
|
1111
1373
|
if (frontPage.type === 'page') {
|
|
1112
|
-
const page =
|
|
1374
|
+
const page = pageByPath.get(frontPage.page_path);
|
|
1113
1375
|
if (!page) {
|
|
1114
|
-
throw new Error(`Invalid front page configuration: site.front_page.
|
|
1376
|
+
throw new Error(`Invalid front page configuration: site.front_page.page_path "${frontPage.page_path}" does not match a page.`);
|
|
1115
1377
|
}
|
|
1116
1378
|
|
|
1117
1379
|
return {
|
|
@@ -1231,13 +1493,17 @@ function resolveWidgetItem(item, previewData, renderData, widgetAreaId, index) {
|
|
|
1231
1493
|
case 'tags':
|
|
1232
1494
|
return resolveTagsWidget(baseWidget, item.settings, previewData);
|
|
1233
1495
|
case 'archives':
|
|
1234
|
-
return
|
|
1496
|
+
return previewData.site.archive.enabled === true
|
|
1497
|
+
? resolveArchivesWidget(baseWidget, item.settings, previewData)
|
|
1498
|
+
: null;
|
|
1235
1499
|
case 'text':
|
|
1236
1500
|
return resolveTextWidget(baseWidget, item.settings);
|
|
1237
1501
|
case 'link-list':
|
|
1238
1502
|
return resolveLinkListWidget(baseWidget, item.settings);
|
|
1239
1503
|
case 'search':
|
|
1240
|
-
return
|
|
1504
|
+
return previewData.site.search.enabled === true
|
|
1505
|
+
? resolveSearchWidget(baseWidget, item.settings, widgetAreaId, index)
|
|
1506
|
+
: null;
|
|
1241
1507
|
case 'profile':
|
|
1242
1508
|
return resolveProfileWidget(baseWidget, item.settings);
|
|
1243
1509
|
default:
|
|
@@ -1322,7 +1588,7 @@ function resolveArchivesWidget(baseWidget, settings, previewData) {
|
|
|
1322
1588
|
.slice(0, limit)
|
|
1323
1589
|
.map((entry) => ({
|
|
1324
1590
|
label: entry.label,
|
|
1325
|
-
url:
|
|
1591
|
+
url: previewData.site.archive.url,
|
|
1326
1592
|
count: entry.count,
|
|
1327
1593
|
year: entry.year,
|
|
1328
1594
|
month: entry.month,
|
|
@@ -1414,9 +1680,11 @@ function preparePage(page, site) {
|
|
|
1414
1680
|
const documentType = normalizeDocumentType(page.document_type);
|
|
1415
1681
|
const renderedDocument = renderDocument(page.content, documentType);
|
|
1416
1682
|
const permalink = resolvePagePermalink(site, page);
|
|
1683
|
+
const pageFields = { ...page };
|
|
1684
|
+
delete pageFields.comments;
|
|
1417
1685
|
|
|
1418
1686
|
return {
|
|
1419
|
-
...
|
|
1687
|
+
...pageFields,
|
|
1420
1688
|
url: permalink.url,
|
|
1421
1689
|
document_type: documentType,
|
|
1422
1690
|
html: renderedDocument.html,
|
|
@@ -1425,7 +1693,7 @@ function preparePage(page, site) {
|
|
|
1425
1693
|
};
|
|
1426
1694
|
}
|
|
1427
1695
|
|
|
1428
|
-
function preparePost(post, site, authorsById, categoriesBySlug, tagsBySlug
|
|
1696
|
+
function preparePost(post, site, authorsById, categoriesBySlug, tagsBySlug) {
|
|
1429
1697
|
const documentType = normalizeDocumentType(post.document_type);
|
|
1430
1698
|
const renderedDocument = renderDocument(post.content, documentType);
|
|
1431
1699
|
const author = authorsById.get(post.author_id);
|
|
@@ -1480,7 +1748,6 @@ function preparePost(post, site, authorsById, categoriesBySlug, tagsBySlug, them
|
|
|
1480
1748
|
published_at: formatTimestamp(post.published_at_iso, site),
|
|
1481
1749
|
updated_at: formatTimestamp(post.updated_at_iso, site),
|
|
1482
1750
|
reading_time: calculateReadingTime(renderedDocument.html),
|
|
1483
|
-
comments_enabled: themeSupportsComments && site.disallow_comments !== true && post.allow_comments === true,
|
|
1484
1751
|
};
|
|
1485
1752
|
}
|
|
1486
1753
|
|
|
@@ -1730,7 +1997,7 @@ function buildAdjacentPostSummary(post) {
|
|
|
1730
1997
|
};
|
|
1731
1998
|
}
|
|
1732
1999
|
|
|
1733
|
-
function buildArchiveGroups(posts, postBySlug) {
|
|
2000
|
+
function buildArchiveGroups(posts, postBySlug, site) {
|
|
1734
2001
|
const groups = new Map();
|
|
1735
2002
|
|
|
1736
2003
|
for (const post of posts) {
|
|
@@ -1739,14 +2006,12 @@ function buildArchiveGroups(posts, postBySlug) {
|
|
|
1739
2006
|
continue;
|
|
1740
2007
|
}
|
|
1741
2008
|
|
|
1742
|
-
const
|
|
1743
|
-
const
|
|
1744
|
-
const month = date.getUTCMonth() + 1;
|
|
1745
|
-
const label = `${year}-${String(month).padStart(2, '0')}`;
|
|
2009
|
+
const parts = getZonedDateParts(prepared.published_at_iso, site);
|
|
2010
|
+
const label = `${parts.year}-${padDatePart(parts.month)}`;
|
|
1746
2011
|
const current = groups.get(label) || {
|
|
1747
2012
|
label,
|
|
1748
|
-
year,
|
|
1749
|
-
month,
|
|
2013
|
+
year: parts.year,
|
|
2014
|
+
month: parts.month,
|
|
1750
2015
|
items: [],
|
|
1751
2016
|
};
|
|
1752
2017
|
|
|
@@ -1754,7 +2019,8 @@ function buildArchiveGroups(posts, postBySlug) {
|
|
|
1754
2019
|
groups.set(label, current);
|
|
1755
2020
|
}
|
|
1756
2021
|
|
|
1757
|
-
return Array.from(groups.values())
|
|
2022
|
+
return Array.from(groups.values())
|
|
2023
|
+
.sort((left, right) => right.year - left.year || right.month - left.month);
|
|
1758
2024
|
}
|
|
1759
2025
|
|
|
1760
2026
|
function buildTaxonomyRouteData(kind, item, countBySlug) {
|
|
@@ -1789,25 +2055,31 @@ function buildArchiveEntries(posts, site) {
|
|
|
1789
2055
|
}
|
|
1790
2056
|
|
|
1791
2057
|
const date = toDate(publishedAt);
|
|
1792
|
-
const
|
|
1793
|
-
const
|
|
2058
|
+
const parts = getZonedDateParts(date, site);
|
|
2059
|
+
const key = `${parts.year}-${padDatePart(parts.month)}`;
|
|
2060
|
+
const current = entries.get(key) || {
|
|
2061
|
+
date,
|
|
2062
|
+
year: parts.year,
|
|
2063
|
+
month: parts.month,
|
|
2064
|
+
count: 0,
|
|
2065
|
+
};
|
|
1794
2066
|
current.count += 1;
|
|
1795
2067
|
entries.set(key, current);
|
|
1796
2068
|
}
|
|
1797
2069
|
|
|
1798
2070
|
return Array.from(entries.values())
|
|
1799
|
-
.sort((left, right) => right.
|
|
2071
|
+
.sort((left, right) => right.year - left.year || right.month - left.month)
|
|
1800
2072
|
.map((entry) => ({
|
|
1801
2073
|
label: formatArchiveLabel(entry.date, site),
|
|
1802
2074
|
count: entry.count,
|
|
1803
|
-
year: entry.
|
|
1804
|
-
month: entry.
|
|
2075
|
+
year: entry.year,
|
|
2076
|
+
month: entry.month,
|
|
1805
2077
|
}));
|
|
1806
2078
|
}
|
|
1807
2079
|
|
|
1808
2080
|
function formatArchiveLabel(date, site) {
|
|
1809
2081
|
return new Intl.DateTimeFormat(normalizeLocale(site.locale || DEFAULT_LOCALE), {
|
|
1810
|
-
timeZone:
|
|
2082
|
+
timeZone: normalizeTimezone(site.timezone),
|
|
1811
2083
|
year: 'numeric',
|
|
1812
2084
|
month: 'long',
|
|
1813
2085
|
}).format(date);
|
|
@@ -1818,7 +2090,7 @@ function formatTimestamp(value, site) {
|
|
|
1818
2090
|
const locale = normalizeLocale(site.locale || DEFAULT_LOCALE);
|
|
1819
2091
|
const dateStyle = DATETIME_STYLES.has(site.date_style) ? site.date_style : DEFAULT_DATE_STYLE;
|
|
1820
2092
|
const timeStyle = DATETIME_STYLES.has(site.time_style) ? site.time_style : DEFAULT_TIME_STYLE;
|
|
1821
|
-
const siteTimezone =
|
|
2093
|
+
const siteTimezone = normalizeTimezone(site.timezone);
|
|
1822
2094
|
|
|
1823
2095
|
if (dateStyle === 'none' && timeStyle === 'none') {
|
|
1824
2096
|
return '';
|
|
@@ -1858,32 +2130,43 @@ function normalizeOptionalString(value) {
|
|
|
1858
2130
|
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
1859
2131
|
}
|
|
1860
2132
|
|
|
2133
|
+
function preserveOpaqueNonBlankString(value) {
|
|
2134
|
+
return typeof value === 'string' && value.trim() ? value : '';
|
|
2135
|
+
}
|
|
2136
|
+
|
|
1861
2137
|
function normalizeOptionalRawString(value) {
|
|
1862
2138
|
return typeof value === 'string' && value.trim() ? value : '';
|
|
1863
2139
|
}
|
|
1864
2140
|
|
|
1865
2141
|
function normalizeLocale(value) {
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
if (parts.length === 0) {
|
|
1872
|
-
return DEFAULT_LOCALE;
|
|
2142
|
+
const candidate = typeof value === 'string' && value.trim() ? value.trim() : DEFAULT_LOCALE;
|
|
2143
|
+
try {
|
|
2144
|
+
return Intl.getCanonicalLocales(candidate)[0] || DEFAULT_LOCALE;
|
|
2145
|
+
} catch {
|
|
2146
|
+
throw new Error(`Invalid preview-data: site.locale "${candidate}" is not a valid BCP 47 language tag.`);
|
|
1873
2147
|
}
|
|
2148
|
+
}
|
|
1874
2149
|
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
2150
|
+
function normalizeTimezone(value) {
|
|
2151
|
+
const candidate = typeof value === 'string' && value.trim() ? value.trim() : DEFAULT_TIMEZONE;
|
|
2152
|
+
const offsetMatch = /^([+-])(\d{2}):(\d{2})$/u.exec(candidate);
|
|
2153
|
+
if (offsetMatch) {
|
|
2154
|
+
const hours = Number(offsetMatch[2]);
|
|
2155
|
+
const minutes = Number(offsetMatch[3]);
|
|
2156
|
+
if (minutes > 59 || hours > 14 || (hours === 14 && minutes !== 0)) {
|
|
2157
|
+
throw new Error(`Invalid preview-data: site.timezone "${candidate}" is outside the supported fixed-offset range.`);
|
|
1881
2158
|
}
|
|
1882
|
-
if (
|
|
1883
|
-
return
|
|
2159
|
+
if (hours === 0 && minutes === 0) {
|
|
2160
|
+
return DEFAULT_TIMEZONE;
|
|
1884
2161
|
}
|
|
1885
|
-
return
|
|
1886
|
-
}
|
|
2162
|
+
return `${offsetMatch[1]}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`;
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
try {
|
|
2166
|
+
return new Intl.DateTimeFormat('en-US', { timeZone: candidate }).resolvedOptions().timeZone;
|
|
2167
|
+
} catch {
|
|
2168
|
+
throw new Error(`Invalid preview-data: site.timezone "${candidate}" is not UTC, an IANA time zone, or a valid fixed offset.`);
|
|
2169
|
+
}
|
|
1887
2170
|
}
|
|
1888
2171
|
|
|
1889
2172
|
function normalizeDocumentType(value) {
|
|
@@ -1967,61 +2250,6 @@ async function normalizeAndValidateThemePackage(themePackage) {
|
|
|
1967
2250
|
};
|
|
1968
2251
|
}
|
|
1969
2252
|
|
|
1970
|
-
function formatThemeValidationFailure(validation) {
|
|
1971
|
-
const blocks = [
|
|
1972
|
-
[
|
|
1973
|
-
'Theme validation failed',
|
|
1974
|
-
`Errors: ${validation.errors.length}`,
|
|
1975
|
-
`Checked files: ${validation.checkedFiles}`,
|
|
1976
|
-
].join('\n'),
|
|
1977
|
-
...validation.errors.map((issue) => formatThemeValidationIssue(issue)),
|
|
1978
|
-
];
|
|
1979
|
-
return blocks.join('\n\n');
|
|
1980
|
-
}
|
|
1981
|
-
|
|
1982
|
-
function formatThemeValidationIssue(issue) {
|
|
1983
|
-
if (!issue) {
|
|
1984
|
-
return 'Reason: Unknown error';
|
|
1985
|
-
}
|
|
1986
|
-
|
|
1987
|
-
const lines = [`ERROR ${issue.code || 'THEME_VALIDATION_ERROR'}`];
|
|
1988
|
-
const location = splitIssuePath(issue.path);
|
|
1989
|
-
if (location.file) {
|
|
1990
|
-
lines.push(`File: ${location.file}`);
|
|
1991
|
-
}
|
|
1992
|
-
if (location.path) {
|
|
1993
|
-
lines.push(`Path: ${location.path}`);
|
|
1994
|
-
}
|
|
1995
|
-
if (Number.isInteger(issue.line) && Number.isInteger(issue.column)) {
|
|
1996
|
-
lines.push(`Line: ${issue.line}, Column: ${issue.column}`);
|
|
1997
|
-
}
|
|
1998
|
-
if (issue.category) {
|
|
1999
|
-
lines.push(`Category: ${issue.category}`);
|
|
2000
|
-
}
|
|
2001
|
-
lines.push(`Reason: ${issue.message || 'Unknown error'}`);
|
|
2002
|
-
if (issue.snippet) {
|
|
2003
|
-
const lineLabel = Number.isInteger(issue.line) ? String(issue.line) : '';
|
|
2004
|
-
lines.push('', `${lineLabel} | ${issue.snippet.line}`, `${' '.repeat(lineLabel.length)} | ${issue.snippet.pointer}`);
|
|
2005
|
-
}
|
|
2006
|
-
if (issue.hint) {
|
|
2007
|
-
lines.push('', 'Hint:', issue.hint);
|
|
2008
|
-
}
|
|
2009
|
-
|
|
2010
|
-
return lines.join('\n');
|
|
2011
|
-
}
|
|
2012
|
-
|
|
2013
|
-
function splitIssuePath(issuePath) {
|
|
2014
|
-
const normalizedPath = String(issuePath || '');
|
|
2015
|
-
if (normalizedPath.startsWith('theme.json.')) {
|
|
2016
|
-
return {
|
|
2017
|
-
file: 'theme.json',
|
|
2018
|
-
path: normalizedPath.slice('theme.json.'.length),
|
|
2019
|
-
};
|
|
2020
|
-
}
|
|
2021
|
-
|
|
2022
|
-
return { file: normalizedPath, path: '' };
|
|
2023
|
-
}
|
|
2024
|
-
|
|
2025
2253
|
function normalizeThemePackageMetadata(sourceMetadata, manifest) {
|
|
2026
2254
|
return {
|
|
2027
2255
|
...manifest,
|
|
@@ -2029,59 +2257,47 @@ function normalizeThemePackageMetadata(sourceMetadata, manifest) {
|
|
|
2029
2257
|
};
|
|
2030
2258
|
}
|
|
2031
2259
|
|
|
2032
|
-
|
|
2260
|
+
function buildAssetOutputs(assets, assetProcessor, options) {
|
|
2033
2261
|
const outputs = [];
|
|
2034
2262
|
|
|
2035
2263
|
for (const [assetPath, content] of assets.entries()) {
|
|
2036
|
-
const ext = assetPath.split('.').pop()?.toLowerCase();
|
|
2037
|
-
let processedContent = content;
|
|
2038
|
-
let contentType = getContentType(assetPath);
|
|
2039
|
-
|
|
2040
|
-
if (ext === 'css') {
|
|
2041
|
-
processedContent = await assetProcessor.processCSS(new TextDecoder().decode(content));
|
|
2042
|
-
contentType = 'text/css';
|
|
2043
|
-
} else if (ext === 'js') {
|
|
2044
|
-
processedContent = await assetProcessor.processJavaScript(new TextDecoder().decode(content));
|
|
2045
|
-
contentType = 'application/javascript';
|
|
2046
|
-
}
|
|
2047
|
-
|
|
2048
2264
|
const hash = options.assetHashing ? `.${assetProcessor.generateAssetHash(content)}` : '';
|
|
2049
2265
|
const targetPath = `assets/${assetPath.replace(/(\.[^.]+)$/, `${hash}$1`)}`;
|
|
2050
2266
|
|
|
2051
2267
|
outputs.push({
|
|
2052
2268
|
originalPath: assetPath,
|
|
2053
2269
|
path: targetPath,
|
|
2054
|
-
content
|
|
2055
|
-
contentType,
|
|
2270
|
+
content,
|
|
2271
|
+
contentType: getContentType(assetPath),
|
|
2056
2272
|
});
|
|
2057
2273
|
}
|
|
2058
2274
|
|
|
2059
2275
|
return outputs;
|
|
2060
2276
|
}
|
|
2061
2277
|
|
|
2062
|
-
|
|
2063
|
-
const content =
|
|
2064
|
-
if (!content) {
|
|
2278
|
+
function buildCustomCssAsset(customCss, assetProcessor, options) {
|
|
2279
|
+
const content = customCss?.content;
|
|
2280
|
+
if (typeof content !== 'string' || !content.trim()) {
|
|
2065
2281
|
return null;
|
|
2066
2282
|
}
|
|
2067
2283
|
|
|
2068
2284
|
const sourceBuffer = new TextEncoder().encode(content);
|
|
2069
|
-
const processedContent = await assetProcessor.processCSS(content);
|
|
2070
2285
|
const hash = options.assetHashing ? `.${assetProcessor.generateAssetHash(sourceBuffer)}` : '';
|
|
2071
2286
|
|
|
2072
2287
|
return {
|
|
2073
2288
|
originalPath: '__zeropress_custom.css',
|
|
2074
2289
|
path: `assets/zeropress-custom${hash}.css`,
|
|
2075
|
-
content
|
|
2290
|
+
content,
|
|
2076
2291
|
contentType: 'text/css',
|
|
2077
2292
|
};
|
|
2078
2293
|
}
|
|
2079
2294
|
|
|
2080
|
-
function createRenderContext(site, currentUrl) {
|
|
2295
|
+
function createRenderContext(site, currentUrl, comments = DISABLED_COMMENTS_CONTEXT) {
|
|
2081
2296
|
return {
|
|
2082
2297
|
site,
|
|
2083
2298
|
currentUrl,
|
|
2084
2299
|
language: site.locale,
|
|
2300
|
+
comments,
|
|
2085
2301
|
};
|
|
2086
2302
|
}
|
|
2087
2303
|
|
|
@@ -2097,23 +2313,26 @@ function buildRouteContext(type, url, options = {}) {
|
|
|
2097
2313
|
|
|
2098
2314
|
function buildPageMeta(site, options = {}) {
|
|
2099
2315
|
const resolvedTitle = normalizeNonEmptyString(options.title, site.title);
|
|
2100
|
-
const
|
|
2101
|
-
const
|
|
2102
|
-
const
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
const
|
|
2316
|
+
const includeRichMetadata = options.includeRichMetadata !== false;
|
|
2317
|
+
const resolvedDescription = includeRichMetadata ? normalizeOptionalString(options.description) : '';
|
|
2318
|
+
const canonicalUrl = includeRichMetadata
|
|
2319
|
+
? resolveMetaCanonicalUrl(site, options.canonicalUrl || options.currentUrl)
|
|
2320
|
+
: '';
|
|
2321
|
+
const ogImage = includeRichMetadata ? resolveMetaImageUrl(options.image) : '';
|
|
2322
|
+
const ogType = includeRichMetadata ? normalizeNonEmptyString(options.ogType, 'website') : '';
|
|
2323
|
+
const publishedTime = includeRichMetadata ? normalizeOptionalString(options.publishedTime) : '';
|
|
2324
|
+
const modifiedTime = includeRichMetadata ? normalizeOptionalString(options.modifiedTime) : '';
|
|
2106
2325
|
const robotsNoindex = options.robotsNoindex === true;
|
|
2107
2326
|
|
|
2108
2327
|
const meta = {
|
|
2109
2328
|
title: escapeHtml(resolvedTitle),
|
|
2110
2329
|
description: resolvedDescription ? escapeHtml(resolvedDescription) : '',
|
|
2111
2330
|
canonical_url: canonicalUrl ? escapeHtml(canonicalUrl) : '',
|
|
2112
|
-
og_title: escapeHtml(resolvedTitle),
|
|
2113
|
-
og_description: resolvedDescription ? escapeHtml(resolvedDescription) : '',
|
|
2331
|
+
og_title: includeRichMetadata ? escapeHtml(resolvedTitle) : '',
|
|
2332
|
+
og_description: includeRichMetadata && resolvedDescription ? escapeHtml(resolvedDescription) : '',
|
|
2114
2333
|
og_type: escapeHtml(ogType),
|
|
2115
|
-
og_url: canonicalUrl ? escapeHtml(canonicalUrl) : '',
|
|
2116
|
-
og_site_name: escapeHtml(site.title),
|
|
2334
|
+
og_url: includeRichMetadata && canonicalUrl ? escapeHtml(canonicalUrl) : '',
|
|
2335
|
+
og_site_name: includeRichMetadata ? escapeHtml(site.title) : '',
|
|
2117
2336
|
og_image: ogImage ? escapeHtml(ogImage) : '',
|
|
2118
2337
|
article_published_time: publishedTime ? escapeHtml(publishedTime) : '',
|
|
2119
2338
|
article_modified_time: modifiedTime ? escapeHtml(modifiedTime) : '',
|
|
@@ -2122,7 +2341,7 @@ function buildPageMeta(site, options = {}) {
|
|
|
2122
2341
|
|
|
2123
2342
|
return {
|
|
2124
2343
|
...meta,
|
|
2125
|
-
head_tags: buildMetaHeadTags(meta),
|
|
2344
|
+
head_tags: buildMetaHeadTags(meta, site, options),
|
|
2126
2345
|
};
|
|
2127
2346
|
}
|
|
2128
2347
|
|
|
@@ -2138,7 +2357,7 @@ function buildFrontPageTitle(site) {
|
|
|
2138
2357
|
return resolvedDescription ? `${resolvedSiteTitle} - ${resolvedDescription}` : resolvedSiteTitle;
|
|
2139
2358
|
}
|
|
2140
2359
|
|
|
2141
|
-
function buildMetaHeadTags(meta) {
|
|
2360
|
+
function buildMetaHeadTags(meta, site, options = {}) {
|
|
2142
2361
|
const tags = [];
|
|
2143
2362
|
|
|
2144
2363
|
if (meta.description) {
|
|
@@ -2150,24 +2369,29 @@ function buildMetaHeadTags(meta) {
|
|
|
2150
2369
|
if (meta.canonical_url) {
|
|
2151
2370
|
tags.push(`<link rel="canonical" href="${meta.canonical_url}">`);
|
|
2152
2371
|
}
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
if (meta.og_description) {
|
|
2156
|
-
tags.push(`<meta property="og:description" content="${meta.og_description}">`);
|
|
2157
|
-
}
|
|
2158
|
-
tags.push(`<meta property="og:type" content="${meta.og_type}">`);
|
|
2159
|
-
if (meta.og_url) {
|
|
2160
|
-
tags.push(`<meta property="og:url" content="${meta.og_url}">`);
|
|
2161
|
-
}
|
|
2162
|
-
tags.push(`<meta property="og:site_name" content="${meta.og_site_name}">`);
|
|
2163
|
-
if (meta.og_image) {
|
|
2164
|
-
tags.push(`<meta property="og:image" content="${meta.og_image}">`);
|
|
2372
|
+
if (options.includeFeedLink !== false && site.feed?.enabled === true) {
|
|
2373
|
+
tags.push(`<link rel="alternate" type="application/rss+xml" title="${escapeHtml(site.title)} Feed" href="${escapeHtml(site.feed.url)}">`);
|
|
2165
2374
|
}
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2375
|
+
|
|
2376
|
+
if (meta.og_title) {
|
|
2377
|
+
tags.push(`<meta property="og:title" content="${meta.og_title}">`);
|
|
2378
|
+
if (meta.og_description) {
|
|
2379
|
+
tags.push(`<meta property="og:description" content="${meta.og_description}">`);
|
|
2380
|
+
}
|
|
2381
|
+
tags.push(`<meta property="og:type" content="${meta.og_type}">`);
|
|
2382
|
+
if (meta.og_url) {
|
|
2383
|
+
tags.push(`<meta property="og:url" content="${meta.og_url}">`);
|
|
2384
|
+
}
|
|
2385
|
+
tags.push(`<meta property="og:site_name" content="${meta.og_site_name}">`);
|
|
2386
|
+
if (meta.og_image) {
|
|
2387
|
+
tags.push(`<meta property="og:image" content="${meta.og_image}">`);
|
|
2388
|
+
}
|
|
2389
|
+
if (meta.article_published_time) {
|
|
2390
|
+
tags.push(`<meta property="article:published_time" content="${meta.article_published_time}">`);
|
|
2391
|
+
}
|
|
2392
|
+
if (meta.article_modified_time) {
|
|
2393
|
+
tags.push(`<meta property="article:modified_time" content="${meta.article_modified_time}">`);
|
|
2394
|
+
}
|
|
2171
2395
|
}
|
|
2172
2396
|
|
|
2173
2397
|
return tags.length ? `${tags.join('\n')}\n` : '';
|
|
@@ -2194,7 +2418,7 @@ function resolveMetaImageUrl(image) {
|
|
|
2194
2418
|
return '';
|
|
2195
2419
|
}
|
|
2196
2420
|
|
|
2197
|
-
function normalizeMediaField(value,
|
|
2421
|
+
function normalizeMediaField(value, media_origin) {
|
|
2198
2422
|
if (value === undefined) {
|
|
2199
2423
|
return undefined;
|
|
2200
2424
|
}
|
|
@@ -2204,22 +2428,145 @@ function normalizeMediaField(value, media_base_url) {
|
|
|
2204
2428
|
return '';
|
|
2205
2429
|
}
|
|
2206
2430
|
|
|
2207
|
-
|
|
2208
|
-
|
|
2431
|
+
const normalizedMediaUrl = normalizeMediaUrl(normalizedValue);
|
|
2432
|
+
if (!normalizedMediaUrl) {
|
|
2433
|
+
return '';
|
|
2209
2434
|
}
|
|
2210
2435
|
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2436
|
+
if (isAbsoluteUrl(normalizedMediaUrl)) {
|
|
2437
|
+
return normalizedMediaUrl;
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
const normalizedOrigin = normalizeMediaOrigin(media_origin);
|
|
2441
|
+
if (!normalizedOrigin) {
|
|
2442
|
+
return normalizedMediaUrl;
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
try {
|
|
2446
|
+
return decodeURI(new URL(normalizedMediaUrl, `${normalizedOrigin}/`).toString());
|
|
2447
|
+
} catch {
|
|
2448
|
+
return '';
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
function normalizeMediaOrigin(value) {
|
|
2453
|
+
const normalizedValue = normalizeOptionalString(value);
|
|
2454
|
+
if (!normalizedValue) {
|
|
2455
|
+
return '';
|
|
2214
2456
|
}
|
|
2215
2457
|
|
|
2216
2458
|
try {
|
|
2217
|
-
|
|
2459
|
+
const url = new URL(normalizedValue);
|
|
2460
|
+
if (
|
|
2461
|
+
!SAFE_MEDIA_PROTOCOLS.has(url.protocol)
|
|
2462
|
+
|| url.username
|
|
2463
|
+
|| url.password
|
|
2464
|
+
|| url.search
|
|
2465
|
+
|| url.hash
|
|
2466
|
+
|| url.pathname !== '/'
|
|
2467
|
+
|| !isSafeUrlText(normalizedValue)
|
|
2468
|
+
) {
|
|
2469
|
+
return '';
|
|
2470
|
+
}
|
|
2471
|
+
return url.origin;
|
|
2218
2472
|
} catch {
|
|
2219
2473
|
return '';
|
|
2220
2474
|
}
|
|
2221
2475
|
}
|
|
2222
2476
|
|
|
2477
|
+
function normalizeSiteOrigin(value) {
|
|
2478
|
+
const normalizedValue = normalizeOptionalString(value);
|
|
2479
|
+
if (!normalizedValue) {
|
|
2480
|
+
return '';
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
try {
|
|
2484
|
+
const url = new URL(normalizedValue);
|
|
2485
|
+
if (
|
|
2486
|
+
!SAFE_LINK_PROTOCOLS.has(url.protocol)
|
|
2487
|
+
|| url.username
|
|
2488
|
+
|| url.password
|
|
2489
|
+
|| url.search
|
|
2490
|
+
|| url.hash
|
|
2491
|
+
|| url.pathname !== '/'
|
|
2492
|
+
|| !isSafeUrlText(normalizedValue)
|
|
2493
|
+
) {
|
|
2494
|
+
throw new Error('site.url must be an HTTP(S) origin without credentials, path, query, or fragment');
|
|
2495
|
+
}
|
|
2496
|
+
return url.origin;
|
|
2497
|
+
} catch (error) {
|
|
2498
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2499
|
+
throw new Error(`Invalid preview-data: site.url is not a safe HTTP(S) origin: ${reason}`);
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
function normalizeNavigationUrl(value) {
|
|
2504
|
+
return normalizeSafeResourceUrl(value, { allowRoot: true });
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
function normalizeMediaUrl(value) {
|
|
2508
|
+
return normalizeSafeResourceUrl(value, { allowRoot: false });
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
function normalizeSafeResourceUrl(value, options) {
|
|
2512
|
+
const normalizedValue = normalizeOptionalString(value);
|
|
2513
|
+
if (!normalizedValue || !isSafeUrlText(normalizedValue)) {
|
|
2514
|
+
return '';
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
if (normalizedValue.startsWith('/')) {
|
|
2518
|
+
if (normalizedValue.startsWith('//')) {
|
|
2519
|
+
return '';
|
|
2520
|
+
}
|
|
2521
|
+
const pathname = normalizedValue.split(/[?#]/u, 1)[0];
|
|
2522
|
+
if ((!options.allowRoot && pathname === '/') || hasDotPathSegment(pathname)) {
|
|
2523
|
+
return '';
|
|
2524
|
+
}
|
|
2525
|
+
return normalizedValue;
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
if (!isAbsoluteUrl(normalizedValue)) {
|
|
2529
|
+
return '';
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
const rawPath = extractAbsoluteUrlPath(normalizedValue);
|
|
2533
|
+
if ((!options.allowRoot && (!rawPath || rawPath === '/')) || hasDotPathSegment(rawPath || '/')) {
|
|
2534
|
+
return '';
|
|
2535
|
+
}
|
|
2536
|
+
return normalizeAbsoluteUrl(normalizedValue, SAFE_LINK_PROTOCOLS);
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
function isSafeUrlText(value) {
|
|
2540
|
+
if (/[\s\\\u0000-\u001F\u007F]/u.test(value)) {
|
|
2541
|
+
return false;
|
|
2542
|
+
}
|
|
2543
|
+
try {
|
|
2544
|
+
decodeURI(value);
|
|
2545
|
+
return true;
|
|
2546
|
+
} catch {
|
|
2547
|
+
return false;
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
function extractAbsoluteUrlPath(value) {
|
|
2552
|
+
const match = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^/?#]*(?<path>[^?#]*)/u.exec(value);
|
|
2553
|
+
return match?.groups?.path || '/';
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
function hasDotPathSegment(pathname) {
|
|
2557
|
+
return String(pathname || '').split('/').some((segment) => {
|
|
2558
|
+
if (!segment) {
|
|
2559
|
+
return false;
|
|
2560
|
+
}
|
|
2561
|
+
try {
|
|
2562
|
+
const decoded = decodeURIComponent(segment).normalize('NFC');
|
|
2563
|
+
return decoded === '.' || decoded === '..';
|
|
2564
|
+
} catch {
|
|
2565
|
+
return true;
|
|
2566
|
+
}
|
|
2567
|
+
});
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2223
2570
|
function isAbsoluteUrl(value) {
|
|
2224
2571
|
try {
|
|
2225
2572
|
new URL(value);
|
|
@@ -2232,7 +2579,7 @@ function isAbsoluteUrl(value) {
|
|
|
2232
2579
|
function normalizeAbsoluteUrl(value, allowedProtocols) {
|
|
2233
2580
|
try {
|
|
2234
2581
|
const url = new URL(value);
|
|
2235
|
-
if (!allowedProtocols.has(url.protocol)) {
|
|
2582
|
+
if (!allowedProtocols.has(url.protocol) || url.username || url.password || !isSafeUrlText(value)) {
|
|
2236
2583
|
return '';
|
|
2237
2584
|
}
|
|
2238
2585
|
return decodeURI(url.toString());
|
|
@@ -2242,16 +2589,7 @@ function normalizeAbsoluteUrl(value, allowedProtocols) {
|
|
|
2242
2589
|
}
|
|
2243
2590
|
|
|
2244
2591
|
function normalizeThemeLinkUrl(value) {
|
|
2245
|
-
|
|
2246
|
-
if (!normalizedValue || normalizedValue.startsWith('//')) {
|
|
2247
|
-
return '';
|
|
2248
|
-
}
|
|
2249
|
-
|
|
2250
|
-
if (isAbsoluteUrl(normalizedValue)) {
|
|
2251
|
-
return normalizeAbsoluteUrl(normalizedValue, SAFE_LINK_PROTOCOLS);
|
|
2252
|
-
}
|
|
2253
|
-
|
|
2254
|
-
return normalizedValue;
|
|
2592
|
+
return normalizeNavigationUrl(value);
|
|
2255
2593
|
}
|
|
2256
2594
|
|
|
2257
2595
|
async function writeOutput(writer, summaries, path, content, contentType) {
|
|
@@ -2320,6 +2658,31 @@ function resolvePagePermalink(site, page) {
|
|
|
2320
2658
|
return resolvePermalink(site, 'pages', page);
|
|
2321
2659
|
}
|
|
2322
2660
|
|
|
2661
|
+
function resolveEffectivePageReferencePath(site, page) {
|
|
2662
|
+
return normalizePageReferencePath(resolvePagePermalink(site, page).path);
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
function normalizePageReferencePath(value) {
|
|
2666
|
+
const normalized = decodeRoutePath(normalizeOptionalString(value))
|
|
2667
|
+
.replace(/^\/+|\/+$/gu, '')
|
|
2668
|
+
.normalize('NFC');
|
|
2669
|
+
return normalized;
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
function assertUniquePageReferencePaths(pageReferencePaths) {
|
|
2673
|
+
const seen = new Map();
|
|
2674
|
+
for (const [page, pagePath] of pageReferencePaths.entries()) {
|
|
2675
|
+
if (!pagePath) {
|
|
2676
|
+
throw new Error(`Invalid preview-data: Page "${page?.slug || ''}" resolves to an empty effective path.`);
|
|
2677
|
+
}
|
|
2678
|
+
const existing = seen.get(pagePath);
|
|
2679
|
+
if (existing) {
|
|
2680
|
+
throw new Error(`Invalid preview-data: Pages "${existing.slug}" and "${page.slug}" resolve to the same effective path "${pagePath}".`);
|
|
2681
|
+
}
|
|
2682
|
+
seen.set(pagePath, page);
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2323
2686
|
function resolvePermalink(site, kind, item) {
|
|
2324
2687
|
const pattern = normalizeNonEmptyString(site.permalinks?.[kind], DEFAULT_PERMALINKS[kind]);
|
|
2325
2688
|
return buildRouteInfo(applyPermalinkPattern(pattern, kind, item, site), site.permalinks.output_style);
|
|
@@ -2358,19 +2721,19 @@ function buildPermalinkTokenValues(kind, item, site) {
|
|
|
2358
2721
|
};
|
|
2359
2722
|
|
|
2360
2723
|
if (kind === 'posts') {
|
|
2361
|
-
const parts =
|
|
2724
|
+
const parts = getZonedDateParts(item.published_at_iso, site);
|
|
2362
2725
|
values.public_id = String(item.public_id);
|
|
2363
|
-
values.year = parts.year;
|
|
2364
|
-
values.month = parts.month;
|
|
2365
|
-
values.day = parts.day;
|
|
2726
|
+
values.year = String(parts.year);
|
|
2727
|
+
values.month = padDatePart(parts.month);
|
|
2728
|
+
values.day = padDatePart(parts.day);
|
|
2366
2729
|
}
|
|
2367
2730
|
|
|
2368
2731
|
return values;
|
|
2369
2732
|
}
|
|
2370
2733
|
|
|
2371
|
-
function
|
|
2734
|
+
function getZonedDateParts(value, site) {
|
|
2372
2735
|
const date = toDate(value);
|
|
2373
|
-
const timeZone =
|
|
2736
|
+
const timeZone = normalizeTimezone(site.timezone);
|
|
2374
2737
|
const parts = new Intl.DateTimeFormat('en-US', {
|
|
2375
2738
|
timeZone,
|
|
2376
2739
|
year: 'numeric',
|
|
@@ -2379,12 +2742,16 @@ function getPermalinkDateParts(value, site) {
|
|
|
2379
2742
|
}).formatToParts(date);
|
|
2380
2743
|
|
|
2381
2744
|
return {
|
|
2382
|
-
year: parts.find((part) => part.type === 'year')?.value ||
|
|
2383
|
-
month: parts.find((part) => part.type === 'month')?.value ||
|
|
2384
|
-
day: parts.find((part) => part.type === 'day')?.value ||
|
|
2745
|
+
year: Number(parts.find((part) => part.type === 'year')?.value) || date.getUTCFullYear(),
|
|
2746
|
+
month: Number(parts.find((part) => part.type === 'month')?.value) || date.getUTCMonth() + 1,
|
|
2747
|
+
day: Number(parts.find((part) => part.type === 'day')?.value) || date.getUTCDate(),
|
|
2385
2748
|
};
|
|
2386
2749
|
}
|
|
2387
2750
|
|
|
2751
|
+
function padDatePart(value) {
|
|
2752
|
+
return String(value).padStart(2, '0');
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2388
2755
|
function normalizeOutputPath(filePath) {
|
|
2389
2756
|
return String(filePath || '').replace(/^\/+/, '');
|
|
2390
2757
|
}
|
|
@@ -2437,29 +2804,31 @@ function assertPlannedOutputPathsSafe(state) {
|
|
|
2437
2804
|
outputPath: pageToOutputPath(page, outputStyle),
|
|
2438
2805
|
})),
|
|
2439
2806
|
];
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
const plannedPaths = [
|
|
2443
|
-
...routeEntries.map((entry) => entry.outputPath),
|
|
2807
|
+
const nonRoutePaths = [
|
|
2444
2808
|
...state.assetOutputs.map((assetOutput) => assetOutput.path),
|
|
2445
2809
|
];
|
|
2446
2810
|
|
|
2447
|
-
if (shouldGenerateSearchArtifacts(state
|
|
2448
|
-
|
|
2811
|
+
if (shouldGenerateSearchArtifacts(state)) {
|
|
2812
|
+
nonRoutePaths.push(SEARCH_INDEX_OUTPUT_PATH, SEARCH_ADAPTER_OUTPUT_PATH, SEARCH_PAGEFIND_ADAPTER_OUTPUT_PATH);
|
|
2449
2813
|
}
|
|
2450
2814
|
|
|
2451
|
-
if (state
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2815
|
+
if (hasTemplate(state, '404')) {
|
|
2816
|
+
nonRoutePaths.push('404.html');
|
|
2817
|
+
}
|
|
2818
|
+
if (shouldGenerateRobotsTxt(state.options)) {
|
|
2819
|
+
nonRoutePaths.push('robots.txt');
|
|
2820
|
+
}
|
|
2821
|
+
if (hasCanonicalSiteUrl(state.previewData.site.url)) {
|
|
2822
|
+
nonRoutePaths.push('sitemap.xml');
|
|
2823
|
+
if (shouldGenerateFeed(state)) {
|
|
2824
|
+
nonRoutePaths.push('feed.xml');
|
|
2461
2825
|
}
|
|
2462
2826
|
}
|
|
2827
|
+
nonRoutePaths.push(...normalizeReservedOutputPaths(state.options.reservedOutputPaths));
|
|
2828
|
+
const plannedPaths = [
|
|
2829
|
+
...routeEntries.map((entry) => entry.outputPath),
|
|
2830
|
+
...nonRoutePaths,
|
|
2831
|
+
];
|
|
2463
2832
|
|
|
2464
2833
|
for (const plannedPath of plannedPaths) {
|
|
2465
2834
|
const rawPath = String(plannedPath || '');
|
|
@@ -2467,29 +2836,92 @@ function assertPlannedOutputPathsSafe(state) {
|
|
|
2467
2836
|
assertSafeRelativeOutputPath(rawPath, normalizedPath);
|
|
2468
2837
|
}
|
|
2469
2838
|
|
|
2839
|
+
const publicUrlClaims = [
|
|
2840
|
+
...routeEntries.flatMap((entry, index) => {
|
|
2841
|
+
const owner = `route:${index}`;
|
|
2842
|
+
return [entry.url, ...buildOutputPublicUrlAliases(entry.outputPath)]
|
|
2843
|
+
.map((url) => ({ url, owner }));
|
|
2844
|
+
}),
|
|
2845
|
+
...nonRoutePaths.flatMap((outputPath, index) => {
|
|
2846
|
+
const owner = `file:${index}`;
|
|
2847
|
+
return buildOutputPublicUrlAliases(outputPath)
|
|
2848
|
+
.map((url) => ({ url, owner }));
|
|
2849
|
+
}),
|
|
2850
|
+
];
|
|
2851
|
+
assertUniquePublicUrlClaims(publicUrlClaims);
|
|
2470
2852
|
assertUniqueOutputPaths(plannedPaths);
|
|
2471
2853
|
}
|
|
2472
2854
|
|
|
2473
|
-
function
|
|
2855
|
+
function normalizeReservedOutputPaths(value) {
|
|
2856
|
+
if (value === undefined) {
|
|
2857
|
+
return [];
|
|
2858
|
+
}
|
|
2859
|
+
if (!Array.isArray(value)) {
|
|
2860
|
+
throw new Error('reservedOutputPaths must be an array of relative output file paths');
|
|
2861
|
+
}
|
|
2862
|
+
return value.map((outputPath) => {
|
|
2863
|
+
if (typeof outputPath !== 'string') {
|
|
2864
|
+
throw new Error('reservedOutputPaths must contain only relative output file paths');
|
|
2865
|
+
}
|
|
2866
|
+
return outputPath;
|
|
2867
|
+
});
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
function buildOutputPublicUrlAliases(outputPath) {
|
|
2871
|
+
const normalizedPath = normalizeOutputPath(outputPath);
|
|
2872
|
+
const aliases = new Set([`/${normalizedPath}`]);
|
|
2873
|
+
|
|
2874
|
+
if (normalizedPath.endsWith('.html')) {
|
|
2875
|
+
aliases.add(`/${normalizedPath.slice(0, -'.html'.length)}`);
|
|
2876
|
+
}
|
|
2877
|
+
if (normalizedPath === 'index.html') {
|
|
2878
|
+
aliases.add('/');
|
|
2879
|
+
} else if (normalizedPath.endsWith('/index.html')) {
|
|
2880
|
+
aliases.add(`/${normalizedPath.slice(0, -'index.html'.length)}`);
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
return [...aliases];
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
function assertUniquePublicUrlClaims(claims) {
|
|
2474
2887
|
const seenUrls = new Map();
|
|
2475
|
-
for (const
|
|
2476
|
-
const normalizedUrl = normalizeRouteCollisionKey(
|
|
2477
|
-
|
|
2478
|
-
|
|
2888
|
+
for (const claim of claims) {
|
|
2889
|
+
const normalizedUrl = normalizeRouteCollisionKey(claim.url);
|
|
2890
|
+
const existingOwner = seenUrls.get(normalizedUrl);
|
|
2891
|
+
if (existingOwner !== undefined && existingOwner !== claim.owner) {
|
|
2892
|
+
throw new Error(`Duplicate public URL detected: ${claim.url}`);
|
|
2479
2893
|
}
|
|
2480
|
-
seenUrls.set(normalizedUrl,
|
|
2894
|
+
seenUrls.set(normalizedUrl, claim.owner);
|
|
2481
2895
|
}
|
|
2482
2896
|
}
|
|
2483
2897
|
|
|
2484
2898
|
function assertUniqueOutputPaths(plannedPaths) {
|
|
2899
|
+
const normalizedPaths = plannedPaths.map((plannedPath) => normalizeOutputPath(plannedPath));
|
|
2485
2900
|
const seenPaths = new Set();
|
|
2486
|
-
for (const
|
|
2487
|
-
const normalizedPath = normalizeOutputPath(plannedPath);
|
|
2901
|
+
for (const [index, normalizedPath] of normalizedPaths.entries()) {
|
|
2488
2902
|
if (seenPaths.has(normalizedPath)) {
|
|
2489
|
-
throw new Error(`Duplicate output path detected: ${
|
|
2903
|
+
throw new Error(`Duplicate output path detected: ${plannedPaths[index]}`);
|
|
2490
2904
|
}
|
|
2491
2905
|
seenPaths.add(normalizedPath);
|
|
2492
2906
|
}
|
|
2907
|
+
|
|
2908
|
+
const shallowestFirst = [...seenPaths]
|
|
2909
|
+
.map((outputPath) => ({ outputPath, segments: outputPath.split('/') }))
|
|
2910
|
+
.sort((left, right) => (
|
|
2911
|
+
left.segments.length - right.segments.length
|
|
2912
|
+
|| left.outputPath.localeCompare(right.outputPath)
|
|
2913
|
+
));
|
|
2914
|
+
const processedPaths = new Set();
|
|
2915
|
+
for (const { outputPath, segments } of shallowestFirst) {
|
|
2916
|
+
let ancestorPath = '';
|
|
2917
|
+
for (const segment of segments.slice(0, -1)) {
|
|
2918
|
+
ancestorPath = ancestorPath ? `${ancestorPath}/${segment}` : segment;
|
|
2919
|
+
if (processedPaths.has(ancestorPath)) {
|
|
2920
|
+
throw new Error(`Conflicting output path hierarchy detected: ${ancestorPath} and ${outputPath}`);
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
processedPaths.add(outputPath);
|
|
2924
|
+
}
|
|
2493
2925
|
}
|
|
2494
2926
|
|
|
2495
2927
|
function normalizeRouteCollisionKey(url) {
|
|
@@ -2532,6 +2964,7 @@ function assertSafeRelativeOutputPath(rawPath, normalizedPath = normalizeOutputP
|
|
|
2532
2964
|
if (
|
|
2533
2965
|
originalPath.startsWith('/') ||
|
|
2534
2966
|
originalPath.startsWith('\\') ||
|
|
2967
|
+
originalPath.includes('\\') ||
|
|
2535
2968
|
/^[A-Za-z]:[\\/]/.test(originalPath) ||
|
|
2536
2969
|
normalizedSeparators.startsWith('//')
|
|
2537
2970
|
) {
|
|
@@ -2554,11 +2987,11 @@ function sha256(content) {
|
|
|
2554
2987
|
return hash.digest('hex');
|
|
2555
2988
|
}
|
|
2556
2989
|
|
|
2557
|
-
function injectSiteCustomizations(html, state) {
|
|
2990
|
+
function injectSiteCustomizations(html, state, target) {
|
|
2558
2991
|
let next = injectFaviconLinks(html, state.favicon);
|
|
2559
2992
|
next = injectGeneratorMeta(next, state.exposeGenerator);
|
|
2560
2993
|
next = injectCustomCssAssetLink(next, state.customCssHref);
|
|
2561
|
-
next = injectCustomHtml(next, state.customHtml);
|
|
2994
|
+
next = injectCustomHtml(next, state.customHtml, target);
|
|
2562
2995
|
return next;
|
|
2563
2996
|
}
|
|
2564
2997
|
|
|
@@ -2568,7 +3001,7 @@ function injectFaviconLinks(html, favicon) {
|
|
|
2568
3001
|
return html;
|
|
2569
3002
|
}
|
|
2570
3003
|
|
|
2571
|
-
return html
|
|
3004
|
+
return insertBeforeClosingTag(html, HEAD_CLOSING_TAG_PATTERN, `${links}\n`);
|
|
2572
3005
|
}
|
|
2573
3006
|
|
|
2574
3007
|
function buildFaviconLinks(favicon) {
|
|
@@ -2577,14 +3010,36 @@ function buildFaviconLinks(favicon) {
|
|
|
2577
3010
|
}
|
|
2578
3011
|
|
|
2579
3012
|
const lines = [];
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
3013
|
+
const icon = normalizeOptionalString(favicon.icon);
|
|
3014
|
+
const iconDark = normalizeOptionalString(favicon.icon_dark);
|
|
3015
|
+
const svg = normalizeOptionalString(favicon.svg);
|
|
3016
|
+
const png = normalizeOptionalString(favicon.png);
|
|
3017
|
+
const hasDefaultIcon = Boolean(icon || svg || png);
|
|
3018
|
+
const hasBothColorSchemes = Boolean(iconDark && hasDefaultIcon);
|
|
3019
|
+
|
|
3020
|
+
if (iconDark) {
|
|
3021
|
+
lines.push(buildFaviconLink(
|
|
3022
|
+
iconDark,
|
|
3023
|
+
undefined,
|
|
3024
|
+
hasBothColorSchemes ? '(prefers-color-scheme: dark)' : undefined,
|
|
3025
|
+
));
|
|
3026
|
+
}
|
|
3027
|
+
if (icon) {
|
|
3028
|
+
lines.push(buildFaviconLink(icon, undefined, hasBothColorSchemes ? '(prefers-color-scheme: light)' : undefined));
|
|
3029
|
+
}
|
|
3030
|
+
if (svg) {
|
|
3031
|
+
lines.push(buildFaviconLink(
|
|
3032
|
+
svg,
|
|
3033
|
+
'image/svg+xml',
|
|
3034
|
+
hasBothColorSchemes ? '(prefers-color-scheme: light)' : undefined,
|
|
3035
|
+
));
|
|
3036
|
+
}
|
|
3037
|
+
if (png) {
|
|
3038
|
+
lines.push(buildFaviconLink(
|
|
3039
|
+
png,
|
|
3040
|
+
'image/png',
|
|
3041
|
+
hasBothColorSchemes ? '(prefers-color-scheme: light)' : undefined,
|
|
3042
|
+
));
|
|
2588
3043
|
}
|
|
2589
3044
|
if (normalizeOptionalString(favicon.apple_touch_icon)) {
|
|
2590
3045
|
lines.push(` <link rel="apple-touch-icon" href="${escapeHtml(favicon.apple_touch_icon)}">`);
|
|
@@ -2593,12 +3048,20 @@ function buildFaviconLinks(favicon) {
|
|
|
2593
3048
|
return lines.join('\n');
|
|
2594
3049
|
}
|
|
2595
3050
|
|
|
3051
|
+
function buildFaviconLink(href, type, media) {
|
|
3052
|
+
return ` <link rel="icon" href="${escapeHtml(href)}"${type ? ` type="${type}"` : ''}${media ? ` media="${media}"` : ''}>`;
|
|
3053
|
+
}
|
|
3054
|
+
|
|
2596
3055
|
function injectGeneratorMeta(html, exposeGenerator) {
|
|
2597
3056
|
if (exposeGenerator === false) {
|
|
2598
3057
|
return html;
|
|
2599
3058
|
}
|
|
2600
3059
|
|
|
2601
|
-
return
|
|
3060
|
+
return insertBeforeClosingTag(
|
|
3061
|
+
html,
|
|
3062
|
+
HEAD_CLOSING_TAG_PATTERN,
|
|
3063
|
+
' <meta name="generator" content="ZeroPress">\n',
|
|
3064
|
+
);
|
|
2602
3065
|
}
|
|
2603
3066
|
|
|
2604
3067
|
function injectCustomCssAssetLink(html, href) {
|
|
@@ -2606,24 +3069,66 @@ function injectCustomCssAssetLink(html, href) {
|
|
|
2606
3069
|
return html;
|
|
2607
3070
|
}
|
|
2608
3071
|
|
|
2609
|
-
return
|
|
3072
|
+
return insertBeforeClosingTag(
|
|
3073
|
+
html,
|
|
3074
|
+
HEAD_CLOSING_TAG_PATTERN,
|
|
3075
|
+
` <link rel="stylesheet" href="${escapeHtml(href)}">\n`,
|
|
3076
|
+
);
|
|
2610
3077
|
}
|
|
2611
3078
|
|
|
2612
|
-
function injectCustomHtml(html, customHtml) {
|
|
3079
|
+
function injectCustomHtml(html, customHtml, target) {
|
|
2613
3080
|
let next = html;
|
|
2614
|
-
const headEnd =
|
|
2615
|
-
const bodyEnd =
|
|
3081
|
+
const headEnd = normalizeCustomHtmlSlot(customHtml?.head_end);
|
|
3082
|
+
const bodyEnd = normalizeCustomHtmlSlot(customHtml?.body_end);
|
|
2616
3083
|
|
|
2617
3084
|
if (headEnd) {
|
|
2618
|
-
next =
|
|
3085
|
+
next = injectRequiredCustomHtmlSlot(
|
|
3086
|
+
next,
|
|
3087
|
+
'head_end',
|
|
3088
|
+
headEnd,
|
|
3089
|
+
'head',
|
|
3090
|
+
HEAD_CLOSING_TAG_PATTERN,
|
|
3091
|
+
target,
|
|
3092
|
+
);
|
|
2619
3093
|
}
|
|
2620
3094
|
if (bodyEnd) {
|
|
2621
|
-
next =
|
|
3095
|
+
next = injectRequiredCustomHtmlSlot(
|
|
3096
|
+
next,
|
|
3097
|
+
'body_end',
|
|
3098
|
+
bodyEnd,
|
|
3099
|
+
'body',
|
|
3100
|
+
BODY_CLOSING_TAG_PATTERN,
|
|
3101
|
+
target,
|
|
3102
|
+
);
|
|
2622
3103
|
}
|
|
2623
3104
|
|
|
2624
3105
|
return next;
|
|
2625
3106
|
}
|
|
2626
3107
|
|
|
3108
|
+
function injectRequiredCustomHtmlSlot(html, slot, content, tagName, closingTagPattern, target) {
|
|
3109
|
+
let injected = false;
|
|
3110
|
+
const next = String(html).replace(closingTagPattern, (closingTag) => {
|
|
3111
|
+
injected = true;
|
|
3112
|
+
return `${content}\n${closingTag}`;
|
|
3113
|
+
});
|
|
3114
|
+
|
|
3115
|
+
if (injected) {
|
|
3116
|
+
return next;
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
const route = normalizeOptionalString(target?.route) || '<unknown>';
|
|
3120
|
+
const outputPath = normalizeOptionalString(target?.outputPath) || '<unknown>';
|
|
3121
|
+
throw new Error(
|
|
3122
|
+
`Unable to inject custom_html.${slot} for route "${route}" into output "${outputPath}": `
|
|
3123
|
+
+ `rendered theme HTML is missing a closing </${tagName}> tag. `
|
|
3124
|
+
+ `Add </${tagName}> to the rendered theme layout or remove custom_html.${slot}.`,
|
|
3125
|
+
);
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
function insertBeforeClosingTag(html, closingTagPattern, content) {
|
|
3129
|
+
return String(html).replace(closingTagPattern, (closingTag) => `${content}${closingTag}`);
|
|
3130
|
+
}
|
|
3131
|
+
|
|
2627
3132
|
function buildSearchIndexJson(state) {
|
|
2628
3133
|
return `${JSON.stringify(buildSearchIndexItems(state), null, 2)}\n`;
|
|
2629
3134
|
}
|
|
@@ -2649,18 +3154,26 @@ function buildSearchIndexItems(state) {
|
|
|
2649
3154
|
? state.renderData.frontPageRoute.page
|
|
2650
3155
|
: null;
|
|
2651
3156
|
const frontPageItems = frontPagePage && frontPagePage.status === 'published' && !isDelistedDocument(frontPagePage)
|
|
2652
|
-
? [buildSearchPageItem(
|
|
3157
|
+
? [buildSearchPageItem(
|
|
3158
|
+
frontPagePage,
|
|
3159
|
+
'/',
|
|
3160
|
+
state.renderData.pageReferencePathByPage.get(frontPagePage),
|
|
3161
|
+
)]
|
|
2653
3162
|
: [];
|
|
2654
3163
|
const pageItems = state.renderData.pages
|
|
2655
3164
|
.filter((page) => page.status === 'published' && !isDelistedDocument(page))
|
|
2656
|
-
.map((page) => buildSearchPageItem(
|
|
3165
|
+
.map((page) => buildSearchPageItem(
|
|
3166
|
+
page,
|
|
3167
|
+
page.url,
|
|
3168
|
+
state.renderData.pageReferencePathByPage.get(page),
|
|
3169
|
+
));
|
|
2657
3170
|
|
|
2658
3171
|
return [...posts, ...frontPageItems, ...pageItems];
|
|
2659
3172
|
}
|
|
2660
3173
|
|
|
2661
|
-
function buildSearchPageItem(page, url) {
|
|
3174
|
+
function buildSearchPageItem(page, url, pageReferencePath) {
|
|
2662
3175
|
return {
|
|
2663
|
-
id: `page:${
|
|
3176
|
+
id: `page:${pageReferencePath}`,
|
|
2664
3177
|
type: 'page',
|
|
2665
3178
|
title: page.title,
|
|
2666
3179
|
url,
|
|
@@ -2717,11 +3230,13 @@ function normalizeSearchText(value) {
|
|
|
2717
3230
|
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
2718
3231
|
}
|
|
2719
3232
|
|
|
2720
|
-
function buildSearchAdapterJs() {
|
|
3233
|
+
function buildSearchAdapterJs(locale) {
|
|
2721
3234
|
const fieldWeightsJson = JSON.stringify(SEARCH_FIELD_WEIGHTS, null, 2);
|
|
3235
|
+
const localeJson = JSON.stringify(locale);
|
|
2722
3236
|
return `const FIELD_WEIGHTS = ${fieldWeightsJson};
|
|
2723
3237
|
const FIELD_NAMES = Object.keys(FIELD_WEIGHTS);
|
|
2724
3238
|
const RECENCY_BOOST_MAX = ${SEARCH_RECENCY_BOOST_MAX};
|
|
3239
|
+
const SEGMENTER_LOCALE = ${localeJson};
|
|
2725
3240
|
const DEFAULT_LIMIT = 20;
|
|
2726
3241
|
const BM25_K1 = 1.2;
|
|
2727
3242
|
const BM25_B = 0.75;
|
|
@@ -2961,44 +3476,59 @@ function tokenize(value) {
|
|
|
2961
3476
|
}
|
|
2962
3477
|
|
|
2963
3478
|
const tokens = [];
|
|
3479
|
+
const cjkRuns = Array.from(
|
|
3480
|
+
text.matchAll(/[\\p{Script=Han}\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Hangul}]+/gu),
|
|
3481
|
+
(match) => match[0],
|
|
3482
|
+
);
|
|
3483
|
+
const nonCjkText = text.replace(
|
|
3484
|
+
/[\\p{Script=Han}\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Hangul}]+/gu,
|
|
3485
|
+
' ',
|
|
3486
|
+
);
|
|
3487
|
+
let segmented = false;
|
|
3488
|
+
|
|
2964
3489
|
if (typeof Intl !== 'undefined' && typeof Intl.Segmenter === 'function') {
|
|
2965
3490
|
try {
|
|
2966
|
-
const segmenter = new Intl.Segmenter(
|
|
2967
|
-
for (const part of segmenter.segment(
|
|
3491
|
+
const segmenter = new Intl.Segmenter(SEGMENTER_LOCALE, { granularity: 'word' });
|
|
3492
|
+
for (const part of segmenter.segment(nonCjkText)) {
|
|
2968
3493
|
if (part.isWordLike && isUsefulToken(part.segment)) {
|
|
2969
3494
|
tokens.push(part.segment);
|
|
2970
3495
|
}
|
|
2971
3496
|
}
|
|
3497
|
+
segmented = true;
|
|
2972
3498
|
} catch {
|
|
2973
3499
|
// Fall through to regex tokenization.
|
|
2974
3500
|
}
|
|
2975
3501
|
}
|
|
2976
3502
|
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
3503
|
+
if (!segmented) {
|
|
3504
|
+
for (const match of nonCjkText.matchAll(/[\\p{Letter}\\p{Number}]+/gu)) {
|
|
3505
|
+
if (isUsefulToken(match[0])) {
|
|
3506
|
+
tokens.push(match[0]);
|
|
3507
|
+
}
|
|
2980
3508
|
}
|
|
2981
3509
|
}
|
|
2982
3510
|
|
|
2983
|
-
for (const
|
|
2984
|
-
tokens.push(...
|
|
3511
|
+
for (const run of cjkRuns) {
|
|
3512
|
+
tokens.push(...buildCjkTokens(run));
|
|
2985
3513
|
}
|
|
2986
3514
|
|
|
2987
3515
|
return tokens;
|
|
2988
3516
|
}
|
|
2989
3517
|
|
|
2990
|
-
function
|
|
2991
|
-
const
|
|
2992
|
-
|
|
2993
|
-
|
|
3518
|
+
function buildCjkTokens(value) {
|
|
3519
|
+
const characters = Array.from(value);
|
|
3520
|
+
const tokens = new Set();
|
|
3521
|
+
if (isUsefulToken(value)) {
|
|
3522
|
+
tokens.add(value);
|
|
2994
3523
|
}
|
|
2995
3524
|
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
3525
|
+
if (characters.length > 2) {
|
|
3526
|
+
for (let index = 0; index <= characters.length - 2; index += 1) {
|
|
3527
|
+
tokens.add(characters.slice(index, index + 2).join(''));
|
|
3528
|
+
}
|
|
2999
3529
|
}
|
|
3000
|
-
|
|
3001
|
-
return tokens;
|
|
3530
|
+
|
|
3531
|
+
return Array.from(tokens);
|
|
3002
3532
|
}
|
|
3003
3533
|
|
|
3004
3534
|
function isUsefulToken(value) {
|
|
@@ -3104,7 +3634,7 @@ function normalizeLimit(value) {
|
|
|
3104
3634
|
`;
|
|
3105
3635
|
}
|
|
3106
3636
|
|
|
3107
|
-
function buildSitemapXml(site, emitted,
|
|
3637
|
+
function buildSitemapXml(site, emitted, stylesheetHref = '') {
|
|
3108
3638
|
const entries = [
|
|
3109
3639
|
...(emitted.frontPage && emitted.frontPage.includeInSitemap !== false
|
|
3110
3640
|
? [{
|
|
@@ -3153,7 +3683,7 @@ function buildSitemapXml(site, emitted, generatedAt, stylesheetHref = '') {
|
|
|
3153
3683
|
return `<?xml version="1.0" encoding="UTF-8"?>${stylesheet}\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${body}\n</urlset>`;
|
|
3154
3684
|
}
|
|
3155
3685
|
|
|
3156
|
-
function buildFeedXml(site, emitted,
|
|
3686
|
+
function buildFeedXml(site, emitted, feedGeneratedAt) {
|
|
3157
3687
|
const channelLink = resolveSiteUrl(site.url, '/');
|
|
3158
3688
|
const selfLink = resolveSiteUrl(site.url, '/feed.xml');
|
|
3159
3689
|
const items = [...emitted.posts]
|
|
@@ -3165,12 +3695,12 @@ function buildFeedXml(site, emitted, generatedAt) {
|
|
|
3165
3695
|
})
|
|
3166
3696
|
.join('\n');
|
|
3167
3697
|
|
|
3168
|
-
return `<?xml version="1.0" encoding="UTF-8"?>\n<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">\n <channel>\n <title>${escapeXml(site.title)}</title>\n <link>${escapeXml(channelLink)}</link>\n <description>${escapeXml(site.description)}</description>\n <language>${site.locale}</language>\n <lastBuildDate>${
|
|
3698
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">\n <channel>\n <title>${escapeXml(site.title)}</title>\n <link>${escapeXml(channelLink)}</link>\n <description>${escapeXml(site.description)}</description>\n <language>${site.locale}</language>\n <lastBuildDate>${feedGeneratedAt.toUTCString()}</lastBuildDate>\n <atom:link href="${escapeXml(selfLink)}" rel="self" type="application/rss+xml" />\n${items}\n </channel>\n</rss>`;
|
|
3169
3699
|
}
|
|
3170
3700
|
|
|
3171
3701
|
function buildRobotsTxt(site) {
|
|
3172
3702
|
const lines = ['User-agent: *'];
|
|
3173
|
-
if (site.
|
|
3703
|
+
if (site.robots?.allow_indexing === false) {
|
|
3174
3704
|
lines.push('Disallow: /');
|
|
3175
3705
|
return `${lines.join('\n')}\n`;
|
|
3176
3706
|
}
|
|
@@ -3183,15 +3713,15 @@ function buildRobotsTxt(site) {
|
|
|
3183
3713
|
}
|
|
3184
3714
|
|
|
3185
3715
|
function shouldGenerateRobotsTxt(options) {
|
|
3186
|
-
return options.
|
|
3716
|
+
return options.generateRobotsTxt !== false;
|
|
3187
3717
|
}
|
|
3188
3718
|
|
|
3189
|
-
function shouldGenerateFeed(
|
|
3190
|
-
return
|
|
3719
|
+
function shouldGenerateFeed(state) {
|
|
3720
|
+
return state.previewData.site.feed.enabled === true;
|
|
3191
3721
|
}
|
|
3192
3722
|
|
|
3193
|
-
function shouldGenerateSearchArtifacts(state
|
|
3194
|
-
return
|
|
3723
|
+
function shouldGenerateSearchArtifacts(state) {
|
|
3724
|
+
return state.previewData.site.search.enabled === true;
|
|
3195
3725
|
}
|
|
3196
3726
|
|
|
3197
3727
|
function getContentType(assetPath) {
|
|
@@ -3200,6 +3730,7 @@ function getContentType(assetPath) {
|
|
|
3200
3730
|
html: 'text/html',
|
|
3201
3731
|
css: 'text/css',
|
|
3202
3732
|
js: 'application/javascript',
|
|
3733
|
+
mjs: 'application/javascript',
|
|
3203
3734
|
json: 'application/json',
|
|
3204
3735
|
xml: 'application/xml',
|
|
3205
3736
|
txt: 'text/plain',
|
|
@@ -3240,11 +3771,44 @@ function resolveSiteUrl(siteUrl, relativePath) {
|
|
|
3240
3771
|
}
|
|
3241
3772
|
|
|
3242
3773
|
function hasCanonicalSiteUrl(siteUrl) {
|
|
3243
|
-
|
|
3774
|
+
if (typeof siteUrl !== 'string' || !siteUrl.trim()) {
|
|
3775
|
+
return false;
|
|
3776
|
+
}
|
|
3777
|
+
try {
|
|
3778
|
+
return normalizeSiteOrigin(siteUrl) === siteUrl;
|
|
3779
|
+
} catch {
|
|
3780
|
+
return false;
|
|
3781
|
+
}
|
|
3244
3782
|
}
|
|
3245
3783
|
|
|
3246
3784
|
function toDate(value) {
|
|
3247
|
-
|
|
3785
|
+
if (value instanceof Date) {
|
|
3786
|
+
const timestamp = value.getTime();
|
|
3787
|
+
if (!Number.isFinite(timestamp)) {
|
|
3788
|
+
throw new Error('Invalid RFC 3339 timestamp');
|
|
3789
|
+
}
|
|
3790
|
+
return new Date(timestamp);
|
|
3791
|
+
}
|
|
3792
|
+
|
|
3793
|
+
if (typeof value !== 'string' || !value) {
|
|
3794
|
+
throw new Error(`Invalid RFC 3339 timestamp: ${String(value)}`);
|
|
3795
|
+
}
|
|
3796
|
+
|
|
3797
|
+
const timestampMatch = value.match(RFC3339_TIMESTAMP_PATTERN);
|
|
3798
|
+
if (!timestampMatch) {
|
|
3799
|
+
throw new Error(`Invalid RFC 3339 timestamp: ${value}`);
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3802
|
+
const isLeapSecond = timestampMatch[2] === '60';
|
|
3803
|
+
const parseValue = isLeapSecond
|
|
3804
|
+
? `${timestampMatch[1]}59${timestampMatch[3] || ''}${timestampMatch[4]}`
|
|
3805
|
+
: value;
|
|
3806
|
+
const timestamp = Date.parse(parseValue);
|
|
3807
|
+
if (!Number.isFinite(timestamp)) {
|
|
3808
|
+
throw new Error(`Invalid RFC 3339 timestamp: ${value}`);
|
|
3809
|
+
}
|
|
3810
|
+
|
|
3811
|
+
return new Date(timestamp + (isLeapSecond ? 1000 : 0));
|
|
3248
3812
|
}
|
|
3249
3813
|
|
|
3250
3814
|
function formatUtcIsoSeconds(value) {
|