@zeropress/build-core 0.6.13 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -42
- package/package.json +7 -7
- package/src/api/build-site.js +889 -303
- package/src/index.d.ts +43 -11
- 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/api/build-site.js
CHANGED
|
@@ -8,20 +8,24 @@ import { ZeroPressEngine } from '../render/zeropress-engine.js';
|
|
|
8
8
|
|
|
9
9
|
const DEFAULT_OPTIONS = {
|
|
10
10
|
assetHashing: true,
|
|
11
|
-
generateSpecialFiles: true,
|
|
12
11
|
generateFeed: true,
|
|
13
12
|
generateRobotsTxt: true,
|
|
14
|
-
writeManifest: false,
|
|
15
13
|
};
|
|
16
14
|
|
|
17
15
|
const DEFAULT_POSTS_PER_PAGE = 10;
|
|
18
|
-
const DEFAULT_DATETIME_DISPLAY = 'static';
|
|
19
16
|
const DEFAULT_DATE_STYLE = 'medium';
|
|
20
17
|
const DEFAULT_TIME_STYLE = 'none';
|
|
21
18
|
const DEFAULT_TIMEZONE = 'UTC';
|
|
22
19
|
const DEFAULT_LOCALE = 'en-US';
|
|
23
|
-
const
|
|
20
|
+
const DEFAULT_COMMENTS_PROVIDER = 'zeropress';
|
|
21
|
+
const DEFAULT_COMMENTS_PER_PAGE = 50;
|
|
22
|
+
const DEFAULT_COMMENTS_ORDER = 'desc';
|
|
23
|
+
const DEFAULT_COMMENTS_THREADING_ENABLED = true;
|
|
24
|
+
const DEFAULT_COMMENTS_THREADING_MAX_DEPTH = 2;
|
|
24
25
|
const DATETIME_STYLES = new Set(['none', 'short', 'medium', 'long', 'full']);
|
|
26
|
+
const COMMENTS_PROVIDERS = new Set(['zeropress', 'wordpress']);
|
|
27
|
+
const COMMENTS_ORDERS = new Set(['asc', 'desc']);
|
|
28
|
+
const DISABLED_COMMENTS_CONTEXT = Object.freeze({ enabled: false });
|
|
25
29
|
const DEFAULT_PERMALINKS = Object.freeze({
|
|
26
30
|
output_style: 'directory',
|
|
27
31
|
posts: '/posts/:slug/',
|
|
@@ -43,9 +47,12 @@ const SEARCH_ADAPTER_OUTPUT_PATH = '_zeropress/search.js';
|
|
|
43
47
|
const SEARCH_PAGEFIND_ADAPTER_OUTPUT_PATH = '_zeropress/search_pagefind.js';
|
|
44
48
|
const OUTPUT_PATH_CONTROL_CHAR_PATTERN = /[\u0000-\u001F\u007F]/;
|
|
45
49
|
const SAFE_MEDIA_PROTOCOLS = new Set(['http:', 'https:']);
|
|
46
|
-
const SAFE_LINK_PROTOCOLS = new Set(['http:', 'https:'
|
|
50
|
+
const SAFE_LINK_PROTOCOLS = new Set(['http:', 'https:']);
|
|
47
51
|
const MEDIA_DELIVERY_MODES = new Set(['none', 'media_domain']);
|
|
48
52
|
const DISCOVERABILITY_VALUES = new Set(['default', 'noindex', 'delist']);
|
|
53
|
+
const CUSTOM_HTML_SLOT_MAX_CODE_POINTS = 65_536;
|
|
54
|
+
const HEAD_CLOSING_TAG_PATTERN = /<\/head\s*>/i;
|
|
55
|
+
const BODY_CLOSING_TAG_PATTERN = /<\/body\s*>/i;
|
|
49
56
|
const RESPONSIVE_IMAGE_WIDTHS = [320, 480, 768, 1024, 1280, 1600, 1920];
|
|
50
57
|
const RESPONSIVE_IMAGE_EXTENSIONS = new Set(['jpg', 'jpeg', 'png', 'webp', 'avif']);
|
|
51
58
|
const SEARCH_FIELD_WEIGHTS = Object.freeze({
|
|
@@ -100,32 +107,32 @@ export async function buildSite(input) {
|
|
|
100
107
|
await writeOutput(state.writer, state.summaries, assetOutput.path, assetOutput.content, assetOutput.contentType);
|
|
101
108
|
}
|
|
102
109
|
|
|
103
|
-
if (shouldGenerateSearchArtifacts(state
|
|
110
|
+
if (shouldGenerateSearchArtifacts(state)) {
|
|
104
111
|
await writeOutput(state.writer, state.summaries, SEARCH_INDEX_OUTPUT_PATH, buildSearchIndexJson(state), 'application/json');
|
|
105
112
|
await writeOutput(state.writer, state.summaries, SEARCH_ADAPTER_OUTPUT_PATH, buildSearchAdapterJs(), 'application/javascript');
|
|
106
113
|
await writeOutput(state.writer, state.summaries, SEARCH_PAGEFIND_ADAPTER_OUTPUT_PATH, buildSearchPagefindAdapterJs(), 'application/javascript');
|
|
107
114
|
}
|
|
108
115
|
|
|
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');
|
|
116
|
+
await maybeRenderNotFoundPage(state);
|
|
117
|
+
if (hasCanonicalSiteUrl(state.previewData.site.url)) {
|
|
118
|
+
await writeOutput(
|
|
119
|
+
state.writer,
|
|
120
|
+
state.summaries,
|
|
121
|
+
'sitemap.xml',
|
|
122
|
+
buildSitemapXml(state.previewData.site, state.emitted, state.generatedAt, options.sitemapStylesheetHref),
|
|
123
|
+
'application/xml',
|
|
124
|
+
);
|
|
125
|
+
if (shouldGenerateFeed(state)) {
|
|
126
|
+
await writeOutput(state.writer, state.summaries, 'feed.xml', buildFeedXml(state.previewData.site, state.emitted, state.generatedAt), 'application/rss+xml');
|
|
125
127
|
}
|
|
126
128
|
}
|
|
129
|
+
if (shouldGenerateRobotsTxt(options)) {
|
|
130
|
+
await writeOutput(state.writer, state.summaries, 'robots.txt', buildRobotsTxt(state.previewData.site), 'text/plain');
|
|
131
|
+
}
|
|
127
132
|
|
|
128
|
-
return
|
|
133
|
+
return {
|
|
134
|
+
files: state.summaries,
|
|
135
|
+
};
|
|
129
136
|
}
|
|
130
137
|
|
|
131
138
|
async function createBuildState(input, options) {
|
|
@@ -140,7 +147,7 @@ async function createBuildState(input, options) {
|
|
|
140
147
|
const assetProcessor = new AssetProcessor();
|
|
141
148
|
const summaries = [];
|
|
142
149
|
const previewData = normalizePreviewData(input.previewData, options);
|
|
143
|
-
const renderData = createRenderData(previewData, themePackage
|
|
150
|
+
const renderData = createRenderData(previewData, themePackage, options);
|
|
144
151
|
|
|
145
152
|
engine.initialize(themePackage);
|
|
146
153
|
|
|
@@ -193,30 +200,9 @@ function assertBuildPreviewData(previewData) {
|
|
|
193
200
|
}
|
|
194
201
|
}
|
|
195
202
|
|
|
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
203
|
async function renderRoute(state, templateName, route) {
|
|
219
204
|
const currentUrl = routePathToPublicUrl(route.path, state.previewData.site.permalinks.output_style);
|
|
205
|
+
const outputPath = routePathToOutputPath(route.path, state.previewData.site.permalinks.output_style);
|
|
220
206
|
const routeContext = buildRouteContext(route.route_type || templateName, currentUrl, {
|
|
221
207
|
isFrontPage: route.is_front_page === true,
|
|
222
208
|
isPostIndex: route.is_post_index === true,
|
|
@@ -242,8 +228,11 @@ async function renderRoute(state, templateName, route) {
|
|
|
242
228
|
createRenderContext(state.previewData.site, currentUrl),
|
|
243
229
|
);
|
|
244
230
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
245
|
-
html = injectSiteCustomizations(html, state
|
|
246
|
-
|
|
231
|
+
html = injectSiteCustomizations(html, state, {
|
|
232
|
+
route: currentUrl,
|
|
233
|
+
outputPath,
|
|
234
|
+
});
|
|
235
|
+
await writeOutput(state.writer, state.summaries, outputPath, html, 'text/html');
|
|
247
236
|
recordRouteEmission(state, templateName, route, currentUrl);
|
|
248
237
|
}
|
|
249
238
|
|
|
@@ -288,10 +277,17 @@ async function renderFrontPage(state, route) {
|
|
|
288
277
|
robotsNoindex: shouldNoindexDocument(page),
|
|
289
278
|
}),
|
|
290
279
|
},
|
|
291
|
-
createRenderContext(
|
|
280
|
+
createRenderContext(
|
|
281
|
+
state.previewData.site,
|
|
282
|
+
currentUrl,
|
|
283
|
+
getTargetCommentsContext(state, 'page', state.renderData.pageReferencePathByPage.get(route.page)),
|
|
284
|
+
),
|
|
292
285
|
);
|
|
293
286
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
294
|
-
html = injectSiteCustomizations(html, state
|
|
287
|
+
html = injectSiteCustomizations(html, state, {
|
|
288
|
+
route: currentUrl,
|
|
289
|
+
outputPath: 'index.html',
|
|
290
|
+
});
|
|
295
291
|
await writeOutput(state.writer, state.summaries, 'index.html', html, 'text/html');
|
|
296
292
|
state.emitted.frontPage = {
|
|
297
293
|
url: currentUrl,
|
|
@@ -323,7 +319,10 @@ async function renderFrontPage(state, route) {
|
|
|
323
319
|
createRenderContext(state.previewData.site, currentUrl),
|
|
324
320
|
);
|
|
325
321
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
326
|
-
html = injectSiteCustomizations(html, state
|
|
322
|
+
html = injectSiteCustomizations(html, state, {
|
|
323
|
+
route: currentUrl,
|
|
324
|
+
outputPath: 'index.html',
|
|
325
|
+
});
|
|
327
326
|
await writeOutput(state.writer, state.summaries, 'index.html', html, 'text/html');
|
|
328
327
|
state.emitted.frontPage = {
|
|
329
328
|
url: currentUrl,
|
|
@@ -335,6 +334,7 @@ async function renderFrontPage(state, route) {
|
|
|
335
334
|
|
|
336
335
|
async function renderPost(state, post) {
|
|
337
336
|
const currentUrl = post.url;
|
|
337
|
+
const outputPath = routePathToOutputPath(post.url, state.previewData.site.permalinks.output_style);
|
|
338
338
|
let html = await state.engine.render(
|
|
339
339
|
'post',
|
|
340
340
|
{
|
|
@@ -355,11 +355,18 @@ async function renderPost(state, post) {
|
|
|
355
355
|
robotsNoindex: shouldNoindexDocument(post),
|
|
356
356
|
}),
|
|
357
357
|
},
|
|
358
|
-
createRenderContext(
|
|
358
|
+
createRenderContext(
|
|
359
|
+
state.previewData.site,
|
|
360
|
+
currentUrl,
|
|
361
|
+
getTargetCommentsContext(state, 'post', post.slug),
|
|
362
|
+
),
|
|
359
363
|
);
|
|
360
364
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
361
|
-
html = injectSiteCustomizations(html, state
|
|
362
|
-
|
|
365
|
+
html = injectSiteCustomizations(html, state, {
|
|
366
|
+
route: currentUrl,
|
|
367
|
+
outputPath,
|
|
368
|
+
});
|
|
369
|
+
await writeOutput(state.writer, state.summaries, outputPath, html, 'text/html');
|
|
363
370
|
if (!isDelistedDocument(post)) {
|
|
364
371
|
state.emitted.posts.push({
|
|
365
372
|
url: currentUrl,
|
|
@@ -395,10 +402,17 @@ async function renderPage(state, page) {
|
|
|
395
402
|
robotsNoindex: shouldNoindexDocument(page),
|
|
396
403
|
}),
|
|
397
404
|
},
|
|
398
|
-
createRenderContext(
|
|
405
|
+
createRenderContext(
|
|
406
|
+
state.previewData.site,
|
|
407
|
+
currentUrl,
|
|
408
|
+
getTargetCommentsContext(state, 'page', state.renderData.pageReferencePathByPage.get(page)),
|
|
409
|
+
),
|
|
399
410
|
);
|
|
400
411
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
401
|
-
html = injectSiteCustomizations(html, state
|
|
412
|
+
html = injectSiteCustomizations(html, state, {
|
|
413
|
+
route: currentUrl,
|
|
414
|
+
outputPath,
|
|
415
|
+
});
|
|
402
416
|
await writeOutput(state.writer, state.summaries, outputPath, html, 'text/html');
|
|
403
417
|
state.emitted.pages.push({
|
|
404
418
|
url: currentUrl,
|
|
@@ -425,53 +439,72 @@ async function maybeRenderNotFoundPage(state) {
|
|
|
425
439
|
route: buildRouteContext('not_found', '/404.html'),
|
|
426
440
|
meta: buildPageMeta(state.previewData.site, {
|
|
427
441
|
currentUrl: '/404.html',
|
|
428
|
-
title: state.previewData.site.title,
|
|
429
|
-
|
|
430
|
-
|
|
442
|
+
title: buildDocumentTitle('Page Not Found', state.previewData.site.title),
|
|
443
|
+
robotsNoindex: true,
|
|
444
|
+
includeRichMetadata: false,
|
|
445
|
+
includeFeedLink: false,
|
|
431
446
|
}),
|
|
432
447
|
},
|
|
433
448
|
createRenderContext(state.previewData.site, '/404.html'),
|
|
434
449
|
);
|
|
435
450
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
436
|
-
html = injectSiteCustomizations(html, state
|
|
451
|
+
html = injectSiteCustomizations(html, state, {
|
|
452
|
+
route: '/404.html',
|
|
453
|
+
outputPath: '404.html',
|
|
454
|
+
});
|
|
437
455
|
await writeOutput(state.writer, state.summaries, '404.html', html, 'text/html');
|
|
438
456
|
}
|
|
439
457
|
|
|
440
458
|
function normalizePreviewData(previewData, options = {}) {
|
|
441
|
-
const
|
|
459
|
+
const media_origin = normalizeMediaOrigin(previewData.site.media_origin);
|
|
460
|
+
const mediaDeliveryMode = MEDIA_DELIVERY_MODES.has(previewData.site.media_delivery_mode)
|
|
461
|
+
? previewData.site.media_delivery_mode
|
|
462
|
+
: 'none';
|
|
463
|
+
if (mediaDeliveryMode === 'media_domain' && !media_origin) {
|
|
464
|
+
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".');
|
|
465
|
+
}
|
|
466
|
+
const {
|
|
467
|
+
search: siteSearch,
|
|
468
|
+
feed: siteFeed,
|
|
469
|
+
archive: siteArchive,
|
|
470
|
+
comments: siteComments,
|
|
471
|
+
robots: siteRobots,
|
|
472
|
+
...siteFields
|
|
473
|
+
} = previewData.site;
|
|
474
|
+
const normalizedComments = normalizeSiteComments(siteComments);
|
|
442
475
|
const normalizedSite = {
|
|
443
|
-
...
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
: 'none',
|
|
476
|
+
...siteFields,
|
|
477
|
+
url: normalizeSiteOrigin(previewData.site.url),
|
|
478
|
+
media_origin,
|
|
479
|
+
media_delivery_mode: mediaDeliveryMode,
|
|
448
480
|
favicon: previewData.site.favicon
|
|
449
|
-
? normalizeSiteFavicon(previewData.site.favicon,
|
|
481
|
+
? normalizeSiteFavicon(previewData.site.favicon, media_origin)
|
|
450
482
|
: normalizeSiteFavicon(options.favicon, ''),
|
|
451
|
-
logo: normalizeSiteLogo(previewData.site.logo,
|
|
483
|
+
logo: normalizeSiteLogo(previewData.site.logo, media_origin),
|
|
452
484
|
newsletter: normalizeSiteNewsletter(previewData.site.newsletter),
|
|
453
485
|
posts_per_page: Number.isInteger(previewData.site.posts_per_page) && previewData.site.posts_per_page > 0
|
|
454
486
|
? previewData.site.posts_per_page
|
|
455
487
|
: 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
488
|
date_style: DATETIME_STYLES.has(previewData.site.date_style)
|
|
460
489
|
? previewData.site.date_style
|
|
461
490
|
: DEFAULT_DATE_STYLE,
|
|
462
491
|
time_style: DATETIME_STYLES.has(previewData.site.time_style)
|
|
463
492
|
? previewData.site.time_style
|
|
464
493
|
: DEFAULT_TIME_STYLE,
|
|
465
|
-
timezone:
|
|
466
|
-
locale: normalizeLocale(previewData.site.locale
|
|
467
|
-
disallow_comments: previewData.site.disallow_comments === true,
|
|
494
|
+
timezone: normalizeTimezone(previewData.site.timezone),
|
|
495
|
+
locale: normalizeLocale(previewData.site.locale),
|
|
468
496
|
expose_generator: previewData.site.expose_generator !== false,
|
|
469
|
-
search:
|
|
470
|
-
|
|
497
|
+
search: normalizeRequestedFeatureState(siteSearch),
|
|
498
|
+
feed: normalizeRequestedFeatureState(siteFeed),
|
|
499
|
+
archive: normalizeRequestedFeatureState(siteArchive),
|
|
500
|
+
robots: {
|
|
501
|
+
allow_indexing: siteRobots?.allow_indexing !== false,
|
|
502
|
+
},
|
|
471
503
|
permalinks: normalizePermalinks(previewData.site.permalinks),
|
|
472
504
|
front_page: normalizeFrontPage(previewData.site.front_page),
|
|
473
505
|
post_index: normalizePostIndex(previewData.site.post_index),
|
|
474
506
|
footer: normalizeSiteFooter(previewData.site.footer),
|
|
507
|
+
...(normalizedComments ? { comments: normalizedComments } : {}),
|
|
475
508
|
};
|
|
476
509
|
const media = normalizeContentMedia(previewData.content.media, normalizedSite);
|
|
477
510
|
const mediaRegistry = buildMediaRegistry(media);
|
|
@@ -479,15 +512,15 @@ function normalizePreviewData(previewData, options = {}) {
|
|
|
479
512
|
return {
|
|
480
513
|
...previewData,
|
|
481
514
|
site: normalizedSite,
|
|
482
|
-
menus:
|
|
515
|
+
menus: normalizeMenus(previewData.menus),
|
|
483
516
|
collections: normalizeCollections(previewData.collections),
|
|
484
|
-
widgets: normalizeWidgetAreas(previewData.widgets, normalizedSite.
|
|
517
|
+
widgets: normalizeWidgetAreas(previewData.widgets, normalizedSite.media_origin),
|
|
485
518
|
custom_css: normalizeCustomCss(previewData.custom_css),
|
|
486
519
|
custom_html: normalizeCustomHtml(previewData.custom_html),
|
|
487
520
|
content: {
|
|
488
521
|
...previewData.content,
|
|
489
522
|
authors: previewData.content.authors.map((author) => {
|
|
490
|
-
const avatar = normalizeMediaField(author.avatar, normalizedSite.
|
|
523
|
+
const avatar = normalizeMediaField(author.avatar, normalizedSite.media_origin);
|
|
491
524
|
const avatarMedia = deriveManagedMedia(avatar, mediaRegistry, normalizedSite);
|
|
492
525
|
return {
|
|
493
526
|
...author,
|
|
@@ -497,27 +530,35 @@ function normalizePreviewData(previewData, options = {}) {
|
|
|
497
530
|
}),
|
|
498
531
|
posts: previewData.content.posts
|
|
499
532
|
.map((post) => {
|
|
500
|
-
const
|
|
533
|
+
const { comments, ...postFields } = post;
|
|
534
|
+
const normalizedPostComments = normalizeTargetComments(comments);
|
|
535
|
+
const featuredImage = normalizeMediaField(post.featured_image, normalizedSite.media_origin);
|
|
501
536
|
const featuredMedia = deriveManagedMedia(featuredImage, mediaRegistry, normalizedSite);
|
|
502
537
|
return {
|
|
503
|
-
...
|
|
538
|
+
...postFields,
|
|
504
539
|
published_at_iso: normalizeIsoTimestamp(post.published_at_iso),
|
|
505
540
|
updated_at_iso: normalizeIsoTimestamp(post.updated_at_iso),
|
|
541
|
+
allow_comments: post.allow_comments === true,
|
|
506
542
|
discoverability: normalizeDiscoverability(post.discoverability),
|
|
507
543
|
featured_image: featuredImage,
|
|
508
544
|
...(featuredMedia ? { featured_media: featuredMedia } : {}),
|
|
545
|
+
...(normalizedPostComments ? { comments: normalizedPostComments } : {}),
|
|
509
546
|
};
|
|
510
547
|
})
|
|
511
548
|
.sort((left, right) => toDate(right.published_at_iso).getTime() - toDate(left.published_at_iso).getTime()),
|
|
512
549
|
pages: previewData.content.pages.map((page) => {
|
|
513
|
-
const
|
|
550
|
+
const { comments, ...pageFields } = page;
|
|
551
|
+
const normalizedPageComments = normalizeTargetComments(comments);
|
|
552
|
+
const featuredImage = normalizeMediaField(page.featured_image, normalizedSite.media_origin);
|
|
514
553
|
const featuredMedia = deriveManagedMedia(featuredImage, mediaRegistry, normalizedSite);
|
|
515
554
|
return {
|
|
516
|
-
...
|
|
555
|
+
...pageFields,
|
|
517
556
|
...(page.updated_at_iso ? { updated_at_iso: normalizeIsoTimestamp(page.updated_at_iso) } : {}),
|
|
557
|
+
allow_comments: page.allow_comments === true,
|
|
518
558
|
discoverability: normalizeDiscoverability(page.discoverability),
|
|
519
559
|
featured_image: featuredImage,
|
|
520
560
|
...(featuredMedia ? { featured_media: featuredMedia } : {}),
|
|
561
|
+
...(normalizedPageComments ? { comments: normalizedPageComments } : {}),
|
|
521
562
|
};
|
|
522
563
|
}),
|
|
523
564
|
categories: [...previewData.content.categories],
|
|
@@ -535,6 +576,22 @@ function normalizeRecordMap(value) {
|
|
|
535
576
|
return { ...value };
|
|
536
577
|
}
|
|
537
578
|
|
|
579
|
+
function normalizeMenus(menus) {
|
|
580
|
+
const normalized = normalizeRecordMap(menus);
|
|
581
|
+
return Object.fromEntries(Object.entries(normalized).map(([menuId, menu]) => [
|
|
582
|
+
menuId,
|
|
583
|
+
{
|
|
584
|
+
...menu,
|
|
585
|
+
items: Array.isArray(menu?.items)
|
|
586
|
+
? menu.items.map((item) => ({
|
|
587
|
+
...item,
|
|
588
|
+
url: normalizeNavigationUrl(item?.url),
|
|
589
|
+
}))
|
|
590
|
+
: [],
|
|
591
|
+
},
|
|
592
|
+
]));
|
|
593
|
+
}
|
|
594
|
+
|
|
538
595
|
function normalizeDiscoverability(value) {
|
|
539
596
|
return DISCOVERABILITY_VALUES.has(value) ? value : 'default';
|
|
540
597
|
}
|
|
@@ -557,7 +614,7 @@ function normalizeContentMedia(mediaItems, site) {
|
|
|
557
614
|
if (!item || typeof item !== 'object') {
|
|
558
615
|
return null;
|
|
559
616
|
}
|
|
560
|
-
const src = normalizeMediaField(item.src, site.
|
|
617
|
+
const src = normalizeMediaField(item.src, site.media_origin);
|
|
561
618
|
const width = Number.isInteger(item.width) && item.width > 0 ? item.width : 0;
|
|
562
619
|
const height = Number.isInteger(item.height) && item.height > 0 ? item.height : 0;
|
|
563
620
|
if (!src || !width || !height) {
|
|
@@ -603,8 +660,8 @@ function buildResponsiveImageSrcset(media, site) {
|
|
|
603
660
|
return '';
|
|
604
661
|
}
|
|
605
662
|
|
|
606
|
-
const
|
|
607
|
-
if (!
|
|
663
|
+
const media_origin = normalizeMediaOrigin(site.media_origin);
|
|
664
|
+
if (!media_origin || !isUrlAtMediaOrigin(media.src, media_origin) || !isResponsiveRasterImage(media.src)) {
|
|
608
665
|
return '';
|
|
609
666
|
}
|
|
610
667
|
|
|
@@ -631,12 +688,10 @@ function buildResponsiveImageVariantUrl(src, width) {
|
|
|
631
688
|
}
|
|
632
689
|
}
|
|
633
690
|
|
|
634
|
-
function
|
|
691
|
+
function isUrlAtMediaOrigin(src, media_origin) {
|
|
635
692
|
try {
|
|
636
693
|
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);
|
|
694
|
+
return sourceUrl.origin === media_origin;
|
|
640
695
|
} catch {
|
|
641
696
|
return false;
|
|
642
697
|
}
|
|
@@ -671,7 +726,7 @@ function normalizeCollections(collections) {
|
|
|
671
726
|
);
|
|
672
727
|
}
|
|
673
728
|
|
|
674
|
-
function normalizeWidgetAreas(widget_areas,
|
|
729
|
+
function normalizeWidgetAreas(widget_areas, media_origin) {
|
|
675
730
|
if (!widget_areas || typeof widget_areas !== 'object') {
|
|
676
731
|
return {};
|
|
677
732
|
}
|
|
@@ -683,7 +738,7 @@ function normalizeWidgetAreas(widget_areas, media_base_url) {
|
|
|
683
738
|
...widgetArea,
|
|
684
739
|
name: normalizeNonEmptyString(widgetArea?.name, widgetAreaId),
|
|
685
740
|
items: Array.isArray(widgetArea?.items)
|
|
686
|
-
? widgetArea.items.map((item) => normalizeWidgetItem(item,
|
|
741
|
+
? widgetArea.items.map((item) => normalizeWidgetItem(item, media_origin))
|
|
687
742
|
: [],
|
|
688
743
|
},
|
|
689
744
|
]),
|
|
@@ -700,7 +755,7 @@ function normalizeSiteFooter(footer) {
|
|
|
700
755
|
};
|
|
701
756
|
}
|
|
702
757
|
|
|
703
|
-
function normalizeWidgetItem(item,
|
|
758
|
+
function normalizeWidgetItem(item, media_origin) {
|
|
704
759
|
const normalizedItem = {
|
|
705
760
|
...item,
|
|
706
761
|
title: typeof item?.title === 'string' ? item.title.trim() : '',
|
|
@@ -711,7 +766,7 @@ function normalizeWidgetItem(item, media_base_url) {
|
|
|
711
766
|
...normalizedItem,
|
|
712
767
|
settings: {
|
|
713
768
|
...item.settings,
|
|
714
|
-
avatar: normalizeMediaField(item.settings.avatar,
|
|
769
|
+
avatar: normalizeMediaField(item.settings.avatar, media_origin),
|
|
715
770
|
},
|
|
716
771
|
};
|
|
717
772
|
}
|
|
@@ -725,44 +780,52 @@ function normalizeCustomCss(customCss) {
|
|
|
725
780
|
}
|
|
726
781
|
|
|
727
782
|
function normalizeCustomHtml(customHtml) {
|
|
728
|
-
if (!customHtml || typeof customHtml !== 'object') {
|
|
783
|
+
if (!customHtml || typeof customHtml !== 'object' || Array.isArray(customHtml)) {
|
|
729
784
|
return undefined;
|
|
730
785
|
}
|
|
731
786
|
|
|
732
|
-
const headEnd =
|
|
733
|
-
const bodyEnd =
|
|
787
|
+
const headEnd = normalizeCustomHtmlSlot(customHtml.head_end);
|
|
788
|
+
const bodyEnd = normalizeCustomHtmlSlot(customHtml.body_end);
|
|
734
789
|
if (!headEnd && !bodyEnd) {
|
|
735
790
|
return undefined;
|
|
736
791
|
}
|
|
737
792
|
|
|
738
793
|
return {
|
|
739
|
-
...(headEnd ? { head_end:
|
|
740
|
-
...(bodyEnd ? { body_end:
|
|
794
|
+
...(headEnd ? { head_end: headEnd } : {}),
|
|
795
|
+
...(bodyEnd ? { body_end: bodyEnd } : {}),
|
|
741
796
|
};
|
|
742
797
|
}
|
|
743
798
|
|
|
744
|
-
function
|
|
799
|
+
function normalizeCustomHtmlSlot(value) {
|
|
800
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
801
|
+
return '';
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
return [...value].length <= CUSTOM_HTML_SLOT_MAX_CODE_POINTS ? value : '';
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function normalizeSiteFavicon(favicon, media_origin) {
|
|
745
808
|
if (!favicon || typeof favicon !== 'object') {
|
|
746
809
|
return undefined;
|
|
747
810
|
}
|
|
748
811
|
|
|
749
812
|
const normalized = {};
|
|
750
|
-
for (const key of ['icon', 'svg', 'png', 'apple_touch_icon']) {
|
|
813
|
+
for (const key of ['icon', 'icon_dark', 'svg', 'png', 'apple_touch_icon']) {
|
|
751
814
|
const value = normalizeOptionalString(favicon[key]);
|
|
752
815
|
if (value) {
|
|
753
|
-
normalized[key] = normalizeMediaField(value,
|
|
816
|
+
normalized[key] = normalizeMediaField(value, media_origin);
|
|
754
817
|
}
|
|
755
818
|
}
|
|
756
819
|
|
|
757
820
|
return Object.keys(normalized).length ? normalized : undefined;
|
|
758
821
|
}
|
|
759
822
|
|
|
760
|
-
function normalizeSiteLogo(logo,
|
|
823
|
+
function normalizeSiteLogo(logo, media_origin) {
|
|
761
824
|
if (!logo || typeof logo !== 'object') {
|
|
762
825
|
return undefined;
|
|
763
826
|
}
|
|
764
827
|
|
|
765
|
-
const src = normalizeMediaField(logo.src,
|
|
828
|
+
const src = normalizeMediaField(logo.src, media_origin);
|
|
766
829
|
if (!src) {
|
|
767
830
|
return undefined;
|
|
768
831
|
}
|
|
@@ -782,8 +845,8 @@ function normalizeSiteNewsletter(newsletter) {
|
|
|
782
845
|
const title = normalizeOptionalString(newsletter.title);
|
|
783
846
|
const description = normalizeOptionalString(newsletter.description);
|
|
784
847
|
const buttonLabel = normalizeOptionalString(newsletter.button_label);
|
|
785
|
-
const signupUrl =
|
|
786
|
-
const embedUrl =
|
|
848
|
+
const signupUrl = normalizeNavigationUrl(newsletter.signup_url);
|
|
849
|
+
const embedUrl = normalizeNavigationUrl(newsletter.embed_url);
|
|
787
850
|
|
|
788
851
|
return {
|
|
789
852
|
enabled: newsletter.enabled === true,
|
|
@@ -795,6 +858,66 @@ function normalizeSiteNewsletter(newsletter) {
|
|
|
795
858
|
};
|
|
796
859
|
}
|
|
797
860
|
|
|
861
|
+
function normalizeSiteComments(comments) {
|
|
862
|
+
if (!comments || typeof comments !== 'object' || Array.isArray(comments)) {
|
|
863
|
+
return undefined;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
const apiBaseUrl = normalizeCommentsApiBaseUrl(comments.api_base_url);
|
|
867
|
+
if (!apiBaseUrl) {
|
|
868
|
+
return undefined;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const threading = comments.threading && typeof comments.threading === 'object' && !Array.isArray(comments.threading)
|
|
872
|
+
? comments.threading
|
|
873
|
+
: {};
|
|
874
|
+
|
|
875
|
+
return {
|
|
876
|
+
enabled: comments.enabled === true,
|
|
877
|
+
provider: COMMENTS_PROVIDERS.has(comments.provider)
|
|
878
|
+
? comments.provider
|
|
879
|
+
: DEFAULT_COMMENTS_PROVIDER,
|
|
880
|
+
api_base_url: apiBaseUrl,
|
|
881
|
+
per_page: Number.isInteger(comments.per_page) && comments.per_page >= 1 && comments.per_page <= 100
|
|
882
|
+
? comments.per_page
|
|
883
|
+
: DEFAULT_COMMENTS_PER_PAGE,
|
|
884
|
+
order: COMMENTS_ORDERS.has(comments.order)
|
|
885
|
+
? comments.order
|
|
886
|
+
: DEFAULT_COMMENTS_ORDER,
|
|
887
|
+
threading: {
|
|
888
|
+
enabled: typeof threading.enabled === 'boolean'
|
|
889
|
+
? threading.enabled
|
|
890
|
+
: DEFAULT_COMMENTS_THREADING_ENABLED,
|
|
891
|
+
max_depth: Number.isInteger(threading.max_depth) && threading.max_depth >= 2 && threading.max_depth <= 10
|
|
892
|
+
? threading.max_depth
|
|
893
|
+
: DEFAULT_COMMENTS_THREADING_MAX_DEPTH,
|
|
894
|
+
},
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function normalizeRequestedFeatureState(value) {
|
|
899
|
+
return {
|
|
900
|
+
enabled: value?.enabled !== false,
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function normalizeCommentsApiBaseUrl(value) {
|
|
905
|
+
const normalized = normalizeOptionalString(value);
|
|
906
|
+
if (!normalized || normalized === '/') {
|
|
907
|
+
return normalized;
|
|
908
|
+
}
|
|
909
|
+
return normalized.replace(/\/+$/u, '');
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function normalizeTargetComments(comments) {
|
|
913
|
+
if (!comments || typeof comments !== 'object' || Array.isArray(comments)) {
|
|
914
|
+
return undefined;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
const requestToken = preserveOpaqueNonBlankString(comments.request_token);
|
|
918
|
+
return requestToken ? { request_token: requestToken } : undefined;
|
|
919
|
+
}
|
|
920
|
+
|
|
798
921
|
function normalizePermalinks(permalinks) {
|
|
799
922
|
const source = permalinks && typeof permalinks === 'object' ? permalinks : {};
|
|
800
923
|
const outputStyle = typeof source.output_style === 'string' && PERMALINK_OUTPUT_STYLES.has(source.output_style)
|
|
@@ -821,7 +944,7 @@ function normalizeFrontPage(frontPage) {
|
|
|
821
944
|
|
|
822
945
|
return {
|
|
823
946
|
type,
|
|
824
|
-
...(type === 'page' ? {
|
|
947
|
+
...(type === 'page' ? { page_path: normalizePageReferencePath(frontPage.page_path) } : {}),
|
|
825
948
|
...(type === 'standalone_html' ? { html: normalizeOptionalRawString(frontPage.html) } : {}),
|
|
826
949
|
};
|
|
827
950
|
}
|
|
@@ -838,11 +961,31 @@ function normalizePostIndex(post_index) {
|
|
|
838
961
|
};
|
|
839
962
|
}
|
|
840
963
|
|
|
841
|
-
function createRenderData(previewData,
|
|
964
|
+
function createRenderData(previewData, themePackage = {}, options = {}) {
|
|
965
|
+
const themeMetadata = themePackage.metadata || {};
|
|
842
966
|
const themeSupportsComments = themeMetadata?.features?.comments === true;
|
|
843
967
|
const themeSupportsPostIndex = themeMetadata?.features?.post_index !== false;
|
|
844
968
|
const themeSupportsSearch = themeMetadata?.features?.search === true;
|
|
845
|
-
|
|
969
|
+
const outputStyle = previewData.site.permalinks.output_style;
|
|
970
|
+
const requestedComments = previewData.site.comments;
|
|
971
|
+
previewData.site.search = {
|
|
972
|
+
enabled: previewData.site.search.enabled === true && themeSupportsSearch,
|
|
973
|
+
};
|
|
974
|
+
previewData.site.feed = previewData.site.feed.enabled === true
|
|
975
|
+
&& hasCanonicalSiteUrl(previewData.site.url)
|
|
976
|
+
&& options.generateFeed !== false
|
|
977
|
+
? { enabled: true, url: '/feed.xml' }
|
|
978
|
+
: { enabled: false };
|
|
979
|
+
previewData.site.archive = previewData.site.archive.enabled === true
|
|
980
|
+
&& themePackage.templates?.has('archive') === true
|
|
981
|
+
? {
|
|
982
|
+
enabled: true,
|
|
983
|
+
url: outputStyle === 'html-extension' ? '/archive' : '/archive/',
|
|
984
|
+
}
|
|
985
|
+
: { enabled: false };
|
|
986
|
+
previewData.site.comments = requestedComments?.enabled === true && themeSupportsComments
|
|
987
|
+
? { ...requestedComments, enabled: true }
|
|
988
|
+
: { enabled: false };
|
|
846
989
|
const authorsById = new Map(previewData.content.authors.map((author) => [author.id, author]));
|
|
847
990
|
const categoriesBySlug = new Map(previewData.content.categories.map((category) => [category.slug, category]));
|
|
848
991
|
const tagsBySlug = new Map(previewData.content.tags.map((tag) => [tag.slug, tag]));
|
|
@@ -864,7 +1007,38 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
864
1007
|
}
|
|
865
1008
|
}
|
|
866
1009
|
|
|
867
|
-
const
|
|
1010
|
+
const rawPageReferencePaths = new Map(previewData.content.pages.map((page) => [
|
|
1011
|
+
page,
|
|
1012
|
+
resolveEffectivePageReferencePath(previewData.site, page),
|
|
1013
|
+
]));
|
|
1014
|
+
assertUniquePageReferencePaths(rawPageReferencePaths);
|
|
1015
|
+
const commentsByTarget = {
|
|
1016
|
+
posts: new Map(previewData.content.posts.map((post) => [
|
|
1017
|
+
post.slug,
|
|
1018
|
+
buildTargetCommentsContext({
|
|
1019
|
+
site: previewData.site,
|
|
1020
|
+
target: post,
|
|
1021
|
+
targetType: 'post',
|
|
1022
|
+
themeSupportsComments,
|
|
1023
|
+
}),
|
|
1024
|
+
])),
|
|
1025
|
+
pages: new Map(previewData.content.pages.map((page) => [
|
|
1026
|
+
rawPageReferencePaths.get(page),
|
|
1027
|
+
buildTargetCommentsContext({
|
|
1028
|
+
site: previewData.site,
|
|
1029
|
+
target: page,
|
|
1030
|
+
targetType: 'page',
|
|
1031
|
+
themeSupportsComments,
|
|
1032
|
+
}),
|
|
1033
|
+
])),
|
|
1034
|
+
};
|
|
1035
|
+
for (const target of previewData.content.posts) {
|
|
1036
|
+
delete target.comments;
|
|
1037
|
+
}
|
|
1038
|
+
for (const target of previewData.content.pages) {
|
|
1039
|
+
delete target.comments;
|
|
1040
|
+
}
|
|
1041
|
+
const preparedPosts = previewData.content.posts.map((post) => preparePost(post, previewData.site, authorsById, categoriesBySlug, tagsBySlug));
|
|
868
1042
|
const discoverablePreparedPosts = preparedPosts.filter((post) => !isDelistedDocument(post));
|
|
869
1043
|
const adjacentPostsBySlug = new Map(
|
|
870
1044
|
discoverablePreparedPosts.map((post, index) => [post.slug, {
|
|
@@ -878,30 +1052,50 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
878
1052
|
next: adjacentPostsBySlug.get(post.slug)?.next || null,
|
|
879
1053
|
}));
|
|
880
1054
|
const pages = previewData.content.pages.map((page) => preparePage(page, previewData.site));
|
|
1055
|
+
const pageReferencePathByPage = new Map(pages.map((page, index) => [
|
|
1056
|
+
page,
|
|
1057
|
+
rawPageReferencePaths.get(previewData.content.pages[index]),
|
|
1058
|
+
]));
|
|
881
1059
|
const postBySlug = new Map(posts.map((post) => [post.slug, post]));
|
|
882
|
-
const
|
|
1060
|
+
const pageByPath = new Map(pages.map((page) => [pageReferencePathByPage.get(page), page]));
|
|
883
1061
|
const frontPage = previewData.site.front_page;
|
|
884
|
-
const
|
|
885
|
-
|
|
1062
|
+
const collectionTargetByItem = new WeakMap();
|
|
1063
|
+
const collections = resolveCollections(
|
|
1064
|
+
previewData.collections,
|
|
1065
|
+
postBySlug,
|
|
1066
|
+
pageByPath,
|
|
1067
|
+
frontPage,
|
|
1068
|
+
pageReferencePathByPage,
|
|
1069
|
+
collectionTargetByItem,
|
|
1070
|
+
);
|
|
1071
|
+
attachCollectionCursors(collections, collectionTargetByItem);
|
|
886
1072
|
const post_index = previewData.site.post_index;
|
|
887
1073
|
const effectivePostIndexEnabled = post_index.enabled !== false && themeSupportsPostIndex;
|
|
888
1074
|
const effectivePostIndexPaginate = effectivePostIndexEnabled && post_index.paginate !== false;
|
|
889
1075
|
const post_indexBasePath = normalizeRoutePath(post_index.path || DEFAULT_POST_INDEX.path);
|
|
1076
|
+
previewData.site.post_index = {
|
|
1077
|
+
...post_index,
|
|
1078
|
+
enabled: effectivePostIndexEnabled,
|
|
1079
|
+
paginate: effectivePostIndexPaginate,
|
|
1080
|
+
};
|
|
890
1081
|
|
|
891
1082
|
if (frontPage.type !== 'theme_index' && effectivePostIndexEnabled && post_indexBasePath === '/') {
|
|
892
1083
|
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
1084
|
}
|
|
894
1085
|
|
|
895
|
-
const frontPageRoute = buildFrontPageRoute(frontPage,
|
|
896
|
-
const
|
|
897
|
-
const preparedPages =
|
|
898
|
-
? pages.filter((page) => page
|
|
1086
|
+
const frontPageRoute = buildFrontPageRoute(frontPage, pageByPath, effectivePostIndexEnabled, post_indexBasePath);
|
|
1087
|
+
const frontPagePage = frontPageRoute?.front_page_type === 'page' ? frontPageRoute.page : null;
|
|
1088
|
+
const preparedPages = frontPagePage
|
|
1089
|
+
? pages.filter((page) => page !== frontPagePage)
|
|
899
1090
|
: pages;
|
|
900
1091
|
|
|
901
1092
|
return {
|
|
902
1093
|
posts,
|
|
903
1094
|
pages: preparedPages,
|
|
904
1095
|
postBySlug,
|
|
1096
|
+
pageByPath,
|
|
1097
|
+
pageReferencePathByPage,
|
|
1098
|
+
commentsByTarget,
|
|
905
1099
|
collections,
|
|
906
1100
|
taxonomies: buildGlobalTaxonomies(previewData, categoryCountBySlug, tagCountBySlug),
|
|
907
1101
|
frontPageRoute,
|
|
@@ -915,20 +1109,22 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
915
1109
|
postBySlug,
|
|
916
1110
|
frontPage,
|
|
917
1111
|
}),
|
|
918
|
-
archiveRoutes:
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1112
|
+
archiveRoutes: previewData.site.archive.enabled
|
|
1113
|
+
? buildPaginatedCollection({
|
|
1114
|
+
items: discoverableSourcePosts,
|
|
1115
|
+
posts_per_page: previewData.site.posts_per_page,
|
|
1116
|
+
basePath: '/archive/',
|
|
1117
|
+
outputStyle: previewData.site.permalinks.output_style,
|
|
1118
|
+
}).map((entry) => ({
|
|
1119
|
+
path: entry.path,
|
|
1120
|
+
page: entry.page,
|
|
1121
|
+
totalPages: entry.totalPages,
|
|
1122
|
+
archive: {
|
|
1123
|
+
groups: buildArchiveGroups(entry.items, postBySlug, previewData.site),
|
|
1124
|
+
},
|
|
1125
|
+
pagination: buildStructuredPagination(entry.paginationData),
|
|
1126
|
+
}))
|
|
1127
|
+
: [],
|
|
932
1128
|
categoryRoutes: buildTaxonomyRoutes({
|
|
933
1129
|
items: previewData.content.categories,
|
|
934
1130
|
postsBySlug: categoryPostsBySlug,
|
|
@@ -954,6 +1150,55 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
954
1150
|
};
|
|
955
1151
|
}
|
|
956
1152
|
|
|
1153
|
+
export function buildTargetCommentsContext({ site, target, targetType, themeSupportsComments }) {
|
|
1154
|
+
const targetPublicId = target?.public_id;
|
|
1155
|
+
const comments = site?.comments;
|
|
1156
|
+
|
|
1157
|
+
if (
|
|
1158
|
+
themeSupportsComments !== true ||
|
|
1159
|
+
comments?.enabled !== true ||
|
|
1160
|
+
target?.allow_comments !== true ||
|
|
1161
|
+
!Number.isInteger(targetPublicId) ||
|
|
1162
|
+
targetPublicId <= 0
|
|
1163
|
+
) {
|
|
1164
|
+
return DISABLED_COMMENTS_CONTEXT;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
const common = {
|
|
1168
|
+
enabled: true,
|
|
1169
|
+
target_type: targetType,
|
|
1170
|
+
target_public_id: targetPublicId,
|
|
1171
|
+
provider: comments.provider,
|
|
1172
|
+
api_base_url: comments.api_base_url,
|
|
1173
|
+
per_page: comments.per_page,
|
|
1174
|
+
order: comments.order,
|
|
1175
|
+
threading: {
|
|
1176
|
+
enabled: comments.threading.enabled,
|
|
1177
|
+
max_depth: comments.threading.max_depth,
|
|
1178
|
+
},
|
|
1179
|
+
};
|
|
1180
|
+
|
|
1181
|
+
if (comments.provider === 'wordpress') {
|
|
1182
|
+
return common;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
if (comments.provider !== 'zeropress') {
|
|
1186
|
+
return DISABLED_COMMENTS_CONTEXT;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
const requestToken = preserveOpaqueNonBlankString(target?.comments?.request_token);
|
|
1190
|
+
return requestToken
|
|
1191
|
+
? { ...common, request_token: requestToken }
|
|
1192
|
+
: DISABLED_COMMENTS_CONTEXT;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function getTargetCommentsContext(state, targetType, slug) {
|
|
1196
|
+
const targetMap = targetType === 'post'
|
|
1197
|
+
? state.renderData.commentsByTarget.posts
|
|
1198
|
+
: state.renderData.commentsByTarget.pages;
|
|
1199
|
+
return targetMap.get(slug) || DISABLED_COMMENTS_CONTEXT;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
957
1202
|
function buildGlobalTaxonomies(previewData, categoryCountBySlug, tagCountBySlug) {
|
|
958
1203
|
return {
|
|
959
1204
|
categories: buildGlobalTaxonomyItems(previewData.site, 'categories', previewData.content.categories, categoryCountBySlug),
|
|
@@ -961,14 +1206,22 @@ function buildGlobalTaxonomies(previewData, categoryCountBySlug, tagCountBySlug)
|
|
|
961
1206
|
};
|
|
962
1207
|
}
|
|
963
1208
|
|
|
964
|
-
function resolveCollections(collections, postBySlug,
|
|
1209
|
+
function resolveCollections(collections, postBySlug, pageByPath, frontPage, pageReferencePathByPage, collectionTargetByItem) {
|
|
965
1210
|
if (!collections || typeof collections !== 'object') {
|
|
966
1211
|
return {};
|
|
967
1212
|
}
|
|
968
1213
|
|
|
969
1214
|
return Object.fromEntries(
|
|
970
1215
|
Object.entries(collections).map(([collectionId, collection]) => {
|
|
971
|
-
const items = resolveCollectionItems(
|
|
1216
|
+
const items = resolveCollectionItems(
|
|
1217
|
+
collectionId,
|
|
1218
|
+
collection?.items,
|
|
1219
|
+
postBySlug,
|
|
1220
|
+
pageByPath,
|
|
1221
|
+
frontPage,
|
|
1222
|
+
pageReferencePathByPage,
|
|
1223
|
+
collectionTargetByItem,
|
|
1224
|
+
);
|
|
972
1225
|
return [
|
|
973
1226
|
collectionId,
|
|
974
1227
|
{
|
|
@@ -983,44 +1236,58 @@ function resolveCollections(collections, postBySlug, pageBySlug, frontPage) {
|
|
|
983
1236
|
);
|
|
984
1237
|
}
|
|
985
1238
|
|
|
986
|
-
function resolveCollectionItems(collectionId, items, postBySlug,
|
|
1239
|
+
function resolveCollectionItems(collectionId, items, postBySlug, pageByPath, frontPage, pageReferencePathByPage, collectionTargetByItem) {
|
|
987
1240
|
if (!Array.isArray(items)) {
|
|
988
1241
|
return [];
|
|
989
1242
|
}
|
|
990
1243
|
|
|
991
|
-
return items.map((item, index) => resolveCollectionItem(
|
|
1244
|
+
return items.map((item, index) => resolveCollectionItem(
|
|
1245
|
+
collectionId,
|
|
1246
|
+
item,
|
|
1247
|
+
index,
|
|
1248
|
+
postBySlug,
|
|
1249
|
+
pageByPath,
|
|
1250
|
+
frontPage,
|
|
1251
|
+
pageReferencePathByPage,
|
|
1252
|
+
collectionTargetByItem,
|
|
1253
|
+
));
|
|
992
1254
|
}
|
|
993
1255
|
|
|
994
|
-
function resolveCollectionItem(collectionId, item, index, postBySlug,
|
|
1256
|
+
function resolveCollectionItem(collectionId, item, index, postBySlug, pageByPath, frontPage, pageReferencePathByPage, collectionTargetByItem) {
|
|
995
1257
|
if (item?.type === 'post') {
|
|
996
1258
|
const post = postBySlug.get(item.slug);
|
|
997
1259
|
if (!post) {
|
|
998
1260
|
throw new Error(`Invalid collection "${collectionId}": item ${index + 1} references missing post slug "${item.slug}".`);
|
|
999
1261
|
}
|
|
1000
|
-
|
|
1262
|
+
const resolved = {
|
|
1001
1263
|
type: 'post',
|
|
1002
1264
|
meta: post.meta,
|
|
1003
1265
|
...buildStructuredPostSummary(post),
|
|
1004
1266
|
};
|
|
1267
|
+
collectionTargetByItem.set(resolved, post);
|
|
1268
|
+
return resolved;
|
|
1005
1269
|
}
|
|
1006
1270
|
|
|
1007
1271
|
if (item?.type === 'page') {
|
|
1008
|
-
const
|
|
1272
|
+
const pagePath = normalizePageReferencePath(item.path);
|
|
1273
|
+
const page = pageByPath.get(pagePath);
|
|
1009
1274
|
if (!page) {
|
|
1010
|
-
throw new Error(`Invalid collection "${collectionId}": item ${index + 1} references missing page
|
|
1275
|
+
throw new Error(`Invalid collection "${collectionId}": item ${index + 1} references missing page path "${item.path}".`);
|
|
1011
1276
|
}
|
|
1012
|
-
|
|
1277
|
+
const resolved = buildCollectionPageSummary(page, frontPage, pageReferencePathByPage.get(page));
|
|
1278
|
+
collectionTargetByItem.set(resolved, page);
|
|
1279
|
+
return resolved;
|
|
1013
1280
|
}
|
|
1014
1281
|
|
|
1015
1282
|
throw new Error(`Invalid collection "${collectionId}": item ${index + 1} has unsupported type "${item?.type}".`);
|
|
1016
1283
|
}
|
|
1017
1284
|
|
|
1018
|
-
function buildCollectionPageSummary(page, frontPage) {
|
|
1285
|
+
function buildCollectionPageSummary(page, frontPage, pageReferencePath) {
|
|
1019
1286
|
return {
|
|
1020
1287
|
type: 'page',
|
|
1021
1288
|
title: page.title,
|
|
1022
1289
|
slug: page.slug,
|
|
1023
|
-
url: frontPage?.type === 'page' && frontPage.
|
|
1290
|
+
url: frontPage?.type === 'page' && frontPage.page_path === pageReferencePath ? '/' : page.url,
|
|
1024
1291
|
excerpt: page.excerpt || '',
|
|
1025
1292
|
featured_image: page.featured_image || '',
|
|
1026
1293
|
updated_at: page.updated_at || '',
|
|
@@ -1031,19 +1298,12 @@ function buildCollectionPageSummary(page, frontPage) {
|
|
|
1031
1298
|
};
|
|
1032
1299
|
}
|
|
1033
1300
|
|
|
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
|
-
|
|
1301
|
+
function attachCollectionCursors(collections, collectionTargetByItem) {
|
|
1038
1302
|
for (const [collectionId, collection] of Object.entries(collections || {})) {
|
|
1039
1303
|
const items = Array.isArray(collection.items) ? collection.items : [];
|
|
1040
1304
|
|
|
1041
1305
|
items.forEach((item, index) => {
|
|
1042
|
-
const target = item
|
|
1043
|
-
? postTargets.get(item.slug)
|
|
1044
|
-
: item.type === 'page'
|
|
1045
|
-
? pageTargets.get(item.slug)
|
|
1046
|
-
: null;
|
|
1306
|
+
const target = collectionTargetByItem.get(item);
|
|
1047
1307
|
|
|
1048
1308
|
if (!target) {
|
|
1049
1309
|
return;
|
|
@@ -1094,7 +1354,7 @@ function buildCollectionCursorItemSummary(item) {
|
|
|
1094
1354
|
};
|
|
1095
1355
|
}
|
|
1096
1356
|
|
|
1097
|
-
function buildFrontPageRoute(frontPage,
|
|
1357
|
+
function buildFrontPageRoute(frontPage, pageByPath, effectivePostIndexEnabled, post_indexBasePath) {
|
|
1098
1358
|
if (frontPage.type === 'theme_index') {
|
|
1099
1359
|
if (effectivePostIndexEnabled && post_indexBasePath === '/') {
|
|
1100
1360
|
return null;
|
|
@@ -1109,9 +1369,9 @@ function buildFrontPageRoute(frontPage, pages, effectivePostIndexEnabled, post_i
|
|
|
1109
1369
|
}
|
|
1110
1370
|
|
|
1111
1371
|
if (frontPage.type === 'page') {
|
|
1112
|
-
const page =
|
|
1372
|
+
const page = pageByPath.get(frontPage.page_path);
|
|
1113
1373
|
if (!page) {
|
|
1114
|
-
throw new Error(`Invalid front page configuration: site.front_page.
|
|
1374
|
+
throw new Error(`Invalid front page configuration: site.front_page.page_path "${frontPage.page_path}" does not match a page.`);
|
|
1115
1375
|
}
|
|
1116
1376
|
|
|
1117
1377
|
return {
|
|
@@ -1231,13 +1491,17 @@ function resolveWidgetItem(item, previewData, renderData, widgetAreaId, index) {
|
|
|
1231
1491
|
case 'tags':
|
|
1232
1492
|
return resolveTagsWidget(baseWidget, item.settings, previewData);
|
|
1233
1493
|
case 'archives':
|
|
1234
|
-
return
|
|
1494
|
+
return previewData.site.archive.enabled === true
|
|
1495
|
+
? resolveArchivesWidget(baseWidget, item.settings, previewData)
|
|
1496
|
+
: null;
|
|
1235
1497
|
case 'text':
|
|
1236
1498
|
return resolveTextWidget(baseWidget, item.settings);
|
|
1237
1499
|
case 'link-list':
|
|
1238
1500
|
return resolveLinkListWidget(baseWidget, item.settings);
|
|
1239
1501
|
case 'search':
|
|
1240
|
-
return
|
|
1502
|
+
return previewData.site.search.enabled === true
|
|
1503
|
+
? resolveSearchWidget(baseWidget, item.settings, widgetAreaId, index)
|
|
1504
|
+
: null;
|
|
1241
1505
|
case 'profile':
|
|
1242
1506
|
return resolveProfileWidget(baseWidget, item.settings);
|
|
1243
1507
|
default:
|
|
@@ -1322,7 +1586,7 @@ function resolveArchivesWidget(baseWidget, settings, previewData) {
|
|
|
1322
1586
|
.slice(0, limit)
|
|
1323
1587
|
.map((entry) => ({
|
|
1324
1588
|
label: entry.label,
|
|
1325
|
-
url:
|
|
1589
|
+
url: previewData.site.archive.url,
|
|
1326
1590
|
count: entry.count,
|
|
1327
1591
|
year: entry.year,
|
|
1328
1592
|
month: entry.month,
|
|
@@ -1414,9 +1678,11 @@ function preparePage(page, site) {
|
|
|
1414
1678
|
const documentType = normalizeDocumentType(page.document_type);
|
|
1415
1679
|
const renderedDocument = renderDocument(page.content, documentType);
|
|
1416
1680
|
const permalink = resolvePagePermalink(site, page);
|
|
1681
|
+
const pageFields = { ...page };
|
|
1682
|
+
delete pageFields.comments;
|
|
1417
1683
|
|
|
1418
1684
|
return {
|
|
1419
|
-
...
|
|
1685
|
+
...pageFields,
|
|
1420
1686
|
url: permalink.url,
|
|
1421
1687
|
document_type: documentType,
|
|
1422
1688
|
html: renderedDocument.html,
|
|
@@ -1425,7 +1691,7 @@ function preparePage(page, site) {
|
|
|
1425
1691
|
};
|
|
1426
1692
|
}
|
|
1427
1693
|
|
|
1428
|
-
function preparePost(post, site, authorsById, categoriesBySlug, tagsBySlug
|
|
1694
|
+
function preparePost(post, site, authorsById, categoriesBySlug, tagsBySlug) {
|
|
1429
1695
|
const documentType = normalizeDocumentType(post.document_type);
|
|
1430
1696
|
const renderedDocument = renderDocument(post.content, documentType);
|
|
1431
1697
|
const author = authorsById.get(post.author_id);
|
|
@@ -1480,7 +1746,6 @@ function preparePost(post, site, authorsById, categoriesBySlug, tagsBySlug, them
|
|
|
1480
1746
|
published_at: formatTimestamp(post.published_at_iso, site),
|
|
1481
1747
|
updated_at: formatTimestamp(post.updated_at_iso, site),
|
|
1482
1748
|
reading_time: calculateReadingTime(renderedDocument.html),
|
|
1483
|
-
comments_enabled: themeSupportsComments && site.disallow_comments !== true && post.allow_comments === true,
|
|
1484
1749
|
};
|
|
1485
1750
|
}
|
|
1486
1751
|
|
|
@@ -1730,7 +1995,7 @@ function buildAdjacentPostSummary(post) {
|
|
|
1730
1995
|
};
|
|
1731
1996
|
}
|
|
1732
1997
|
|
|
1733
|
-
function buildArchiveGroups(posts, postBySlug) {
|
|
1998
|
+
function buildArchiveGroups(posts, postBySlug, site) {
|
|
1734
1999
|
const groups = new Map();
|
|
1735
2000
|
|
|
1736
2001
|
for (const post of posts) {
|
|
@@ -1739,14 +2004,12 @@ function buildArchiveGroups(posts, postBySlug) {
|
|
|
1739
2004
|
continue;
|
|
1740
2005
|
}
|
|
1741
2006
|
|
|
1742
|
-
const
|
|
1743
|
-
const
|
|
1744
|
-
const month = date.getUTCMonth() + 1;
|
|
1745
|
-
const label = `${year}-${String(month).padStart(2, '0')}`;
|
|
2007
|
+
const parts = getZonedDateParts(prepared.published_at_iso, site);
|
|
2008
|
+
const label = `${parts.year}-${padDatePart(parts.month)}`;
|
|
1746
2009
|
const current = groups.get(label) || {
|
|
1747
2010
|
label,
|
|
1748
|
-
year,
|
|
1749
|
-
month,
|
|
2011
|
+
year: parts.year,
|
|
2012
|
+
month: parts.month,
|
|
1750
2013
|
items: [],
|
|
1751
2014
|
};
|
|
1752
2015
|
|
|
@@ -1754,7 +2017,8 @@ function buildArchiveGroups(posts, postBySlug) {
|
|
|
1754
2017
|
groups.set(label, current);
|
|
1755
2018
|
}
|
|
1756
2019
|
|
|
1757
|
-
return Array.from(groups.values())
|
|
2020
|
+
return Array.from(groups.values())
|
|
2021
|
+
.sort((left, right) => right.year - left.year || right.month - left.month);
|
|
1758
2022
|
}
|
|
1759
2023
|
|
|
1760
2024
|
function buildTaxonomyRouteData(kind, item, countBySlug) {
|
|
@@ -1789,25 +2053,31 @@ function buildArchiveEntries(posts, site) {
|
|
|
1789
2053
|
}
|
|
1790
2054
|
|
|
1791
2055
|
const date = toDate(publishedAt);
|
|
1792
|
-
const
|
|
1793
|
-
const
|
|
2056
|
+
const parts = getZonedDateParts(date, site);
|
|
2057
|
+
const key = `${parts.year}-${padDatePart(parts.month)}`;
|
|
2058
|
+
const current = entries.get(key) || {
|
|
2059
|
+
date,
|
|
2060
|
+
year: parts.year,
|
|
2061
|
+
month: parts.month,
|
|
2062
|
+
count: 0,
|
|
2063
|
+
};
|
|
1794
2064
|
current.count += 1;
|
|
1795
2065
|
entries.set(key, current);
|
|
1796
2066
|
}
|
|
1797
2067
|
|
|
1798
2068
|
return Array.from(entries.values())
|
|
1799
|
-
.sort((left, right) => right.
|
|
2069
|
+
.sort((left, right) => right.year - left.year || right.month - left.month)
|
|
1800
2070
|
.map((entry) => ({
|
|
1801
2071
|
label: formatArchiveLabel(entry.date, site),
|
|
1802
2072
|
count: entry.count,
|
|
1803
|
-
year: entry.
|
|
1804
|
-
month: entry.
|
|
2073
|
+
year: entry.year,
|
|
2074
|
+
month: entry.month,
|
|
1805
2075
|
}));
|
|
1806
2076
|
}
|
|
1807
2077
|
|
|
1808
2078
|
function formatArchiveLabel(date, site) {
|
|
1809
2079
|
return new Intl.DateTimeFormat(normalizeLocale(site.locale || DEFAULT_LOCALE), {
|
|
1810
|
-
timeZone:
|
|
2080
|
+
timeZone: normalizeTimezone(site.timezone),
|
|
1811
2081
|
year: 'numeric',
|
|
1812
2082
|
month: 'long',
|
|
1813
2083
|
}).format(date);
|
|
@@ -1818,7 +2088,7 @@ function formatTimestamp(value, site) {
|
|
|
1818
2088
|
const locale = normalizeLocale(site.locale || DEFAULT_LOCALE);
|
|
1819
2089
|
const dateStyle = DATETIME_STYLES.has(site.date_style) ? site.date_style : DEFAULT_DATE_STYLE;
|
|
1820
2090
|
const timeStyle = DATETIME_STYLES.has(site.time_style) ? site.time_style : DEFAULT_TIME_STYLE;
|
|
1821
|
-
const siteTimezone =
|
|
2091
|
+
const siteTimezone = normalizeTimezone(site.timezone);
|
|
1822
2092
|
|
|
1823
2093
|
if (dateStyle === 'none' && timeStyle === 'none') {
|
|
1824
2094
|
return '';
|
|
@@ -1858,32 +2128,43 @@ function normalizeOptionalString(value) {
|
|
|
1858
2128
|
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
1859
2129
|
}
|
|
1860
2130
|
|
|
2131
|
+
function preserveOpaqueNonBlankString(value) {
|
|
2132
|
+
return typeof value === 'string' && value.trim() ? value : '';
|
|
2133
|
+
}
|
|
2134
|
+
|
|
1861
2135
|
function normalizeOptionalRawString(value) {
|
|
1862
2136
|
return typeof value === 'string' && value.trim() ? value : '';
|
|
1863
2137
|
}
|
|
1864
2138
|
|
|
1865
2139
|
function normalizeLocale(value) {
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
if (parts.length === 0) {
|
|
1872
|
-
return DEFAULT_LOCALE;
|
|
2140
|
+
const candidate = typeof value === 'string' && value.trim() ? value.trim() : DEFAULT_LOCALE;
|
|
2141
|
+
try {
|
|
2142
|
+
return Intl.getCanonicalLocales(candidate)[0] || DEFAULT_LOCALE;
|
|
2143
|
+
} catch {
|
|
2144
|
+
throw new Error(`Invalid preview-data: site.locale "${candidate}" is not a valid BCP 47 language tag.`);
|
|
1873
2145
|
}
|
|
2146
|
+
}
|
|
1874
2147
|
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
2148
|
+
function normalizeTimezone(value) {
|
|
2149
|
+
const candidate = typeof value === 'string' && value.trim() ? value.trim() : DEFAULT_TIMEZONE;
|
|
2150
|
+
const offsetMatch = /^([+-])(\d{2}):(\d{2})$/u.exec(candidate);
|
|
2151
|
+
if (offsetMatch) {
|
|
2152
|
+
const hours = Number(offsetMatch[2]);
|
|
2153
|
+
const minutes = Number(offsetMatch[3]);
|
|
2154
|
+
if (minutes > 59 || hours > 14 || (hours === 14 && minutes !== 0)) {
|
|
2155
|
+
throw new Error(`Invalid preview-data: site.timezone "${candidate}" is outside the supported fixed-offset range.`);
|
|
1881
2156
|
}
|
|
1882
|
-
if (
|
|
1883
|
-
return
|
|
2157
|
+
if (hours === 0 && minutes === 0) {
|
|
2158
|
+
return DEFAULT_TIMEZONE;
|
|
1884
2159
|
}
|
|
1885
|
-
return
|
|
1886
|
-
}
|
|
2160
|
+
return `${offsetMatch[1]}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`;
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
try {
|
|
2164
|
+
return new Intl.DateTimeFormat('en-US', { timeZone: candidate }).resolvedOptions().timeZone;
|
|
2165
|
+
} catch {
|
|
2166
|
+
throw new Error(`Invalid preview-data: site.timezone "${candidate}" is not UTC, an IANA time zone, or a valid fixed offset.`);
|
|
2167
|
+
}
|
|
1887
2168
|
}
|
|
1888
2169
|
|
|
1889
2170
|
function normalizeDocumentType(value) {
|
|
@@ -2077,11 +2358,12 @@ async function buildCustomCssAsset(customCss, assetProcessor, options) {
|
|
|
2077
2358
|
};
|
|
2078
2359
|
}
|
|
2079
2360
|
|
|
2080
|
-
function createRenderContext(site, currentUrl) {
|
|
2361
|
+
function createRenderContext(site, currentUrl, comments = DISABLED_COMMENTS_CONTEXT) {
|
|
2081
2362
|
return {
|
|
2082
2363
|
site,
|
|
2083
2364
|
currentUrl,
|
|
2084
2365
|
language: site.locale,
|
|
2366
|
+
comments,
|
|
2085
2367
|
};
|
|
2086
2368
|
}
|
|
2087
2369
|
|
|
@@ -2097,23 +2379,26 @@ function buildRouteContext(type, url, options = {}) {
|
|
|
2097
2379
|
|
|
2098
2380
|
function buildPageMeta(site, options = {}) {
|
|
2099
2381
|
const resolvedTitle = normalizeNonEmptyString(options.title, site.title);
|
|
2100
|
-
const
|
|
2101
|
-
const
|
|
2102
|
-
const
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
const
|
|
2382
|
+
const includeRichMetadata = options.includeRichMetadata !== false;
|
|
2383
|
+
const resolvedDescription = includeRichMetadata ? normalizeOptionalString(options.description) : '';
|
|
2384
|
+
const canonicalUrl = includeRichMetadata
|
|
2385
|
+
? resolveMetaCanonicalUrl(site, options.canonicalUrl || options.currentUrl)
|
|
2386
|
+
: '';
|
|
2387
|
+
const ogImage = includeRichMetadata ? resolveMetaImageUrl(options.image) : '';
|
|
2388
|
+
const ogType = includeRichMetadata ? normalizeNonEmptyString(options.ogType, 'website') : '';
|
|
2389
|
+
const publishedTime = includeRichMetadata ? normalizeOptionalString(options.publishedTime) : '';
|
|
2390
|
+
const modifiedTime = includeRichMetadata ? normalizeOptionalString(options.modifiedTime) : '';
|
|
2106
2391
|
const robotsNoindex = options.robotsNoindex === true;
|
|
2107
2392
|
|
|
2108
2393
|
const meta = {
|
|
2109
2394
|
title: escapeHtml(resolvedTitle),
|
|
2110
2395
|
description: resolvedDescription ? escapeHtml(resolvedDescription) : '',
|
|
2111
2396
|
canonical_url: canonicalUrl ? escapeHtml(canonicalUrl) : '',
|
|
2112
|
-
og_title: escapeHtml(resolvedTitle),
|
|
2113
|
-
og_description: resolvedDescription ? escapeHtml(resolvedDescription) : '',
|
|
2397
|
+
og_title: includeRichMetadata ? escapeHtml(resolvedTitle) : '',
|
|
2398
|
+
og_description: includeRichMetadata && resolvedDescription ? escapeHtml(resolvedDescription) : '',
|
|
2114
2399
|
og_type: escapeHtml(ogType),
|
|
2115
|
-
og_url: canonicalUrl ? escapeHtml(canonicalUrl) : '',
|
|
2116
|
-
og_site_name: escapeHtml(site.title),
|
|
2400
|
+
og_url: includeRichMetadata && canonicalUrl ? escapeHtml(canonicalUrl) : '',
|
|
2401
|
+
og_site_name: includeRichMetadata ? escapeHtml(site.title) : '',
|
|
2117
2402
|
og_image: ogImage ? escapeHtml(ogImage) : '',
|
|
2118
2403
|
article_published_time: publishedTime ? escapeHtml(publishedTime) : '',
|
|
2119
2404
|
article_modified_time: modifiedTime ? escapeHtml(modifiedTime) : '',
|
|
@@ -2122,7 +2407,7 @@ function buildPageMeta(site, options = {}) {
|
|
|
2122
2407
|
|
|
2123
2408
|
return {
|
|
2124
2409
|
...meta,
|
|
2125
|
-
head_tags: buildMetaHeadTags(meta),
|
|
2410
|
+
head_tags: buildMetaHeadTags(meta, site, options),
|
|
2126
2411
|
};
|
|
2127
2412
|
}
|
|
2128
2413
|
|
|
@@ -2138,7 +2423,7 @@ function buildFrontPageTitle(site) {
|
|
|
2138
2423
|
return resolvedDescription ? `${resolvedSiteTitle} - ${resolvedDescription}` : resolvedSiteTitle;
|
|
2139
2424
|
}
|
|
2140
2425
|
|
|
2141
|
-
function buildMetaHeadTags(meta) {
|
|
2426
|
+
function buildMetaHeadTags(meta, site, options = {}) {
|
|
2142
2427
|
const tags = [];
|
|
2143
2428
|
|
|
2144
2429
|
if (meta.description) {
|
|
@@ -2150,24 +2435,29 @@ function buildMetaHeadTags(meta) {
|
|
|
2150
2435
|
if (meta.canonical_url) {
|
|
2151
2436
|
tags.push(`<link rel="canonical" href="${meta.canonical_url}">`);
|
|
2152
2437
|
}
|
|
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}">`);
|
|
2438
|
+
if (options.includeFeedLink !== false && site.feed?.enabled === true) {
|
|
2439
|
+
tags.push(`<link rel="alternate" type="application/rss+xml" title="${escapeHtml(site.title)} Feed" href="${escapeHtml(site.feed.url)}">`);
|
|
2165
2440
|
}
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2441
|
+
|
|
2442
|
+
if (meta.og_title) {
|
|
2443
|
+
tags.push(`<meta property="og:title" content="${meta.og_title}">`);
|
|
2444
|
+
if (meta.og_description) {
|
|
2445
|
+
tags.push(`<meta property="og:description" content="${meta.og_description}">`);
|
|
2446
|
+
}
|
|
2447
|
+
tags.push(`<meta property="og:type" content="${meta.og_type}">`);
|
|
2448
|
+
if (meta.og_url) {
|
|
2449
|
+
tags.push(`<meta property="og:url" content="${meta.og_url}">`);
|
|
2450
|
+
}
|
|
2451
|
+
tags.push(`<meta property="og:site_name" content="${meta.og_site_name}">`);
|
|
2452
|
+
if (meta.og_image) {
|
|
2453
|
+
tags.push(`<meta property="og:image" content="${meta.og_image}">`);
|
|
2454
|
+
}
|
|
2455
|
+
if (meta.article_published_time) {
|
|
2456
|
+
tags.push(`<meta property="article:published_time" content="${meta.article_published_time}">`);
|
|
2457
|
+
}
|
|
2458
|
+
if (meta.article_modified_time) {
|
|
2459
|
+
tags.push(`<meta property="article:modified_time" content="${meta.article_modified_time}">`);
|
|
2460
|
+
}
|
|
2171
2461
|
}
|
|
2172
2462
|
|
|
2173
2463
|
return tags.length ? `${tags.join('\n')}\n` : '';
|
|
@@ -2194,7 +2484,7 @@ function resolveMetaImageUrl(image) {
|
|
|
2194
2484
|
return '';
|
|
2195
2485
|
}
|
|
2196
2486
|
|
|
2197
|
-
function normalizeMediaField(value,
|
|
2487
|
+
function normalizeMediaField(value, media_origin) {
|
|
2198
2488
|
if (value === undefined) {
|
|
2199
2489
|
return undefined;
|
|
2200
2490
|
}
|
|
@@ -2204,22 +2494,145 @@ function normalizeMediaField(value, media_base_url) {
|
|
|
2204
2494
|
return '';
|
|
2205
2495
|
}
|
|
2206
2496
|
|
|
2207
|
-
|
|
2208
|
-
|
|
2497
|
+
const normalizedMediaUrl = normalizeMediaUrl(normalizedValue);
|
|
2498
|
+
if (!normalizedMediaUrl) {
|
|
2499
|
+
return '';
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
if (isAbsoluteUrl(normalizedMediaUrl)) {
|
|
2503
|
+
return normalizedMediaUrl;
|
|
2209
2504
|
}
|
|
2210
2505
|
|
|
2211
|
-
const
|
|
2212
|
-
if (!
|
|
2213
|
-
return
|
|
2506
|
+
const normalizedOrigin = normalizeMediaOrigin(media_origin);
|
|
2507
|
+
if (!normalizedOrigin) {
|
|
2508
|
+
return normalizedMediaUrl;
|
|
2214
2509
|
}
|
|
2215
2510
|
|
|
2216
2511
|
try {
|
|
2217
|
-
return decodeURI(new URL(
|
|
2512
|
+
return decodeURI(new URL(normalizedMediaUrl, `${normalizedOrigin}/`).toString());
|
|
2218
2513
|
} catch {
|
|
2219
2514
|
return '';
|
|
2220
2515
|
}
|
|
2221
2516
|
}
|
|
2222
2517
|
|
|
2518
|
+
function normalizeMediaOrigin(value) {
|
|
2519
|
+
const normalizedValue = normalizeOptionalString(value);
|
|
2520
|
+
if (!normalizedValue) {
|
|
2521
|
+
return '';
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
try {
|
|
2525
|
+
const url = new URL(normalizedValue);
|
|
2526
|
+
if (
|
|
2527
|
+
!SAFE_MEDIA_PROTOCOLS.has(url.protocol)
|
|
2528
|
+
|| url.username
|
|
2529
|
+
|| url.password
|
|
2530
|
+
|| url.search
|
|
2531
|
+
|| url.hash
|
|
2532
|
+
|| url.pathname !== '/'
|
|
2533
|
+
|| !isSafeUrlText(normalizedValue)
|
|
2534
|
+
) {
|
|
2535
|
+
return '';
|
|
2536
|
+
}
|
|
2537
|
+
return url.origin;
|
|
2538
|
+
} catch {
|
|
2539
|
+
return '';
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
function normalizeSiteOrigin(value) {
|
|
2544
|
+
const normalizedValue = normalizeOptionalString(value);
|
|
2545
|
+
if (!normalizedValue) {
|
|
2546
|
+
return '';
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
try {
|
|
2550
|
+
const url = new URL(normalizedValue);
|
|
2551
|
+
if (
|
|
2552
|
+
!SAFE_LINK_PROTOCOLS.has(url.protocol)
|
|
2553
|
+
|| url.username
|
|
2554
|
+
|| url.password
|
|
2555
|
+
|| url.search
|
|
2556
|
+
|| url.hash
|
|
2557
|
+
|| url.pathname !== '/'
|
|
2558
|
+
|| !isSafeUrlText(normalizedValue)
|
|
2559
|
+
) {
|
|
2560
|
+
throw new Error('site.url must be an HTTP(S) origin without credentials, path, query, or fragment');
|
|
2561
|
+
}
|
|
2562
|
+
return url.origin;
|
|
2563
|
+
} catch (error) {
|
|
2564
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2565
|
+
throw new Error(`Invalid preview-data: site.url is not a safe HTTP(S) origin: ${reason}`);
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
function normalizeNavigationUrl(value) {
|
|
2570
|
+
return normalizeSafeResourceUrl(value, { allowRoot: true });
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
function normalizeMediaUrl(value) {
|
|
2574
|
+
return normalizeSafeResourceUrl(value, { allowRoot: false });
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
function normalizeSafeResourceUrl(value, options) {
|
|
2578
|
+
const normalizedValue = normalizeOptionalString(value);
|
|
2579
|
+
if (!normalizedValue || !isSafeUrlText(normalizedValue)) {
|
|
2580
|
+
return '';
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
if (normalizedValue.startsWith('/')) {
|
|
2584
|
+
if (normalizedValue.startsWith('//')) {
|
|
2585
|
+
return '';
|
|
2586
|
+
}
|
|
2587
|
+
const pathname = normalizedValue.split(/[?#]/u, 1)[0];
|
|
2588
|
+
if ((!options.allowRoot && pathname === '/') || hasDotPathSegment(pathname)) {
|
|
2589
|
+
return '';
|
|
2590
|
+
}
|
|
2591
|
+
return normalizedValue;
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
if (!isAbsoluteUrl(normalizedValue)) {
|
|
2595
|
+
return '';
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
const rawPath = extractAbsoluteUrlPath(normalizedValue);
|
|
2599
|
+
if ((!options.allowRoot && (!rawPath || rawPath === '/')) || hasDotPathSegment(rawPath || '/')) {
|
|
2600
|
+
return '';
|
|
2601
|
+
}
|
|
2602
|
+
return normalizeAbsoluteUrl(normalizedValue, SAFE_LINK_PROTOCOLS);
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
function isSafeUrlText(value) {
|
|
2606
|
+
if (/[\s\\\u0000-\u001F\u007F]/u.test(value)) {
|
|
2607
|
+
return false;
|
|
2608
|
+
}
|
|
2609
|
+
try {
|
|
2610
|
+
decodeURI(value);
|
|
2611
|
+
return true;
|
|
2612
|
+
} catch {
|
|
2613
|
+
return false;
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
function extractAbsoluteUrlPath(value) {
|
|
2618
|
+
const match = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^/?#]*(?<path>[^?#]*)/u.exec(value);
|
|
2619
|
+
return match?.groups?.path || '/';
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
function hasDotPathSegment(pathname) {
|
|
2623
|
+
return String(pathname || '').split('/').some((segment) => {
|
|
2624
|
+
if (!segment) {
|
|
2625
|
+
return false;
|
|
2626
|
+
}
|
|
2627
|
+
try {
|
|
2628
|
+
const decoded = decodeURIComponent(segment).normalize('NFC');
|
|
2629
|
+
return decoded === '.' || decoded === '..';
|
|
2630
|
+
} catch {
|
|
2631
|
+
return true;
|
|
2632
|
+
}
|
|
2633
|
+
});
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2223
2636
|
function isAbsoluteUrl(value) {
|
|
2224
2637
|
try {
|
|
2225
2638
|
new URL(value);
|
|
@@ -2232,7 +2645,7 @@ function isAbsoluteUrl(value) {
|
|
|
2232
2645
|
function normalizeAbsoluteUrl(value, allowedProtocols) {
|
|
2233
2646
|
try {
|
|
2234
2647
|
const url = new URL(value);
|
|
2235
|
-
if (!allowedProtocols.has(url.protocol)) {
|
|
2648
|
+
if (!allowedProtocols.has(url.protocol) || url.username || url.password || !isSafeUrlText(value)) {
|
|
2236
2649
|
return '';
|
|
2237
2650
|
}
|
|
2238
2651
|
return decodeURI(url.toString());
|
|
@@ -2242,16 +2655,7 @@ function normalizeAbsoluteUrl(value, allowedProtocols) {
|
|
|
2242
2655
|
}
|
|
2243
2656
|
|
|
2244
2657
|
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;
|
|
2658
|
+
return normalizeNavigationUrl(value);
|
|
2255
2659
|
}
|
|
2256
2660
|
|
|
2257
2661
|
async function writeOutput(writer, summaries, path, content, contentType) {
|
|
@@ -2320,6 +2724,31 @@ function resolvePagePermalink(site, page) {
|
|
|
2320
2724
|
return resolvePermalink(site, 'pages', page);
|
|
2321
2725
|
}
|
|
2322
2726
|
|
|
2727
|
+
function resolveEffectivePageReferencePath(site, page) {
|
|
2728
|
+
return normalizePageReferencePath(resolvePagePermalink(site, page).path);
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
function normalizePageReferencePath(value) {
|
|
2732
|
+
const normalized = decodeRoutePath(normalizeOptionalString(value))
|
|
2733
|
+
.replace(/^\/+|\/+$/gu, '')
|
|
2734
|
+
.normalize('NFC');
|
|
2735
|
+
return normalized;
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
function assertUniquePageReferencePaths(pageReferencePaths) {
|
|
2739
|
+
const seen = new Map();
|
|
2740
|
+
for (const [page, pagePath] of pageReferencePaths.entries()) {
|
|
2741
|
+
if (!pagePath) {
|
|
2742
|
+
throw new Error(`Invalid preview-data: Page "${page?.slug || ''}" resolves to an empty effective path.`);
|
|
2743
|
+
}
|
|
2744
|
+
const existing = seen.get(pagePath);
|
|
2745
|
+
if (existing) {
|
|
2746
|
+
throw new Error(`Invalid preview-data: Pages "${existing.slug}" and "${page.slug}" resolve to the same effective path "${pagePath}".`);
|
|
2747
|
+
}
|
|
2748
|
+
seen.set(pagePath, page);
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2323
2752
|
function resolvePermalink(site, kind, item) {
|
|
2324
2753
|
const pattern = normalizeNonEmptyString(site.permalinks?.[kind], DEFAULT_PERMALINKS[kind]);
|
|
2325
2754
|
return buildRouteInfo(applyPermalinkPattern(pattern, kind, item, site), site.permalinks.output_style);
|
|
@@ -2358,19 +2787,19 @@ function buildPermalinkTokenValues(kind, item, site) {
|
|
|
2358
2787
|
};
|
|
2359
2788
|
|
|
2360
2789
|
if (kind === 'posts') {
|
|
2361
|
-
const parts =
|
|
2790
|
+
const parts = getZonedDateParts(item.published_at_iso, site);
|
|
2362
2791
|
values.public_id = String(item.public_id);
|
|
2363
|
-
values.year = parts.year;
|
|
2364
|
-
values.month = parts.month;
|
|
2365
|
-
values.day = parts.day;
|
|
2792
|
+
values.year = String(parts.year);
|
|
2793
|
+
values.month = padDatePart(parts.month);
|
|
2794
|
+
values.day = padDatePart(parts.day);
|
|
2366
2795
|
}
|
|
2367
2796
|
|
|
2368
2797
|
return values;
|
|
2369
2798
|
}
|
|
2370
2799
|
|
|
2371
|
-
function
|
|
2800
|
+
function getZonedDateParts(value, site) {
|
|
2372
2801
|
const date = toDate(value);
|
|
2373
|
-
const timeZone =
|
|
2802
|
+
const timeZone = normalizeTimezone(site.timezone);
|
|
2374
2803
|
const parts = new Intl.DateTimeFormat('en-US', {
|
|
2375
2804
|
timeZone,
|
|
2376
2805
|
year: 'numeric',
|
|
@@ -2379,12 +2808,16 @@ function getPermalinkDateParts(value, site) {
|
|
|
2379
2808
|
}).formatToParts(date);
|
|
2380
2809
|
|
|
2381
2810
|
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 ||
|
|
2811
|
+
year: Number(parts.find((part) => part.type === 'year')?.value) || date.getUTCFullYear(),
|
|
2812
|
+
month: Number(parts.find((part) => part.type === 'month')?.value) || date.getUTCMonth() + 1,
|
|
2813
|
+
day: Number(parts.find((part) => part.type === 'day')?.value) || date.getUTCDate(),
|
|
2385
2814
|
};
|
|
2386
2815
|
}
|
|
2387
2816
|
|
|
2817
|
+
function padDatePart(value) {
|
|
2818
|
+
return String(value).padStart(2, '0');
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2388
2821
|
function normalizeOutputPath(filePath) {
|
|
2389
2822
|
return String(filePath || '').replace(/^\/+/, '');
|
|
2390
2823
|
}
|
|
@@ -2437,29 +2870,31 @@ function assertPlannedOutputPathsSafe(state) {
|
|
|
2437
2870
|
outputPath: pageToOutputPath(page, outputStyle),
|
|
2438
2871
|
})),
|
|
2439
2872
|
];
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
const plannedPaths = [
|
|
2443
|
-
...routeEntries.map((entry) => entry.outputPath),
|
|
2873
|
+
const nonRoutePaths = [
|
|
2444
2874
|
...state.assetOutputs.map((assetOutput) => assetOutput.path),
|
|
2445
2875
|
];
|
|
2446
2876
|
|
|
2447
|
-
if (shouldGenerateSearchArtifacts(state
|
|
2448
|
-
|
|
2877
|
+
if (shouldGenerateSearchArtifacts(state)) {
|
|
2878
|
+
nonRoutePaths.push(SEARCH_INDEX_OUTPUT_PATH, SEARCH_ADAPTER_OUTPUT_PATH, SEARCH_PAGEFIND_ADAPTER_OUTPUT_PATH);
|
|
2449
2879
|
}
|
|
2450
2880
|
|
|
2451
|
-
if (state
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2881
|
+
if (hasTemplate(state, '404')) {
|
|
2882
|
+
nonRoutePaths.push('404.html');
|
|
2883
|
+
}
|
|
2884
|
+
if (shouldGenerateRobotsTxt(state.options)) {
|
|
2885
|
+
nonRoutePaths.push('robots.txt');
|
|
2886
|
+
}
|
|
2887
|
+
if (hasCanonicalSiteUrl(state.previewData.site.url)) {
|
|
2888
|
+
nonRoutePaths.push('sitemap.xml');
|
|
2889
|
+
if (shouldGenerateFeed(state)) {
|
|
2890
|
+
nonRoutePaths.push('feed.xml');
|
|
2461
2891
|
}
|
|
2462
2892
|
}
|
|
2893
|
+
nonRoutePaths.push(...normalizeReservedOutputPaths(state.options.reservedOutputPaths));
|
|
2894
|
+
const plannedPaths = [
|
|
2895
|
+
...routeEntries.map((entry) => entry.outputPath),
|
|
2896
|
+
...nonRoutePaths,
|
|
2897
|
+
];
|
|
2463
2898
|
|
|
2464
2899
|
for (const plannedPath of plannedPaths) {
|
|
2465
2900
|
const rawPath = String(plannedPath || '');
|
|
@@ -2467,29 +2902,92 @@ function assertPlannedOutputPathsSafe(state) {
|
|
|
2467
2902
|
assertSafeRelativeOutputPath(rawPath, normalizedPath);
|
|
2468
2903
|
}
|
|
2469
2904
|
|
|
2905
|
+
const publicUrlClaims = [
|
|
2906
|
+
...routeEntries.flatMap((entry, index) => {
|
|
2907
|
+
const owner = `route:${index}`;
|
|
2908
|
+
return [entry.url, ...buildOutputPublicUrlAliases(entry.outputPath)]
|
|
2909
|
+
.map((url) => ({ url, owner }));
|
|
2910
|
+
}),
|
|
2911
|
+
...nonRoutePaths.flatMap((outputPath, index) => {
|
|
2912
|
+
const owner = `file:${index}`;
|
|
2913
|
+
return buildOutputPublicUrlAliases(outputPath)
|
|
2914
|
+
.map((url) => ({ url, owner }));
|
|
2915
|
+
}),
|
|
2916
|
+
];
|
|
2917
|
+
assertUniquePublicUrlClaims(publicUrlClaims);
|
|
2470
2918
|
assertUniqueOutputPaths(plannedPaths);
|
|
2471
2919
|
}
|
|
2472
2920
|
|
|
2473
|
-
function
|
|
2921
|
+
function normalizeReservedOutputPaths(value) {
|
|
2922
|
+
if (value === undefined) {
|
|
2923
|
+
return [];
|
|
2924
|
+
}
|
|
2925
|
+
if (!Array.isArray(value)) {
|
|
2926
|
+
throw new Error('reservedOutputPaths must be an array of relative output file paths');
|
|
2927
|
+
}
|
|
2928
|
+
return value.map((outputPath) => {
|
|
2929
|
+
if (typeof outputPath !== 'string') {
|
|
2930
|
+
throw new Error('reservedOutputPaths must contain only relative output file paths');
|
|
2931
|
+
}
|
|
2932
|
+
return outputPath;
|
|
2933
|
+
});
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
function buildOutputPublicUrlAliases(outputPath) {
|
|
2937
|
+
const normalizedPath = normalizeOutputPath(outputPath);
|
|
2938
|
+
const aliases = new Set([`/${normalizedPath}`]);
|
|
2939
|
+
|
|
2940
|
+
if (normalizedPath.endsWith('.html')) {
|
|
2941
|
+
aliases.add(`/${normalizedPath.slice(0, -'.html'.length)}`);
|
|
2942
|
+
}
|
|
2943
|
+
if (normalizedPath === 'index.html') {
|
|
2944
|
+
aliases.add('/');
|
|
2945
|
+
} else if (normalizedPath.endsWith('/index.html')) {
|
|
2946
|
+
aliases.add(`/${normalizedPath.slice(0, -'index.html'.length)}`);
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
return [...aliases];
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2952
|
+
function assertUniquePublicUrlClaims(claims) {
|
|
2474
2953
|
const seenUrls = new Map();
|
|
2475
|
-
for (const
|
|
2476
|
-
const normalizedUrl = normalizeRouteCollisionKey(
|
|
2477
|
-
|
|
2478
|
-
|
|
2954
|
+
for (const claim of claims) {
|
|
2955
|
+
const normalizedUrl = normalizeRouteCollisionKey(claim.url);
|
|
2956
|
+
const existingOwner = seenUrls.get(normalizedUrl);
|
|
2957
|
+
if (existingOwner !== undefined && existingOwner !== claim.owner) {
|
|
2958
|
+
throw new Error(`Duplicate public URL detected: ${claim.url}`);
|
|
2479
2959
|
}
|
|
2480
|
-
seenUrls.set(normalizedUrl,
|
|
2960
|
+
seenUrls.set(normalizedUrl, claim.owner);
|
|
2481
2961
|
}
|
|
2482
2962
|
}
|
|
2483
2963
|
|
|
2484
2964
|
function assertUniqueOutputPaths(plannedPaths) {
|
|
2965
|
+
const normalizedPaths = plannedPaths.map((plannedPath) => normalizeOutputPath(plannedPath));
|
|
2485
2966
|
const seenPaths = new Set();
|
|
2486
|
-
for (const
|
|
2487
|
-
const normalizedPath = normalizeOutputPath(plannedPath);
|
|
2967
|
+
for (const [index, normalizedPath] of normalizedPaths.entries()) {
|
|
2488
2968
|
if (seenPaths.has(normalizedPath)) {
|
|
2489
|
-
throw new Error(`Duplicate output path detected: ${
|
|
2969
|
+
throw new Error(`Duplicate output path detected: ${plannedPaths[index]}`);
|
|
2490
2970
|
}
|
|
2491
2971
|
seenPaths.add(normalizedPath);
|
|
2492
2972
|
}
|
|
2973
|
+
|
|
2974
|
+
const shallowestFirst = [...seenPaths]
|
|
2975
|
+
.map((outputPath) => ({ outputPath, segments: outputPath.split('/') }))
|
|
2976
|
+
.sort((left, right) => (
|
|
2977
|
+
left.segments.length - right.segments.length
|
|
2978
|
+
|| left.outputPath.localeCompare(right.outputPath)
|
|
2979
|
+
));
|
|
2980
|
+
const processedPaths = new Set();
|
|
2981
|
+
for (const { outputPath, segments } of shallowestFirst) {
|
|
2982
|
+
let ancestorPath = '';
|
|
2983
|
+
for (const segment of segments.slice(0, -1)) {
|
|
2984
|
+
ancestorPath = ancestorPath ? `${ancestorPath}/${segment}` : segment;
|
|
2985
|
+
if (processedPaths.has(ancestorPath)) {
|
|
2986
|
+
throw new Error(`Conflicting output path hierarchy detected: ${ancestorPath} and ${outputPath}`);
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
processedPaths.add(outputPath);
|
|
2990
|
+
}
|
|
2493
2991
|
}
|
|
2494
2992
|
|
|
2495
2993
|
function normalizeRouteCollisionKey(url) {
|
|
@@ -2532,6 +3030,7 @@ function assertSafeRelativeOutputPath(rawPath, normalizedPath = normalizeOutputP
|
|
|
2532
3030
|
if (
|
|
2533
3031
|
originalPath.startsWith('/') ||
|
|
2534
3032
|
originalPath.startsWith('\\') ||
|
|
3033
|
+
originalPath.includes('\\') ||
|
|
2535
3034
|
/^[A-Za-z]:[\\/]/.test(originalPath) ||
|
|
2536
3035
|
normalizedSeparators.startsWith('//')
|
|
2537
3036
|
) {
|
|
@@ -2554,11 +3053,11 @@ function sha256(content) {
|
|
|
2554
3053
|
return hash.digest('hex');
|
|
2555
3054
|
}
|
|
2556
3055
|
|
|
2557
|
-
function injectSiteCustomizations(html, state) {
|
|
3056
|
+
function injectSiteCustomizations(html, state, target) {
|
|
2558
3057
|
let next = injectFaviconLinks(html, state.favicon);
|
|
2559
3058
|
next = injectGeneratorMeta(next, state.exposeGenerator);
|
|
2560
3059
|
next = injectCustomCssAssetLink(next, state.customCssHref);
|
|
2561
|
-
next = injectCustomHtml(next, state.customHtml);
|
|
3060
|
+
next = injectCustomHtml(next, state.customHtml, target);
|
|
2562
3061
|
return next;
|
|
2563
3062
|
}
|
|
2564
3063
|
|
|
@@ -2568,7 +3067,7 @@ function injectFaviconLinks(html, favicon) {
|
|
|
2568
3067
|
return html;
|
|
2569
3068
|
}
|
|
2570
3069
|
|
|
2571
|
-
return html
|
|
3070
|
+
return insertBeforeClosingTag(html, HEAD_CLOSING_TAG_PATTERN, `${links}\n`);
|
|
2572
3071
|
}
|
|
2573
3072
|
|
|
2574
3073
|
function buildFaviconLinks(favicon) {
|
|
@@ -2577,14 +3076,36 @@ function buildFaviconLinks(favicon) {
|
|
|
2577
3076
|
}
|
|
2578
3077
|
|
|
2579
3078
|
const lines = [];
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
3079
|
+
const icon = normalizeOptionalString(favicon.icon);
|
|
3080
|
+
const iconDark = normalizeOptionalString(favicon.icon_dark);
|
|
3081
|
+
const svg = normalizeOptionalString(favicon.svg);
|
|
3082
|
+
const png = normalizeOptionalString(favicon.png);
|
|
3083
|
+
const hasDefaultIcon = Boolean(icon || svg || png);
|
|
3084
|
+
const hasBothColorSchemes = Boolean(iconDark && hasDefaultIcon);
|
|
3085
|
+
|
|
3086
|
+
if (iconDark) {
|
|
3087
|
+
lines.push(buildFaviconLink(
|
|
3088
|
+
iconDark,
|
|
3089
|
+
undefined,
|
|
3090
|
+
hasBothColorSchemes ? '(prefers-color-scheme: dark)' : undefined,
|
|
3091
|
+
));
|
|
3092
|
+
}
|
|
3093
|
+
if (icon) {
|
|
3094
|
+
lines.push(buildFaviconLink(icon, undefined, hasBothColorSchemes ? '(prefers-color-scheme: light)' : undefined));
|
|
3095
|
+
}
|
|
3096
|
+
if (svg) {
|
|
3097
|
+
lines.push(buildFaviconLink(
|
|
3098
|
+
svg,
|
|
3099
|
+
'image/svg+xml',
|
|
3100
|
+
hasBothColorSchemes ? '(prefers-color-scheme: light)' : undefined,
|
|
3101
|
+
));
|
|
3102
|
+
}
|
|
3103
|
+
if (png) {
|
|
3104
|
+
lines.push(buildFaviconLink(
|
|
3105
|
+
png,
|
|
3106
|
+
'image/png',
|
|
3107
|
+
hasBothColorSchemes ? '(prefers-color-scheme: light)' : undefined,
|
|
3108
|
+
));
|
|
2588
3109
|
}
|
|
2589
3110
|
if (normalizeOptionalString(favicon.apple_touch_icon)) {
|
|
2590
3111
|
lines.push(` <link rel="apple-touch-icon" href="${escapeHtml(favicon.apple_touch_icon)}">`);
|
|
@@ -2593,12 +3114,20 @@ function buildFaviconLinks(favicon) {
|
|
|
2593
3114
|
return lines.join('\n');
|
|
2594
3115
|
}
|
|
2595
3116
|
|
|
3117
|
+
function buildFaviconLink(href, type, media) {
|
|
3118
|
+
return ` <link rel="icon" href="${escapeHtml(href)}"${type ? ` type="${type}"` : ''}${media ? ` media="${media}"` : ''}>`;
|
|
3119
|
+
}
|
|
3120
|
+
|
|
2596
3121
|
function injectGeneratorMeta(html, exposeGenerator) {
|
|
2597
3122
|
if (exposeGenerator === false) {
|
|
2598
3123
|
return html;
|
|
2599
3124
|
}
|
|
2600
3125
|
|
|
2601
|
-
return
|
|
3126
|
+
return insertBeforeClosingTag(
|
|
3127
|
+
html,
|
|
3128
|
+
HEAD_CLOSING_TAG_PATTERN,
|
|
3129
|
+
' <meta name="generator" content="ZeroPress">\n',
|
|
3130
|
+
);
|
|
2602
3131
|
}
|
|
2603
3132
|
|
|
2604
3133
|
function injectCustomCssAssetLink(html, href) {
|
|
@@ -2606,24 +3135,66 @@ function injectCustomCssAssetLink(html, href) {
|
|
|
2606
3135
|
return html;
|
|
2607
3136
|
}
|
|
2608
3137
|
|
|
2609
|
-
return
|
|
3138
|
+
return insertBeforeClosingTag(
|
|
3139
|
+
html,
|
|
3140
|
+
HEAD_CLOSING_TAG_PATTERN,
|
|
3141
|
+
` <link rel="stylesheet" href="${escapeHtml(href)}">\n`,
|
|
3142
|
+
);
|
|
2610
3143
|
}
|
|
2611
3144
|
|
|
2612
|
-
function injectCustomHtml(html, customHtml) {
|
|
3145
|
+
function injectCustomHtml(html, customHtml, target) {
|
|
2613
3146
|
let next = html;
|
|
2614
|
-
const headEnd =
|
|
2615
|
-
const bodyEnd =
|
|
3147
|
+
const headEnd = normalizeCustomHtmlSlot(customHtml?.head_end);
|
|
3148
|
+
const bodyEnd = normalizeCustomHtmlSlot(customHtml?.body_end);
|
|
2616
3149
|
|
|
2617
3150
|
if (headEnd) {
|
|
2618
|
-
next =
|
|
3151
|
+
next = injectRequiredCustomHtmlSlot(
|
|
3152
|
+
next,
|
|
3153
|
+
'head_end',
|
|
3154
|
+
headEnd,
|
|
3155
|
+
'head',
|
|
3156
|
+
HEAD_CLOSING_TAG_PATTERN,
|
|
3157
|
+
target,
|
|
3158
|
+
);
|
|
2619
3159
|
}
|
|
2620
3160
|
if (bodyEnd) {
|
|
2621
|
-
next =
|
|
3161
|
+
next = injectRequiredCustomHtmlSlot(
|
|
3162
|
+
next,
|
|
3163
|
+
'body_end',
|
|
3164
|
+
bodyEnd,
|
|
3165
|
+
'body',
|
|
3166
|
+
BODY_CLOSING_TAG_PATTERN,
|
|
3167
|
+
target,
|
|
3168
|
+
);
|
|
2622
3169
|
}
|
|
2623
3170
|
|
|
2624
3171
|
return next;
|
|
2625
3172
|
}
|
|
2626
3173
|
|
|
3174
|
+
function injectRequiredCustomHtmlSlot(html, slot, content, tagName, closingTagPattern, target) {
|
|
3175
|
+
let injected = false;
|
|
3176
|
+
const next = String(html).replace(closingTagPattern, (closingTag) => {
|
|
3177
|
+
injected = true;
|
|
3178
|
+
return `${content}\n${closingTag}`;
|
|
3179
|
+
});
|
|
3180
|
+
|
|
3181
|
+
if (injected) {
|
|
3182
|
+
return next;
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
const route = normalizeOptionalString(target?.route) || '<unknown>';
|
|
3186
|
+
const outputPath = normalizeOptionalString(target?.outputPath) || '<unknown>';
|
|
3187
|
+
throw new Error(
|
|
3188
|
+
`Unable to inject custom_html.${slot} for route "${route}" into output "${outputPath}": `
|
|
3189
|
+
+ `rendered theme HTML is missing a closing </${tagName}> tag. `
|
|
3190
|
+
+ `Add </${tagName}> to the rendered theme layout or remove custom_html.${slot}.`,
|
|
3191
|
+
);
|
|
3192
|
+
}
|
|
3193
|
+
|
|
3194
|
+
function insertBeforeClosingTag(html, closingTagPattern, content) {
|
|
3195
|
+
return String(html).replace(closingTagPattern, (closingTag) => `${content}${closingTag}`);
|
|
3196
|
+
}
|
|
3197
|
+
|
|
2627
3198
|
function buildSearchIndexJson(state) {
|
|
2628
3199
|
return `${JSON.stringify(buildSearchIndexItems(state), null, 2)}\n`;
|
|
2629
3200
|
}
|
|
@@ -2649,18 +3220,26 @@ function buildSearchIndexItems(state) {
|
|
|
2649
3220
|
? state.renderData.frontPageRoute.page
|
|
2650
3221
|
: null;
|
|
2651
3222
|
const frontPageItems = frontPagePage && frontPagePage.status === 'published' && !isDelistedDocument(frontPagePage)
|
|
2652
|
-
? [buildSearchPageItem(
|
|
3223
|
+
? [buildSearchPageItem(
|
|
3224
|
+
frontPagePage,
|
|
3225
|
+
'/',
|
|
3226
|
+
state.renderData.pageReferencePathByPage.get(frontPagePage),
|
|
3227
|
+
)]
|
|
2653
3228
|
: [];
|
|
2654
3229
|
const pageItems = state.renderData.pages
|
|
2655
3230
|
.filter((page) => page.status === 'published' && !isDelistedDocument(page))
|
|
2656
|
-
.map((page) => buildSearchPageItem(
|
|
3231
|
+
.map((page) => buildSearchPageItem(
|
|
3232
|
+
page,
|
|
3233
|
+
page.url,
|
|
3234
|
+
state.renderData.pageReferencePathByPage.get(page),
|
|
3235
|
+
));
|
|
2657
3236
|
|
|
2658
3237
|
return [...posts, ...frontPageItems, ...pageItems];
|
|
2659
3238
|
}
|
|
2660
3239
|
|
|
2661
|
-
function buildSearchPageItem(page, url) {
|
|
3240
|
+
function buildSearchPageItem(page, url, pageReferencePath) {
|
|
2662
3241
|
return {
|
|
2663
|
-
id: `page:${
|
|
3242
|
+
id: `page:${pageReferencePath}`,
|
|
2664
3243
|
type: 'page',
|
|
2665
3244
|
title: page.title,
|
|
2666
3245
|
url,
|
|
@@ -3170,7 +3749,7 @@ function buildFeedXml(site, emitted, generatedAt) {
|
|
|
3170
3749
|
|
|
3171
3750
|
function buildRobotsTxt(site) {
|
|
3172
3751
|
const lines = ['User-agent: *'];
|
|
3173
|
-
if (site.
|
|
3752
|
+
if (site.robots?.allow_indexing === false) {
|
|
3174
3753
|
lines.push('Disallow: /');
|
|
3175
3754
|
return `${lines.join('\n')}\n`;
|
|
3176
3755
|
}
|
|
@@ -3183,15 +3762,15 @@ function buildRobotsTxt(site) {
|
|
|
3183
3762
|
}
|
|
3184
3763
|
|
|
3185
3764
|
function shouldGenerateRobotsTxt(options) {
|
|
3186
|
-
return options.
|
|
3765
|
+
return options.generateRobotsTxt !== false;
|
|
3187
3766
|
}
|
|
3188
3767
|
|
|
3189
|
-
function shouldGenerateFeed(
|
|
3190
|
-
return
|
|
3768
|
+
function shouldGenerateFeed(state) {
|
|
3769
|
+
return state.previewData.site.feed.enabled === true;
|
|
3191
3770
|
}
|
|
3192
3771
|
|
|
3193
|
-
function shouldGenerateSearchArtifacts(state
|
|
3194
|
-
return
|
|
3772
|
+
function shouldGenerateSearchArtifacts(state) {
|
|
3773
|
+
return state.previewData.site.search.enabled === true;
|
|
3195
3774
|
}
|
|
3196
3775
|
|
|
3197
3776
|
function getContentType(assetPath) {
|
|
@@ -3240,7 +3819,14 @@ function resolveSiteUrl(siteUrl, relativePath) {
|
|
|
3240
3819
|
}
|
|
3241
3820
|
|
|
3242
3821
|
function hasCanonicalSiteUrl(siteUrl) {
|
|
3243
|
-
|
|
3822
|
+
if (typeof siteUrl !== 'string' || !siteUrl.trim()) {
|
|
3823
|
+
return false;
|
|
3824
|
+
}
|
|
3825
|
+
try {
|
|
3826
|
+
return normalizeSiteOrigin(siteUrl) === siteUrl;
|
|
3827
|
+
} catch {
|
|
3828
|
+
return false;
|
|
3829
|
+
}
|
|
3244
3830
|
}
|
|
3245
3831
|
|
|
3246
3832
|
function toDate(value) {
|