@zeropress/build-core 0.5.1 → 0.6.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 +38 -42
- package/package.json +4 -4
- package/src/api/build-site.js +1008 -135
- package/src/assets/asset-processor.js +1 -6
- package/src/index.d.ts +11 -4
- package/src/render/content-renderer.js +20 -2
- package/src/render/control-flow-renderer.js +251 -39
- package/src/render/partial-resolver.js +42 -6
- package/src/render/variable-resolver.js +4 -0
- package/src/render/zeropress-engine.js +2 -2
- package/src/theme/load-theme-dir.js +56 -2
package/src/api/build-site.js
CHANGED
|
@@ -9,14 +9,18 @@ import { ZeroPressEngine } from '../render/zeropress-engine.js';
|
|
|
9
9
|
const DEFAULT_OPTIONS = {
|
|
10
10
|
assetHashing: true,
|
|
11
11
|
generateSpecialFiles: true,
|
|
12
|
+
generateRobotsTxt: true,
|
|
12
13
|
writeManifest: false,
|
|
13
14
|
};
|
|
14
15
|
|
|
15
16
|
const DEFAULT_POSTS_PER_PAGE = 10;
|
|
16
|
-
const
|
|
17
|
-
const
|
|
17
|
+
const DEFAULT_DATETIME_DISPLAY = 'static';
|
|
18
|
+
const DEFAULT_DATE_STYLE = 'medium';
|
|
19
|
+
const DEFAULT_TIME_STYLE = 'none';
|
|
18
20
|
const DEFAULT_TIMEZONE = 'UTC';
|
|
19
21
|
const DEFAULT_LOCALE = 'en-US';
|
|
22
|
+
const DATETIME_DISPLAY_MODES = new Set(['static', 'client']);
|
|
23
|
+
const DATETIME_STYLES = new Set(['none', 'short', 'medium', 'long', 'full']);
|
|
20
24
|
const DEFAULT_PERMALINKS = Object.freeze({
|
|
21
25
|
output_style: 'directory',
|
|
22
26
|
posts: '/posts/:slug/',
|
|
@@ -34,9 +38,24 @@ const DEFAULT_POST_INDEX = Object.freeze({
|
|
|
34
38
|
});
|
|
35
39
|
const PERMALINK_OUTPUT_STYLES = new Set(['directory', 'html-extension']);
|
|
36
40
|
const COMMENT_POLICY_OUTPUT_PATH = '_zeropress/comment-policy.json';
|
|
41
|
+
const SEARCH_INDEX_OUTPUT_PATH = '_zeropress/search.json';
|
|
42
|
+
const SEARCH_ADAPTER_OUTPUT_PATH = '_zeropress/search.js';
|
|
37
43
|
const OUTPUT_PATH_CONTROL_CHAR_PATTERN = /[\u0000-\u001F\u007F]/;
|
|
38
44
|
const SAFE_MEDIA_PROTOCOLS = new Set(['http:', 'https:']);
|
|
39
45
|
const SAFE_LINK_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:']);
|
|
46
|
+
const MEDIA_DELIVERY_MODES = new Set(['none', 'media_domain']);
|
|
47
|
+
const DISCOVERABILITY_VALUES = new Set(['default', 'noindex', 'delist']);
|
|
48
|
+
const RESPONSIVE_IMAGE_WIDTHS = [320, 480, 768, 1024, 1280, 1600, 1920];
|
|
49
|
+
const RESPONSIVE_IMAGE_EXTENSIONS = new Set(['jpg', 'jpeg', 'png', 'webp', 'avif']);
|
|
50
|
+
const SEARCH_FIELD_WEIGHTS = Object.freeze({
|
|
51
|
+
title: 5,
|
|
52
|
+
headings: 3,
|
|
53
|
+
tags: 2.5,
|
|
54
|
+
categories: 2,
|
|
55
|
+
excerpt: 1.5,
|
|
56
|
+
content_text: 1,
|
|
57
|
+
});
|
|
58
|
+
const SEARCH_RECENCY_BOOST_MAX = 0.15;
|
|
40
59
|
export async function buildSite(input) {
|
|
41
60
|
const options = { ...DEFAULT_OPTIONS, ...(input.options || {}) };
|
|
42
61
|
const state = await createBuildState(input, options);
|
|
@@ -89,12 +108,22 @@ export async function buildSite(input) {
|
|
|
89
108
|
);
|
|
90
109
|
|
|
91
110
|
if (options.generateSpecialFiles) {
|
|
111
|
+
await writeOutput(state.writer, state.summaries, SEARCH_INDEX_OUTPUT_PATH, buildSearchIndexJson(state), 'application/json');
|
|
112
|
+
await writeOutput(state.writer, state.summaries, SEARCH_ADAPTER_OUTPUT_PATH, buildSearchAdapterJs(), 'application/javascript');
|
|
92
113
|
await maybeRenderNotFoundPage(state);
|
|
93
114
|
if (hasCanonicalSiteUrl(state.previewData.site.url)) {
|
|
94
|
-
await writeOutput(
|
|
115
|
+
await writeOutput(
|
|
116
|
+
state.writer,
|
|
117
|
+
state.summaries,
|
|
118
|
+
'sitemap.xml',
|
|
119
|
+
buildSitemapXml(state.previewData.site, state.emitted, state.generatedAt, options.sitemapStylesheetHref),
|
|
120
|
+
'application/xml',
|
|
121
|
+
);
|
|
95
122
|
await writeOutput(state.writer, state.summaries, 'feed.xml', buildFeedXml(state.previewData.site, state.emitted, state.generatedAt), 'application/rss+xml');
|
|
96
123
|
}
|
|
97
|
-
|
|
124
|
+
if (shouldGenerateRobotsTxt(options)) {
|
|
125
|
+
await writeOutput(state.writer, state.summaries, 'robots.txt', buildRobotsTxt(state.previewData.site), 'text/plain');
|
|
126
|
+
}
|
|
98
127
|
}
|
|
99
128
|
|
|
100
129
|
return finalizeBuildResult(state.writer, state.summaries, options);
|
|
@@ -111,7 +140,7 @@ async function createBuildState(input, options) {
|
|
|
111
140
|
const engine = new ZeroPressEngine();
|
|
112
141
|
const assetProcessor = new AssetProcessor();
|
|
113
142
|
const summaries = [];
|
|
114
|
-
const previewData = normalizePreviewData(input.previewData);
|
|
143
|
+
const previewData = normalizePreviewData(input.previewData, options);
|
|
115
144
|
const renderData = createRenderData(previewData, themePackage.metadata);
|
|
116
145
|
|
|
117
146
|
engine.initialize(themePackage);
|
|
@@ -137,6 +166,8 @@ async function createBuildState(input, options) {
|
|
|
137
166
|
assetMap,
|
|
138
167
|
customCssHref: customCssAsset ? `/${customCssAsset.path}` : '',
|
|
139
168
|
customHtml: previewData.custom_html,
|
|
169
|
+
favicon: previewData.site.favicon,
|
|
170
|
+
exposeGenerator: previewData.site.expose_generator !== false,
|
|
140
171
|
commentPolicyContent: buildCommentPolicyManifest(renderData.posts),
|
|
141
172
|
options,
|
|
142
173
|
generatedAt: new Date(),
|
|
@@ -197,6 +228,7 @@ async function renderRoute(state, templateName, route) {
|
|
|
197
228
|
{
|
|
198
229
|
menus: state.previewData.menus,
|
|
199
230
|
widgets: state.widgets,
|
|
231
|
+
collections: state.renderData.collections,
|
|
200
232
|
taxonomies: state.renderData.taxonomies,
|
|
201
233
|
...route,
|
|
202
234
|
route: routeContext,
|
|
@@ -243,6 +275,7 @@ async function renderFrontPage(state, route) {
|
|
|
243
275
|
{
|
|
244
276
|
menus: state.previewData.menus,
|
|
245
277
|
widgets: state.widgets,
|
|
278
|
+
collections: state.renderData.collections,
|
|
246
279
|
taxonomies: state.renderData.taxonomies,
|
|
247
280
|
page,
|
|
248
281
|
route: routeContext,
|
|
@@ -252,6 +285,7 @@ async function renderFrontPage(state, route) {
|
|
|
252
285
|
description: page.excerpt,
|
|
253
286
|
ogType: 'website',
|
|
254
287
|
image: page.featured_image,
|
|
288
|
+
robotsNoindex: shouldNoindexDocument(page),
|
|
255
289
|
}),
|
|
256
290
|
},
|
|
257
291
|
createRenderContext(state.previewData.site, currentUrl),
|
|
@@ -264,6 +298,7 @@ async function renderFrontPage(state, route) {
|
|
|
264
298
|
title: page.title,
|
|
265
299
|
description: page.excerpt,
|
|
266
300
|
includeInFeed: false,
|
|
301
|
+
includeInSitemap: !isDelistedDocument(page),
|
|
267
302
|
};
|
|
268
303
|
return;
|
|
269
304
|
}
|
|
@@ -273,6 +308,7 @@ async function renderFrontPage(state, route) {
|
|
|
273
308
|
{
|
|
274
309
|
menus: state.previewData.menus,
|
|
275
310
|
widgets: state.widgets,
|
|
311
|
+
collections: state.renderData.collections,
|
|
276
312
|
taxonomies: state.renderData.taxonomies,
|
|
277
313
|
...route,
|
|
278
314
|
route: routeContext,
|
|
@@ -303,6 +339,7 @@ async function renderPost(state, post) {
|
|
|
303
339
|
{
|
|
304
340
|
menus: state.previewData.menus,
|
|
305
341
|
widgets: state.widgets,
|
|
342
|
+
collections: state.renderData.collections,
|
|
306
343
|
taxonomies: state.renderData.taxonomies,
|
|
307
344
|
post,
|
|
308
345
|
route: buildRouteContext('post', currentUrl),
|
|
@@ -314,6 +351,7 @@ async function renderPost(state, post) {
|
|
|
314
351
|
image: post.featured_image,
|
|
315
352
|
publishedTime: post.published_at_iso,
|
|
316
353
|
modifiedTime: post.updated_at_iso,
|
|
354
|
+
robotsNoindex: shouldNoindexDocument(post),
|
|
317
355
|
}),
|
|
318
356
|
},
|
|
319
357
|
createRenderContext(state.previewData.site, currentUrl),
|
|
@@ -321,14 +359,16 @@ async function renderPost(state, post) {
|
|
|
321
359
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
322
360
|
html = injectSiteCustomizations(html, state);
|
|
323
361
|
await writeOutput(state.writer, state.summaries, routePathToOutputPath(post.url, state.previewData.site.permalinks.output_style), html, 'text/html');
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
362
|
+
if (!isDelistedDocument(post)) {
|
|
363
|
+
state.emitted.posts.push({
|
|
364
|
+
url: currentUrl,
|
|
365
|
+
title: post.title,
|
|
366
|
+
description: post.excerpt,
|
|
367
|
+
publishedAt: post.published_at_iso,
|
|
368
|
+
updatedAt: post.updated_at_iso,
|
|
369
|
+
status: post.status,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
332
372
|
}
|
|
333
373
|
|
|
334
374
|
async function renderPage(state, page) {
|
|
@@ -340,6 +380,7 @@ async function renderPage(state, page) {
|
|
|
340
380
|
{
|
|
341
381
|
menus: state.previewData.menus,
|
|
342
382
|
widgets: state.widgets,
|
|
383
|
+
collections: state.renderData.collections,
|
|
343
384
|
taxonomies: state.renderData.taxonomies,
|
|
344
385
|
page,
|
|
345
386
|
route: buildRouteContext('page', currentUrl),
|
|
@@ -350,6 +391,7 @@ async function renderPage(state, page) {
|
|
|
350
391
|
description: page.excerpt,
|
|
351
392
|
ogType: 'website',
|
|
352
393
|
image: page.featured_image,
|
|
394
|
+
robotsNoindex: shouldNoindexDocument(page),
|
|
353
395
|
}),
|
|
354
396
|
},
|
|
355
397
|
createRenderContext(state.previewData.site, currentUrl),
|
|
@@ -362,7 +404,7 @@ async function renderPage(state, page) {
|
|
|
362
404
|
title: page.title,
|
|
363
405
|
description: page.excerpt,
|
|
364
406
|
status: page.status,
|
|
365
|
-
includeInSitemap: page.omit_from_sitemap !== true,
|
|
407
|
+
includeInSitemap: page.omit_from_sitemap !== true && !isDelistedDocument(page),
|
|
366
408
|
});
|
|
367
409
|
}
|
|
368
410
|
|
|
@@ -376,6 +418,7 @@ async function maybeRenderNotFoundPage(state) {
|
|
|
376
418
|
{
|
|
377
419
|
menus: state.previewData.menus,
|
|
378
420
|
widgets: state.widgets,
|
|
421
|
+
collections: state.renderData.collections,
|
|
379
422
|
taxonomies: state.renderData.taxonomies,
|
|
380
423
|
route: buildRouteContext('not_found', '/404.html'),
|
|
381
424
|
meta: buildPageMeta(state.previewData.site, {
|
|
@@ -392,67 +435,246 @@ async function maybeRenderNotFoundPage(state) {
|
|
|
392
435
|
await writeOutput(state.writer, state.summaries, '404.html', html, 'text/html');
|
|
393
436
|
}
|
|
394
437
|
|
|
395
|
-
function normalizePreviewData(previewData) {
|
|
438
|
+
function normalizePreviewData(previewData, options = {}) {
|
|
396
439
|
const normalizedSite = {
|
|
397
440
|
...previewData.site,
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
? previewData.site.
|
|
441
|
+
media_base_url: normalizeOptionalString(previewData.site.media_base_url),
|
|
442
|
+
media_delivery_mode: MEDIA_DELIVERY_MODES.has(previewData.site.media_delivery_mode)
|
|
443
|
+
? previewData.site.media_delivery_mode
|
|
444
|
+
: 'none',
|
|
445
|
+
favicon: normalizeSiteFavicon(previewData.site.favicon || options.favicon),
|
|
446
|
+
posts_per_page: Number.isInteger(previewData.site.posts_per_page) && previewData.site.posts_per_page > 0
|
|
447
|
+
? previewData.site.posts_per_page
|
|
401
448
|
: DEFAULT_POSTS_PER_PAGE,
|
|
402
|
-
|
|
403
|
-
|
|
449
|
+
datetime_display: DATETIME_DISPLAY_MODES.has(previewData.site.datetime_display)
|
|
450
|
+
? previewData.site.datetime_display
|
|
451
|
+
: DEFAULT_DATETIME_DISPLAY,
|
|
452
|
+
date_style: DATETIME_STYLES.has(previewData.site.date_style)
|
|
453
|
+
? previewData.site.date_style
|
|
454
|
+
: DEFAULT_DATE_STYLE,
|
|
455
|
+
time_style: DATETIME_STYLES.has(previewData.site.time_style)
|
|
456
|
+
? previewData.site.time_style
|
|
457
|
+
: DEFAULT_TIME_STYLE,
|
|
404
458
|
timezone: normalizeNonEmptyString(previewData.site.timezone, DEFAULT_TIMEZONE),
|
|
405
459
|
locale: normalizeLocale(previewData.site.locale || DEFAULT_LOCALE),
|
|
406
|
-
|
|
460
|
+
disallow_comments: previewData.site.disallow_comments === true,
|
|
461
|
+
expose_generator: previewData.site.expose_generator !== false,
|
|
462
|
+
indexing: previewData.site.indexing !== false,
|
|
407
463
|
permalinks: normalizePermalinks(previewData.site.permalinks),
|
|
408
464
|
front_page: normalizeFrontPage(previewData.site.front_page),
|
|
409
465
|
post_index: normalizePostIndex(previewData.site.post_index),
|
|
410
466
|
footer: normalizeSiteFooter(previewData.site.footer),
|
|
411
467
|
};
|
|
468
|
+
const media = normalizeContentMedia(previewData.content.media, normalizedSite);
|
|
469
|
+
const mediaRegistry = buildMediaRegistry(media);
|
|
412
470
|
|
|
413
471
|
return {
|
|
414
472
|
...previewData,
|
|
415
473
|
site: normalizedSite,
|
|
416
|
-
|
|
474
|
+
menus: normalizeRecordMap(previewData.menus),
|
|
475
|
+
collections: normalizeCollections(previewData.collections),
|
|
476
|
+
widgets: normalizeWidgetAreas(previewData.widgets, normalizedSite.media_base_url),
|
|
417
477
|
custom_css: normalizeCustomCss(previewData.custom_css),
|
|
418
478
|
custom_html: normalizeCustomHtml(previewData.custom_html),
|
|
419
479
|
content: {
|
|
420
480
|
...previewData.content,
|
|
421
|
-
authors: previewData.content.authors.map((author) =>
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
481
|
+
authors: previewData.content.authors.map((author) => {
|
|
482
|
+
const avatar = normalizeMediaField(author.avatar, normalizedSite.media_base_url);
|
|
483
|
+
const avatarMedia = deriveManagedMedia(avatar, mediaRegistry, normalizedSite);
|
|
484
|
+
return {
|
|
485
|
+
...author,
|
|
486
|
+
avatar,
|
|
487
|
+
...(avatarMedia ? { avatar_media: avatarMedia } : {}),
|
|
488
|
+
};
|
|
489
|
+
}),
|
|
425
490
|
posts: previewData.content.posts
|
|
426
|
-
.map((post) =>
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
491
|
+
.map((post) => {
|
|
492
|
+
const featuredImage = normalizeMediaField(post.featured_image, normalizedSite.media_base_url);
|
|
493
|
+
const featuredMedia = deriveManagedMedia(featuredImage, mediaRegistry, normalizedSite);
|
|
494
|
+
return {
|
|
495
|
+
...post,
|
|
496
|
+
published_at_iso: normalizeIsoTimestamp(post.published_at_iso),
|
|
497
|
+
updated_at_iso: normalizeIsoTimestamp(post.updated_at_iso),
|
|
498
|
+
discoverability: normalizeDiscoverability(post.discoverability),
|
|
499
|
+
featured_image: featuredImage,
|
|
500
|
+
...(featuredMedia ? { featured_media: featuredMedia } : {}),
|
|
501
|
+
};
|
|
502
|
+
})
|
|
432
503
|
.sort((left, right) => toDate(right.published_at_iso).getTime() - toDate(left.published_at_iso).getTime()),
|
|
433
|
-
pages: previewData.content.pages.map((page) =>
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
504
|
+
pages: previewData.content.pages.map((page) => {
|
|
505
|
+
const featuredImage = normalizeMediaField(page.featured_image, normalizedSite.media_base_url);
|
|
506
|
+
const featuredMedia = deriveManagedMedia(featuredImage, mediaRegistry, normalizedSite);
|
|
507
|
+
return {
|
|
508
|
+
...page,
|
|
509
|
+
discoverability: normalizeDiscoverability(page.discoverability),
|
|
510
|
+
featured_image: featuredImage,
|
|
511
|
+
...(featuredMedia ? { featured_media: featuredMedia } : {}),
|
|
512
|
+
};
|
|
513
|
+
}),
|
|
437
514
|
categories: [...previewData.content.categories],
|
|
438
515
|
tags: [...previewData.content.tags],
|
|
516
|
+
media,
|
|
439
517
|
},
|
|
440
518
|
};
|
|
441
519
|
}
|
|
442
520
|
|
|
443
|
-
function
|
|
444
|
-
if (!
|
|
521
|
+
function normalizeRecordMap(value) {
|
|
522
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
523
|
+
return {};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return { ...value };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function normalizeDiscoverability(value) {
|
|
530
|
+
return DISCOVERABILITY_VALUES.has(value) ? value : 'default';
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function isDelistedDocument(document) {
|
|
534
|
+
return document?.discoverability === 'delist';
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function shouldNoindexDocument(document) {
|
|
538
|
+
return document?.discoverability === 'noindex' || document?.discoverability === 'delist';
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function normalizeContentMedia(mediaItems, site) {
|
|
542
|
+
if (!Array.isArray(mediaItems)) {
|
|
543
|
+
return [];
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return mediaItems
|
|
547
|
+
.map((item) => {
|
|
548
|
+
if (!item || typeof item !== 'object') {
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
const src = normalizeMediaField(item.src, site.media_base_url);
|
|
552
|
+
const width = Number.isInteger(item.width) && item.width > 0 ? item.width : 0;
|
|
553
|
+
const height = Number.isInteger(item.height) && item.height > 0 ? item.height : 0;
|
|
554
|
+
if (!src || !width || !height) {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
return {
|
|
558
|
+
src,
|
|
559
|
+
width,
|
|
560
|
+
height,
|
|
561
|
+
alt: typeof item.alt === 'string' ? item.alt : '',
|
|
562
|
+
};
|
|
563
|
+
})
|
|
564
|
+
.filter(Boolean);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function buildMediaRegistry(mediaItems) {
|
|
568
|
+
const registry = new Map();
|
|
569
|
+
for (const item of mediaItems) {
|
|
570
|
+
if (!registry.has(item.src)) {
|
|
571
|
+
registry.set(item.src, item);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return registry;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function deriveManagedMedia(src, mediaRegistry, site) {
|
|
578
|
+
const normalizedSrc = normalizeOptionalString(src);
|
|
579
|
+
if (!normalizedSrc) {
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
const media = mediaRegistry.get(normalizedSrc);
|
|
583
|
+
if (!media) {
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
return {
|
|
587
|
+
...media,
|
|
588
|
+
srcset: buildResponsiveImageSrcset(media, site),
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function buildResponsiveImageSrcset(media, site) {
|
|
593
|
+
if (site.media_delivery_mode !== 'media_domain') {
|
|
594
|
+
return '';
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const media_base_url = normalizeOptionalString(site.media_base_url);
|
|
598
|
+
if (!media_base_url || !isUrlUnderMediaBase(media.src, media_base_url) || !isResponsiveRasterImage(media.src)) {
|
|
599
|
+
return '';
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const widths = RESPONSIVE_IMAGE_WIDTHS.filter((width) => width <= media.width);
|
|
603
|
+
if (!widths.includes(media.width)) {
|
|
604
|
+
widths.push(media.width);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return widths
|
|
608
|
+
.filter((width, index, values) => width > 0 && values.indexOf(width) === index)
|
|
609
|
+
.map((width) => `${buildResponsiveImageVariantUrl(media.src, width)} ${width}w`)
|
|
610
|
+
.join(', ');
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function buildResponsiveImageVariantUrl(src, width) {
|
|
614
|
+
try {
|
|
615
|
+
const url = new URL(src);
|
|
616
|
+
url.searchParams.set('w', String(width));
|
|
617
|
+
url.searchParams.set('fit', 'scale-down');
|
|
618
|
+
url.searchParams.set('format', 'auto');
|
|
619
|
+
return decodeURI(url.toString());
|
|
620
|
+
} catch {
|
|
621
|
+
return src;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function isUrlUnderMediaBase(src, media_base_url) {
|
|
626
|
+
try {
|
|
627
|
+
const sourceUrl = new URL(src);
|
|
628
|
+
const baseUrl = new URL(media_base_url);
|
|
629
|
+
const basePath = baseUrl.pathname.endsWith('/') ? baseUrl.pathname : `${baseUrl.pathname}/`;
|
|
630
|
+
return sourceUrl.origin === baseUrl.origin && sourceUrl.pathname.startsWith(basePath);
|
|
631
|
+
} catch {
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function isResponsiveRasterImage(src) {
|
|
637
|
+
try {
|
|
638
|
+
const url = new URL(src);
|
|
639
|
+
const lastSegment = url.pathname.split('/').pop() || '';
|
|
640
|
+
const extension = lastSegment.includes('.') ? lastSegment.split('.').pop().toLowerCase() : '';
|
|
641
|
+
return RESPONSIVE_IMAGE_EXTENSIONS.has(extension);
|
|
642
|
+
} catch {
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function normalizeCollections(collections) {
|
|
648
|
+
if (!collections || typeof collections !== 'object' || Array.isArray(collections)) {
|
|
649
|
+
return {};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
return Object.fromEntries(
|
|
653
|
+
Object.entries(collections).map(([collectionId, collection]) => [
|
|
654
|
+
collectionId,
|
|
655
|
+
{
|
|
656
|
+
...collection,
|
|
657
|
+
title: normalizeOptionalString(collection?.title),
|
|
658
|
+
description: normalizeOptionalString(collection?.description),
|
|
659
|
+
items: Array.isArray(collection?.items) ? collection.items.map((item) => ({ ...item })) : [],
|
|
660
|
+
},
|
|
661
|
+
]),
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function normalizeWidgetAreas(widget_areas, media_base_url) {
|
|
666
|
+
if (!widget_areas || typeof widget_areas !== 'object') {
|
|
445
667
|
return {};
|
|
446
668
|
}
|
|
447
669
|
|
|
448
670
|
return Object.fromEntries(
|
|
449
|
-
Object.entries(
|
|
671
|
+
Object.entries(widget_areas).map(([widgetAreaId, widgetArea]) => [
|
|
450
672
|
widgetAreaId,
|
|
451
673
|
{
|
|
452
674
|
...widgetArea,
|
|
453
675
|
name: normalizeNonEmptyString(widgetArea?.name, widgetAreaId),
|
|
454
676
|
items: Array.isArray(widgetArea?.items)
|
|
455
|
-
? widgetArea.items.map((item) => normalizeWidgetItem(item,
|
|
677
|
+
? widgetArea.items.map((item) => normalizeWidgetItem(item, media_base_url))
|
|
456
678
|
: [],
|
|
457
679
|
},
|
|
458
680
|
]),
|
|
@@ -461,21 +683,15 @@ function normalizeWidgetAreas(widgetAreas, mediaBaseUrl) {
|
|
|
461
683
|
|
|
462
684
|
function normalizeSiteFooter(footer) {
|
|
463
685
|
const source = footer && typeof footer === 'object' && !Array.isArray(footer) ? footer : {};
|
|
464
|
-
const attribution = source.attribution && typeof source.attribution === 'object' && !Array.isArray(source.attribution)
|
|
465
|
-
? source.attribution
|
|
466
|
-
: {};
|
|
467
686
|
|
|
468
687
|
return {
|
|
469
688
|
...source,
|
|
470
689
|
copyright_text: normalizeOptionalString(source.copyright_text),
|
|
471
|
-
attribution:
|
|
472
|
-
...attribution,
|
|
473
|
-
enabled: attribution.enabled !== false,
|
|
474
|
-
},
|
|
690
|
+
attribution: source.attribution !== false,
|
|
475
691
|
};
|
|
476
692
|
}
|
|
477
693
|
|
|
478
|
-
function normalizeWidgetItem(item,
|
|
694
|
+
function normalizeWidgetItem(item, media_base_url) {
|
|
479
695
|
const normalizedItem = {
|
|
480
696
|
...item,
|
|
481
697
|
title: typeof item?.title === 'string' ? item.title.trim() : '',
|
|
@@ -486,7 +702,7 @@ function normalizeWidgetItem(item, mediaBaseUrl) {
|
|
|
486
702
|
...normalizedItem,
|
|
487
703
|
settings: {
|
|
488
704
|
...item.settings,
|
|
489
|
-
avatar: normalizeMediaField(item.settings.avatar,
|
|
705
|
+
avatar: normalizeMediaField(item.settings.avatar, media_base_url),
|
|
490
706
|
},
|
|
491
707
|
};
|
|
492
708
|
}
|
|
@@ -516,6 +732,22 @@ function normalizeCustomHtml(customHtml) {
|
|
|
516
732
|
};
|
|
517
733
|
}
|
|
518
734
|
|
|
735
|
+
function normalizeSiteFavicon(favicon) {
|
|
736
|
+
if (!favicon || typeof favicon !== 'object') {
|
|
737
|
+
return undefined;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const normalized = {};
|
|
741
|
+
for (const key of ['icon', 'svg', 'png', 'apple_touch_icon']) {
|
|
742
|
+
const value = normalizeOptionalString(favicon[key]);
|
|
743
|
+
if (value) {
|
|
744
|
+
normalized[key] = value;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
return Object.keys(normalized).length ? normalized : undefined;
|
|
749
|
+
}
|
|
750
|
+
|
|
519
751
|
function normalizePermalinks(permalinks) {
|
|
520
752
|
const source = permalinks && typeof permalinks === 'object' ? permalinks : {};
|
|
521
753
|
const outputStyle = typeof source.output_style === 'string' && PERMALINK_OUTPUT_STYLES.has(source.output_style)
|
|
@@ -547,21 +779,21 @@ function normalizeFrontPage(frontPage) {
|
|
|
547
779
|
};
|
|
548
780
|
}
|
|
549
781
|
|
|
550
|
-
function normalizePostIndex(
|
|
551
|
-
if (!
|
|
782
|
+
function normalizePostIndex(post_index) {
|
|
783
|
+
if (!post_index || typeof post_index !== 'object') {
|
|
552
784
|
return { ...DEFAULT_POST_INDEX };
|
|
553
785
|
}
|
|
554
786
|
|
|
555
787
|
return {
|
|
556
|
-
enabled:
|
|
557
|
-
path: normalizeNonEmptyString(
|
|
558
|
-
paginate:
|
|
788
|
+
enabled: post_index.enabled !== false,
|
|
789
|
+
path: normalizeNonEmptyString(post_index.path, DEFAULT_POST_INDEX.path),
|
|
790
|
+
paginate: post_index.paginate !== false,
|
|
559
791
|
};
|
|
560
792
|
}
|
|
561
793
|
|
|
562
794
|
function createRenderData(previewData, themeMetadata = {}) {
|
|
563
795
|
const themeSupportsComments = themeMetadata?.features?.comments === true;
|
|
564
|
-
const themeSupportsPostIndex = themeMetadata?.features?.
|
|
796
|
+
const themeSupportsPostIndex = themeMetadata?.features?.post_index !== false;
|
|
565
797
|
const authorsById = new Map(previewData.content.authors.map((author) => [author.id, author]));
|
|
566
798
|
const categoriesBySlug = new Map(previewData.content.categories.map((category) => [category.slug, category]));
|
|
567
799
|
const tagsBySlug = new Map(previewData.content.tags.map((tag) => [tag.slug, tag]));
|
|
@@ -569,8 +801,9 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
569
801
|
const tagPostsBySlug = new Map();
|
|
570
802
|
const categoryCountBySlug = new Map();
|
|
571
803
|
const tagCountBySlug = new Map();
|
|
804
|
+
const discoverableSourcePosts = previewData.content.posts.filter((post) => !isDelistedDocument(post));
|
|
572
805
|
|
|
573
|
-
for (const post of
|
|
806
|
+
for (const post of discoverableSourcePosts) {
|
|
574
807
|
for (const slug of post.category_slugs) {
|
|
575
808
|
pushToSlugMap(categoryPostsBySlug, slug, post);
|
|
576
809
|
categoryCountBySlug.set(slug, (categoryCountBySlug.get(slug) || 0) + 1);
|
|
@@ -583,24 +816,34 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
583
816
|
}
|
|
584
817
|
|
|
585
818
|
const preparedPosts = previewData.content.posts.map((post) => preparePost(post, previewData.site, authorsById, categoriesBySlug, tagsBySlug, themeSupportsComments));
|
|
586
|
-
const
|
|
819
|
+
const discoverablePreparedPosts = preparedPosts.filter((post) => !isDelistedDocument(post));
|
|
820
|
+
const adjacentPostsBySlug = new Map(
|
|
821
|
+
discoverablePreparedPosts.map((post, index) => [post.slug, {
|
|
822
|
+
prev: index > 0 ? buildAdjacentPostSummary(discoverablePreparedPosts[index - 1]) : null,
|
|
823
|
+
next: index < discoverablePreparedPosts.length - 1 ? buildAdjacentPostSummary(discoverablePreparedPosts[index + 1]) : null,
|
|
824
|
+
}]),
|
|
825
|
+
);
|
|
826
|
+
const posts = preparedPosts.map((post) => ({
|
|
587
827
|
...post,
|
|
588
|
-
prev:
|
|
589
|
-
next:
|
|
828
|
+
prev: adjacentPostsBySlug.get(post.slug)?.prev || null,
|
|
829
|
+
next: adjacentPostsBySlug.get(post.slug)?.next || null,
|
|
590
830
|
}));
|
|
591
831
|
const pages = previewData.content.pages.map((page) => preparePage(page, previewData.site));
|
|
592
832
|
const postBySlug = new Map(posts.map((post) => [post.slug, post]));
|
|
833
|
+
const pageBySlug = new Map(pages.map((page) => [page.slug, page]));
|
|
593
834
|
const frontPage = previewData.site.front_page;
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
const
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
835
|
+
const collections = resolveCollections(previewData.collections, postBySlug, pageBySlug, frontPage);
|
|
836
|
+
attachCollectionCursors(posts, pages, collections);
|
|
837
|
+
const post_index = previewData.site.post_index;
|
|
838
|
+
const effectivePostIndexEnabled = post_index.enabled !== false && themeSupportsPostIndex;
|
|
839
|
+
const effectivePostIndexPaginate = effectivePostIndexEnabled && post_index.paginate !== false;
|
|
840
|
+
const post_indexBasePath = normalizeRoutePath(post_index.path || DEFAULT_POST_INDEX.path);
|
|
841
|
+
|
|
842
|
+
if (frontPage.type !== 'theme_index' && effectivePostIndexEnabled && post_indexBasePath === '/') {
|
|
600
843
|
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.');
|
|
601
844
|
}
|
|
602
845
|
|
|
603
|
-
const frontPageRoute = buildFrontPageRoute(frontPage, pages, effectivePostIndexEnabled,
|
|
846
|
+
const frontPageRoute = buildFrontPageRoute(frontPage, pages, effectivePostIndexEnabled, post_indexBasePath);
|
|
604
847
|
const pageFrontPageSlug = frontPage.type === 'page' ? frontPage.page_slug : '';
|
|
605
848
|
const preparedPages = pageFrontPageSlug
|
|
606
849
|
? pages.filter((page) => page.slug !== pageFrontPageSlug)
|
|
@@ -610,21 +853,22 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
610
853
|
posts,
|
|
611
854
|
pages: preparedPages,
|
|
612
855
|
postBySlug,
|
|
856
|
+
collections,
|
|
613
857
|
taxonomies: buildGlobalTaxonomies(previewData, categoryCountBySlug, tagCountBySlug),
|
|
614
858
|
frontPageRoute,
|
|
615
859
|
indexRoutes: buildPostIndexRoutes({
|
|
616
860
|
enabled: effectivePostIndexEnabled,
|
|
617
861
|
paginate: effectivePostIndexPaginate,
|
|
618
|
-
items:
|
|
619
|
-
|
|
620
|
-
basePath:
|
|
862
|
+
items: discoverableSourcePosts,
|
|
863
|
+
posts_per_page: previewData.site.posts_per_page,
|
|
864
|
+
basePath: post_indexBasePath,
|
|
621
865
|
outputStyle: previewData.site.permalinks.output_style,
|
|
622
866
|
postBySlug,
|
|
623
867
|
frontPage,
|
|
624
868
|
}),
|
|
625
869
|
archiveRoutes: buildPaginatedCollection({
|
|
626
|
-
items:
|
|
627
|
-
|
|
870
|
+
items: discoverableSourcePosts,
|
|
871
|
+
posts_per_page: previewData.site.posts_per_page,
|
|
628
872
|
basePath: '/archive/',
|
|
629
873
|
outputStyle: previewData.site.permalinks.output_style,
|
|
630
874
|
}).map((entry) => ({
|
|
@@ -640,7 +884,7 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
640
884
|
items: previewData.content.categories,
|
|
641
885
|
postsBySlug: categoryPostsBySlug,
|
|
642
886
|
postBySlug,
|
|
643
|
-
|
|
887
|
+
posts_per_page: previewData.site.posts_per_page,
|
|
644
888
|
outputStyle: previewData.site.permalinks.output_style,
|
|
645
889
|
buildBasePath: (category) => resolvePermalink(previewData.site, 'categories', category).path,
|
|
646
890
|
renderExtras: (category) => ({
|
|
@@ -651,7 +895,7 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
651
895
|
items: previewData.content.tags,
|
|
652
896
|
postsBySlug: tagPostsBySlug,
|
|
653
897
|
postBySlug,
|
|
654
|
-
|
|
898
|
+
posts_per_page: previewData.site.posts_per_page,
|
|
655
899
|
outputStyle: previewData.site.permalinks.output_style,
|
|
656
900
|
buildBasePath: (tag) => resolvePermalink(previewData.site, 'tags', tag).path,
|
|
657
901
|
renderExtras: (tag) => ({
|
|
@@ -668,9 +912,134 @@ function buildGlobalTaxonomies(previewData, categoryCountBySlug, tagCountBySlug)
|
|
|
668
912
|
};
|
|
669
913
|
}
|
|
670
914
|
|
|
671
|
-
function
|
|
915
|
+
function resolveCollections(collections, postBySlug, pageBySlug, frontPage) {
|
|
916
|
+
if (!collections || typeof collections !== 'object') {
|
|
917
|
+
return {};
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
return Object.fromEntries(
|
|
921
|
+
Object.entries(collections).map(([collectionId, collection]) => {
|
|
922
|
+
const items = resolveCollectionItems(collectionId, collection?.items, postBySlug, pageBySlug, frontPage);
|
|
923
|
+
return [
|
|
924
|
+
collectionId,
|
|
925
|
+
{
|
|
926
|
+
id: collectionId,
|
|
927
|
+
title: normalizeOptionalString(collection?.title),
|
|
928
|
+
description: normalizeOptionalString(collection?.description),
|
|
929
|
+
count: items.length,
|
|
930
|
+
items,
|
|
931
|
+
},
|
|
932
|
+
];
|
|
933
|
+
}),
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function resolveCollectionItems(collectionId, items, postBySlug, pageBySlug, frontPage) {
|
|
938
|
+
if (!Array.isArray(items)) {
|
|
939
|
+
return [];
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
return items.map((item, index) => resolveCollectionItem(collectionId, item, index, postBySlug, pageBySlug, frontPage));
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function resolveCollectionItem(collectionId, item, index, postBySlug, pageBySlug, frontPage) {
|
|
946
|
+
if (item?.type === 'post') {
|
|
947
|
+
const post = postBySlug.get(item.slug);
|
|
948
|
+
if (!post) {
|
|
949
|
+
throw new Error(`Invalid collection "${collectionId}": item ${index + 1} references missing post slug "${item.slug}".`);
|
|
950
|
+
}
|
|
951
|
+
return {
|
|
952
|
+
type: 'post',
|
|
953
|
+
meta: post.meta,
|
|
954
|
+
...buildStructuredPostSummary(post),
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
if (item?.type === 'page') {
|
|
959
|
+
const page = pageBySlug.get(item.slug);
|
|
960
|
+
if (!page) {
|
|
961
|
+
throw new Error(`Invalid collection "${collectionId}": item ${index + 1} references missing page slug "${item.slug}".`);
|
|
962
|
+
}
|
|
963
|
+
return buildCollectionPageSummary(page, frontPage);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
throw new Error(`Invalid collection "${collectionId}": item ${index + 1} has unsupported type "${item?.type}".`);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function buildCollectionPageSummary(page, frontPage) {
|
|
970
|
+
return {
|
|
971
|
+
type: 'page',
|
|
972
|
+
title: page.title,
|
|
973
|
+
slug: page.slug,
|
|
974
|
+
url: frontPage?.type === 'page' && frontPage.page_slug === page.slug ? '/' : page.url,
|
|
975
|
+
excerpt: page.excerpt || '',
|
|
976
|
+
featured_image: page.featured_image || '',
|
|
977
|
+
...(page.featured_media ? { featured_media: { ...page.featured_media } } : {}),
|
|
978
|
+
meta: page.meta,
|
|
979
|
+
data: page.data,
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function attachCollectionCursors(posts, pages, collections) {
|
|
984
|
+
const postTargets = new Map(posts.map((post) => [post.slug, post]));
|
|
985
|
+
const pageTargets = new Map(pages.map((page) => [page.slug, page]));
|
|
986
|
+
|
|
987
|
+
for (const [collectionId, collection] of Object.entries(collections || {})) {
|
|
988
|
+
const items = Array.isArray(collection.items) ? collection.items : [];
|
|
989
|
+
|
|
990
|
+
items.forEach((item, index) => {
|
|
991
|
+
const target = item.type === 'post'
|
|
992
|
+
? postTargets.get(item.slug)
|
|
993
|
+
: item.type === 'page'
|
|
994
|
+
? pageTargets.get(item.slug)
|
|
995
|
+
: null;
|
|
996
|
+
|
|
997
|
+
if (!target) {
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
target.collection_cursors = target.collection_cursors || {};
|
|
1002
|
+
target.collection_cursors[collectionId] = buildCollectionCursor(collectionId, collection, items, index);
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function buildCollectionCursor(collectionId, collection, items, index) {
|
|
1008
|
+
const count = items.length;
|
|
1009
|
+
|
|
1010
|
+
return {
|
|
1011
|
+
collection_id: collectionId,
|
|
1012
|
+
collection_title: collection.title || '',
|
|
1013
|
+
index,
|
|
1014
|
+
position: index + 1,
|
|
1015
|
+
count,
|
|
1016
|
+
first: index === 0,
|
|
1017
|
+
last: index === count - 1,
|
|
1018
|
+
prev: index > 0 ? buildCollectionCursorItemSummary(items[index - 1]) : null,
|
|
1019
|
+
next: index < count - 1 ? buildCollectionCursorItemSummary(items[index + 1]) : null,
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function buildCollectionCursorItemSummary(item) {
|
|
1024
|
+
if (!item) {
|
|
1025
|
+
return null;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
return {
|
|
1029
|
+
type: item.type,
|
|
1030
|
+
title: item.title,
|
|
1031
|
+
slug: item.slug,
|
|
1032
|
+
url: item.url,
|
|
1033
|
+
excerpt: item.excerpt || '',
|
|
1034
|
+
featured_image: item.featured_image || '',
|
|
1035
|
+
meta: item.meta,
|
|
1036
|
+
data: item.data,
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function buildFrontPageRoute(frontPage, pages, effectivePostIndexEnabled, post_indexBasePath) {
|
|
672
1041
|
if (frontPage.type === 'theme_index') {
|
|
673
|
-
if (effectivePostIndexEnabled &&
|
|
1042
|
+
if (effectivePostIndexEnabled && post_indexBasePath === '/') {
|
|
674
1043
|
return null;
|
|
675
1044
|
}
|
|
676
1045
|
|
|
@@ -716,7 +1085,7 @@ function buildPostIndexRoutes(options) {
|
|
|
716
1085
|
}
|
|
717
1086
|
|
|
718
1087
|
if (!options.paginate) {
|
|
719
|
-
const items = options.items.slice(0, options.
|
|
1088
|
+
const items = options.items.slice(0, options.posts_per_page);
|
|
720
1089
|
return [{
|
|
721
1090
|
path: options.basePath,
|
|
722
1091
|
route_type: 'post_index',
|
|
@@ -731,7 +1100,7 @@ function buildPostIndexRoutes(options) {
|
|
|
731
1100
|
|
|
732
1101
|
return buildPaginatedCollection({
|
|
733
1102
|
items: options.items,
|
|
734
|
-
|
|
1103
|
+
posts_per_page: options.posts_per_page,
|
|
735
1104
|
basePath: options.basePath,
|
|
736
1105
|
outputStyle: options.outputStyle,
|
|
737
1106
|
}).map((entry) => ({
|
|
@@ -822,6 +1191,7 @@ function resolveWidgetItem(item, previewData, renderData, widgetAreaId, index) {
|
|
|
822
1191
|
function resolveRecentPostsWidget(baseWidget, settings, renderData) {
|
|
823
1192
|
const limit = clampInteger(settings?.limit, 5, 1, 20);
|
|
824
1193
|
const items = renderData.posts
|
|
1194
|
+
.filter((post) => !isDelistedDocument(post))
|
|
825
1195
|
.slice(0, limit)
|
|
826
1196
|
.map((post) => ({
|
|
827
1197
|
title: post.title,
|
|
@@ -1031,8 +1401,11 @@ function preparePost(post, site, authorsById, categoriesBySlug, tagsBySlug, them
|
|
|
1031
1401
|
updated_at_iso: post.updated_at_iso,
|
|
1032
1402
|
author_id: post.author_id,
|
|
1033
1403
|
featured_image: post.featured_image,
|
|
1404
|
+
...(post.featured_media ? { featured_media: { ...post.featured_media } } : {}),
|
|
1034
1405
|
meta: post.meta,
|
|
1406
|
+
data: post.data,
|
|
1035
1407
|
status: post.status,
|
|
1408
|
+
discoverability: post.discoverability,
|
|
1036
1409
|
allow_comments: post.allow_comments,
|
|
1037
1410
|
category_slugs: post.category_slugs,
|
|
1038
1411
|
tag_slugs: post.tag_slugs,
|
|
@@ -1040,6 +1413,7 @@ function preparePost(post, site, authorsById, categoriesBySlug, tagsBySlug, them
|
|
|
1040
1413
|
id: post.author_id,
|
|
1041
1414
|
display_name: normalizeNonEmptyString(author?.display_name, post.author_id),
|
|
1042
1415
|
avatar: author?.avatar || '',
|
|
1416
|
+
...(author?.avatar_media ? { avatar_media: { ...author.avatar_media } } : {}),
|
|
1043
1417
|
},
|
|
1044
1418
|
categories,
|
|
1045
1419
|
tags,
|
|
@@ -1048,7 +1422,7 @@ function preparePost(post, site, authorsById, categoriesBySlug, tagsBySlug, them
|
|
|
1048
1422
|
published_at: formatTimestamp(post.published_at_iso, site),
|
|
1049
1423
|
updated_at: formatTimestamp(post.updated_at_iso, site),
|
|
1050
1424
|
reading_time: calculateReadingTime(renderedDocument.html),
|
|
1051
|
-
comments_enabled: themeSupportsComments && site.
|
|
1425
|
+
comments_enabled: themeSupportsComments && site.disallow_comments !== true && post.allow_comments === true,
|
|
1052
1426
|
};
|
|
1053
1427
|
}
|
|
1054
1428
|
|
|
@@ -1075,7 +1449,7 @@ function buildTaxonomyRoutes(options) {
|
|
|
1075
1449
|
|
|
1076
1450
|
const paginated = buildPaginatedCollection({
|
|
1077
1451
|
items: matchedPosts,
|
|
1078
|
-
|
|
1452
|
+
posts_per_page: options.posts_per_page,
|
|
1079
1453
|
basePath: options.buildBasePath(item),
|
|
1080
1454
|
outputStyle: options.outputStyle,
|
|
1081
1455
|
});
|
|
@@ -1099,14 +1473,14 @@ function buildTaxonomyRoutes(options) {
|
|
|
1099
1473
|
function buildPaginatedCollection(options) {
|
|
1100
1474
|
const basePath = normalizePaginationBasePath(options.basePath);
|
|
1101
1475
|
const outputStyle = PERMALINK_OUTPUT_STYLES.has(options.outputStyle) ? options.outputStyle : DEFAULT_PERMALINKS.output_style;
|
|
1102
|
-
const
|
|
1476
|
+
const posts_per_page = Number.isInteger(options.posts_per_page) && options.posts_per_page > 0 ? options.posts_per_page : DEFAULT_POSTS_PER_PAGE;
|
|
1103
1477
|
const totalPosts = options.items.length;
|
|
1104
|
-
const totalPages = Math.max(1, Math.ceil(totalPosts /
|
|
1478
|
+
const totalPages = Math.max(1, Math.ceil(totalPosts / posts_per_page));
|
|
1105
1479
|
const pages = [];
|
|
1106
1480
|
|
|
1107
1481
|
for (let page = 1; page <= totalPages; page += 1) {
|
|
1108
|
-
const start = (page - 1) *
|
|
1109
|
-
const end = start +
|
|
1482
|
+
const start = (page - 1) * posts_per_page;
|
|
1483
|
+
const end = start + posts_per_page;
|
|
1110
1484
|
|
|
1111
1485
|
pages.push({
|
|
1112
1486
|
path: buildPaginatedPath(basePath, page),
|
|
@@ -1281,9 +1655,13 @@ function buildStructuredPostSummary(post) {
|
|
|
1281
1655
|
published_at_iso: post.published_at_iso,
|
|
1282
1656
|
reading_time: post.reading_time,
|
|
1283
1657
|
featured_image: post.featured_image,
|
|
1658
|
+
...(post.featured_media ? { featured_media: { ...post.featured_media } } : {}),
|
|
1659
|
+
meta: post.meta,
|
|
1660
|
+
data: post.data,
|
|
1284
1661
|
author: {
|
|
1285
1662
|
display_name: post.author?.display_name || '',
|
|
1286
1663
|
avatar: post.author?.avatar || '',
|
|
1664
|
+
...(post.author?.avatar_media ? { avatar_media: { ...post.author.avatar_media } } : {}),
|
|
1287
1665
|
},
|
|
1288
1666
|
categories: Array.isArray(post.categories) ? post.categories.map((category) => ({ ...category })) : [],
|
|
1289
1667
|
tags: Array.isArray(post.tags) ? post.tags.map((tag) => ({ ...tag })) : [],
|
|
@@ -1302,6 +1680,7 @@ function buildAdjacentPostSummary(post) {
|
|
|
1302
1680
|
excerpt: post.excerpt,
|
|
1303
1681
|
published_at: post.published_at,
|
|
1304
1682
|
published_at_iso: post.published_at_iso,
|
|
1683
|
+
data: post.data,
|
|
1305
1684
|
};
|
|
1306
1685
|
}
|
|
1307
1686
|
|
|
@@ -1391,50 +1770,25 @@ function formatArchiveLabel(date, site) {
|
|
|
1391
1770
|
function formatTimestamp(value, site) {
|
|
1392
1771
|
const date = toDate(value);
|
|
1393
1772
|
const locale = normalizeLocale(site.locale || DEFAULT_LOCALE);
|
|
1394
|
-
const
|
|
1395
|
-
const
|
|
1773
|
+
const dateStyle = DATETIME_STYLES.has(site.date_style) ? site.date_style : DEFAULT_DATE_STYLE;
|
|
1774
|
+
const timeStyle = DATETIME_STYLES.has(site.time_style) ? site.time_style : DEFAULT_TIME_STYLE;
|
|
1396
1775
|
const siteTimezone = normalizeNonEmptyString(site.timezone, DEFAULT_TIMEZONE);
|
|
1397
1776
|
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
year: 'numeric',
|
|
1401
|
-
month: dateFormat === 'MMMM D, YYYY' ? 'long' : '2-digit',
|
|
1402
|
-
day: '2-digit',
|
|
1403
|
-
}).formatToParts(date);
|
|
1404
|
-
|
|
1405
|
-
const year = dateParts.find((part) => part.type === 'year')?.value || '';
|
|
1406
|
-
const month = dateParts.find((part) => part.type === 'month')?.value || '';
|
|
1407
|
-
const day = dateParts.find((part) => part.type === 'day')?.value || '';
|
|
1408
|
-
|
|
1409
|
-
let formattedDate = `${year}-${month}-${day}`;
|
|
1410
|
-
if (dateFormat === 'DD/MM/YYYY') {
|
|
1411
|
-
formattedDate = `${day}/${month}/${year}`;
|
|
1412
|
-
} else if (dateFormat === 'MM/DD/YYYY') {
|
|
1413
|
-
formattedDate = `${month}/${day}/${year}`;
|
|
1414
|
-
} else if (dateFormat === 'MMMM D, YYYY') {
|
|
1415
|
-
formattedDate = `${month} ${String(Number(day))}, ${year}`;
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
if (!timeFormat) {
|
|
1419
|
-
return formattedDate;
|
|
1777
|
+
if (dateStyle === 'none' && timeStyle === 'none') {
|
|
1778
|
+
return '';
|
|
1420
1779
|
}
|
|
1421
1780
|
|
|
1422
|
-
const
|
|
1781
|
+
const options = {
|
|
1423
1782
|
timeZone: siteTimezone,
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
const dayPeriod = (timeParts.find((part) => part.type === 'dayPeriod')?.value || '').toUpperCase();
|
|
1432
|
-
|
|
1433
|
-
const formattedTime = timeFormat === 'hh:mm A'
|
|
1434
|
-
? `${hour}:${minute}${dayPeriod ? ` ${dayPeriod}` : ''}`
|
|
1435
|
-
: `${hour}:${minute}`;
|
|
1783
|
+
};
|
|
1784
|
+
if (dateStyle !== 'none') {
|
|
1785
|
+
options.dateStyle = dateStyle;
|
|
1786
|
+
}
|
|
1787
|
+
if (timeStyle !== 'none') {
|
|
1788
|
+
options.timeStyle = timeStyle;
|
|
1789
|
+
}
|
|
1436
1790
|
|
|
1437
|
-
return
|
|
1791
|
+
return new Intl.DateTimeFormat(locale, options).format(date);
|
|
1438
1792
|
}
|
|
1439
1793
|
|
|
1440
1794
|
function calculateReadingTime(html) {
|
|
@@ -1530,10 +1884,12 @@ async function normalizeAndValidateThemePackage(themePackage) {
|
|
|
1530
1884
|
...(themePackage.metadata.author ? { author: themePackage.metadata.author } : {}),
|
|
1531
1885
|
...(themePackage.metadata.description ? { description: themePackage.metadata.description } : {}),
|
|
1532
1886
|
...(themePackage.metadata.thumbnail ? { thumbnail: themePackage.metadata.thumbnail } : {}),
|
|
1887
|
+
...(themePackage.metadata.links ? { links: themePackage.metadata.links } : {}),
|
|
1533
1888
|
...(themePackage.metadata.features ? { features: themePackage.metadata.features } : {}),
|
|
1534
|
-
...(themePackage.metadata.
|
|
1535
|
-
...(themePackage.metadata.
|
|
1536
|
-
|
|
1889
|
+
...(themePackage.metadata.menu_slots ? { menu_slots: themePackage.metadata.menu_slots } : {}),
|
|
1890
|
+
...(themePackage.metadata.widget_areas ? { widget_areas: themePackage.metadata.widget_areas } : {}),
|
|
1891
|
+
...(themePackage.metadata.site_meta ? { site_meta: themePackage.metadata.site_meta } : {}),
|
|
1892
|
+
...(themePackage.metadata.collection_slots ? { collection_slots: themePackage.metadata.collection_slots } : {}),
|
|
1537
1893
|
}));
|
|
1538
1894
|
|
|
1539
1895
|
for (const [templateName, templateContent] of themePackage.templates.entries()) {
|
|
@@ -1550,7 +1906,7 @@ async function normalizeAndValidateThemePackage(themePackage) {
|
|
|
1550
1906
|
|
|
1551
1907
|
const validation = await validateThemeFiles(fileMap);
|
|
1552
1908
|
if (!validation.ok) {
|
|
1553
|
-
throw new Error(
|
|
1909
|
+
throw new Error(formatThemeValidationFailure(validation));
|
|
1554
1910
|
}
|
|
1555
1911
|
|
|
1556
1912
|
if (!validation.manifest) {
|
|
@@ -1565,11 +1921,65 @@ async function normalizeAndValidateThemePackage(themePackage) {
|
|
|
1565
1921
|
};
|
|
1566
1922
|
}
|
|
1567
1923
|
|
|
1924
|
+
function formatThemeValidationFailure(validation) {
|
|
1925
|
+
const blocks = [
|
|
1926
|
+
[
|
|
1927
|
+
'Theme validation failed',
|
|
1928
|
+
`Errors: ${validation.errors.length}`,
|
|
1929
|
+
`Checked files: ${validation.checkedFiles}`,
|
|
1930
|
+
].join('\n'),
|
|
1931
|
+
...validation.errors.map((issue) => formatThemeValidationIssue(issue)),
|
|
1932
|
+
];
|
|
1933
|
+
return blocks.join('\n\n');
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
function formatThemeValidationIssue(issue) {
|
|
1937
|
+
if (!issue) {
|
|
1938
|
+
return 'Reason: Unknown error';
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
const lines = [`ERROR ${issue.code || 'THEME_VALIDATION_ERROR'}`];
|
|
1942
|
+
const location = splitIssuePath(issue.path);
|
|
1943
|
+
if (location.file) {
|
|
1944
|
+
lines.push(`File: ${location.file}`);
|
|
1945
|
+
}
|
|
1946
|
+
if (location.path) {
|
|
1947
|
+
lines.push(`Path: ${location.path}`);
|
|
1948
|
+
}
|
|
1949
|
+
if (Number.isInteger(issue.line) && Number.isInteger(issue.column)) {
|
|
1950
|
+
lines.push(`Line: ${issue.line}, Column: ${issue.column}`);
|
|
1951
|
+
}
|
|
1952
|
+
if (issue.category) {
|
|
1953
|
+
lines.push(`Category: ${issue.category}`);
|
|
1954
|
+
}
|
|
1955
|
+
lines.push(`Reason: ${issue.message || 'Unknown error'}`);
|
|
1956
|
+
if (issue.snippet) {
|
|
1957
|
+
const lineLabel = Number.isInteger(issue.line) ? String(issue.line) : '';
|
|
1958
|
+
lines.push('', `${lineLabel} | ${issue.snippet.line}`, `${' '.repeat(lineLabel.length)} | ${issue.snippet.pointer}`);
|
|
1959
|
+
}
|
|
1960
|
+
if (issue.hint) {
|
|
1961
|
+
lines.push('', 'Hint:', issue.hint);
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
return lines.join('\n');
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
function splitIssuePath(issuePath) {
|
|
1968
|
+
const normalizedPath = String(issuePath || '');
|
|
1969
|
+
if (normalizedPath.startsWith('theme.json.')) {
|
|
1970
|
+
return {
|
|
1971
|
+
file: 'theme.json',
|
|
1972
|
+
path: normalizedPath.slice('theme.json.'.length),
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
return { file: normalizedPath, path: '' };
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1568
1979
|
function normalizeThemePackageMetadata(sourceMetadata, manifest) {
|
|
1569
1980
|
return {
|
|
1570
1981
|
...manifest,
|
|
1571
1982
|
...(sourceMetadata?.thumbnail ? { thumbnail: sourceMetadata.thumbnail } : {}),
|
|
1572
|
-
settings: sourceMetadata?.settings || {},
|
|
1573
1983
|
};
|
|
1574
1984
|
}
|
|
1575
1985
|
|
|
@@ -1647,6 +2057,7 @@ function buildPageMeta(site, options = {}) {
|
|
|
1647
2057
|
const ogType = normalizeNonEmptyString(options.ogType, 'website');
|
|
1648
2058
|
const publishedTime = normalizeOptionalString(options.publishedTime);
|
|
1649
2059
|
const modifiedTime = normalizeOptionalString(options.modifiedTime);
|
|
2060
|
+
const robotsNoindex = options.robotsNoindex === true;
|
|
1650
2061
|
|
|
1651
2062
|
const meta = {
|
|
1652
2063
|
title: escapeHtml(resolvedTitle),
|
|
@@ -1660,6 +2071,7 @@ function buildPageMeta(site, options = {}) {
|
|
|
1660
2071
|
og_image: ogImage ? escapeHtml(ogImage) : '',
|
|
1661
2072
|
article_published_time: publishedTime ? escapeHtml(publishedTime) : '',
|
|
1662
2073
|
article_modified_time: modifiedTime ? escapeHtml(modifiedTime) : '',
|
|
2074
|
+
robots_noindex: robotsNoindex,
|
|
1663
2075
|
};
|
|
1664
2076
|
|
|
1665
2077
|
return {
|
|
@@ -1680,6 +2092,9 @@ function buildMetaHeadTags(meta) {
|
|
|
1680
2092
|
if (meta.description) {
|
|
1681
2093
|
tags.push(`<meta name="description" content="${meta.description}">`);
|
|
1682
2094
|
}
|
|
2095
|
+
if (meta.robots_noindex) {
|
|
2096
|
+
tags.push('<meta name="robots" content="noindex">');
|
|
2097
|
+
}
|
|
1683
2098
|
if (meta.canonical_url) {
|
|
1684
2099
|
tags.push(`<link rel="canonical" href="${meta.canonical_url}">`);
|
|
1685
2100
|
}
|
|
@@ -1727,7 +2142,7 @@ function resolveMetaImageUrl(image) {
|
|
|
1727
2142
|
return '';
|
|
1728
2143
|
}
|
|
1729
2144
|
|
|
1730
|
-
function normalizeMediaField(value,
|
|
2145
|
+
function normalizeMediaField(value, media_base_url) {
|
|
1731
2146
|
if (value === undefined) {
|
|
1732
2147
|
return undefined;
|
|
1733
2148
|
}
|
|
@@ -1741,9 +2156,9 @@ function normalizeMediaField(value, mediaBaseUrl) {
|
|
|
1741
2156
|
return normalizeAbsoluteUrl(normalizedValue, SAFE_MEDIA_PROTOCOLS);
|
|
1742
2157
|
}
|
|
1743
2158
|
|
|
1744
|
-
const normalizedBaseUrl = normalizeOptionalString(
|
|
2159
|
+
const normalizedBaseUrl = normalizeOptionalString(media_base_url);
|
|
1745
2160
|
if (!normalizedBaseUrl) {
|
|
1746
|
-
return
|
|
2161
|
+
return normalizedValue;
|
|
1747
2162
|
}
|
|
1748
2163
|
|
|
1749
2164
|
try {
|
|
@@ -1979,7 +2394,11 @@ function assertPlannedOutputPathsSafe(state) {
|
|
|
1979
2394
|
];
|
|
1980
2395
|
|
|
1981
2396
|
if (state.options.generateSpecialFiles) {
|
|
1982
|
-
plannedPaths.push(
|
|
2397
|
+
plannedPaths.push(SEARCH_INDEX_OUTPUT_PATH, SEARCH_ADAPTER_OUTPUT_PATH);
|
|
2398
|
+
plannedPaths.push('404.html');
|
|
2399
|
+
if (shouldGenerateRobotsTxt(state.options)) {
|
|
2400
|
+
plannedPaths.push('robots.txt');
|
|
2401
|
+
}
|
|
1983
2402
|
if (hasCanonicalSiteUrl(state.previewData.site.url)) {
|
|
1984
2403
|
plannedPaths.push('sitemap.xml', 'feed.xml');
|
|
1985
2404
|
}
|
|
@@ -2079,11 +2498,52 @@ function sha256(content) {
|
|
|
2079
2498
|
}
|
|
2080
2499
|
|
|
2081
2500
|
function injectSiteCustomizations(html, state) {
|
|
2082
|
-
let next =
|
|
2501
|
+
let next = injectFaviconLinks(html, state.favicon);
|
|
2502
|
+
next = injectGeneratorMeta(next, state.exposeGenerator);
|
|
2503
|
+
next = injectCustomCssAssetLink(next, state.customCssHref);
|
|
2083
2504
|
next = injectCustomHtml(next, state.customHtml);
|
|
2084
2505
|
return next;
|
|
2085
2506
|
}
|
|
2086
2507
|
|
|
2508
|
+
function injectFaviconLinks(html, favicon) {
|
|
2509
|
+
const links = buildFaviconLinks(favicon);
|
|
2510
|
+
if (!links) {
|
|
2511
|
+
return html;
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
return html.replace('</head>', `${links}\n</head>`);
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
function buildFaviconLinks(favicon) {
|
|
2518
|
+
if (!favicon || typeof favicon !== 'object') {
|
|
2519
|
+
return '';
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
const lines = [];
|
|
2523
|
+
if (normalizeOptionalString(favicon.icon)) {
|
|
2524
|
+
lines.push(` <link rel="icon" href="${escapeHtml(favicon.icon)}" sizes="any">`);
|
|
2525
|
+
}
|
|
2526
|
+
if (normalizeOptionalString(favicon.svg)) {
|
|
2527
|
+
lines.push(` <link rel="icon" href="${escapeHtml(favicon.svg)}" type="image/svg+xml">`);
|
|
2528
|
+
}
|
|
2529
|
+
if (normalizeOptionalString(favicon.png)) {
|
|
2530
|
+
lines.push(` <link rel="icon" href="${escapeHtml(favicon.png)}" type="image/png">`);
|
|
2531
|
+
}
|
|
2532
|
+
if (normalizeOptionalString(favicon.apple_touch_icon)) {
|
|
2533
|
+
lines.push(` <link rel="apple-touch-icon" href="${escapeHtml(favicon.apple_touch_icon)}">`);
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
return lines.join('\n');
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
function injectGeneratorMeta(html, exposeGenerator) {
|
|
2540
|
+
if (exposeGenerator === false) {
|
|
2541
|
+
return html;
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
return html.replace('</head>', ' <meta name="generator" content="ZeroPress">\n</head>');
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2087
2547
|
function injectCustomCssAssetLink(html, href) {
|
|
2088
2548
|
if (!normalizeOptionalString(href)) {
|
|
2089
2549
|
return html;
|
|
@@ -2107,9 +2567,407 @@ function injectCustomHtml(html, customHtml) {
|
|
|
2107
2567
|
return next;
|
|
2108
2568
|
}
|
|
2109
2569
|
|
|
2110
|
-
function
|
|
2570
|
+
function buildSearchIndexJson(state) {
|
|
2571
|
+
return `${JSON.stringify(buildSearchIndexItems(state), null, 2)}\n`;
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
function buildSearchIndexItems(state) {
|
|
2575
|
+
const posts = state.renderData.posts
|
|
2576
|
+
.filter((post) => post.status === 'published' && !isDelistedDocument(post))
|
|
2577
|
+
.map((post) => ({
|
|
2578
|
+
id: `post:${post.slug}`,
|
|
2579
|
+
type: 'post',
|
|
2580
|
+
title: post.title,
|
|
2581
|
+
url: post.url,
|
|
2582
|
+
excerpt: normalizeSearchText(post.excerpt),
|
|
2583
|
+
headings: buildSearchHeadings(post.toc),
|
|
2584
|
+
categories: Array.isArray(post.categories) ? post.categories.map((category) => category.name).filter(Boolean) : [],
|
|
2585
|
+
tags: Array.isArray(post.tags) ? post.tags.map((tag) => tag.name).filter(Boolean) : [],
|
|
2586
|
+
published_at_iso: normalizeIsoTimestamp(post.published_at_iso),
|
|
2587
|
+
updated_at_iso: normalizeIsoTimestamp(post.updated_at_iso),
|
|
2588
|
+
content_text: htmlToSearchText(post.html),
|
|
2589
|
+
}));
|
|
2590
|
+
|
|
2591
|
+
const frontPagePage = state.renderData.frontPageRoute?.front_page_type === 'page'
|
|
2592
|
+
? state.renderData.frontPageRoute.page
|
|
2593
|
+
: null;
|
|
2594
|
+
const frontPageItems = frontPagePage && frontPagePage.status === 'published' && !isDelistedDocument(frontPagePage)
|
|
2595
|
+
? [buildSearchPageItem(frontPagePage, '/')]
|
|
2596
|
+
: [];
|
|
2597
|
+
const pageItems = state.renderData.pages
|
|
2598
|
+
.filter((page) => page.status === 'published' && !isDelistedDocument(page))
|
|
2599
|
+
.map((page) => buildSearchPageItem(page, page.url));
|
|
2600
|
+
|
|
2601
|
+
return [...posts, ...frontPageItems, ...pageItems];
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
function buildSearchPageItem(page, url) {
|
|
2605
|
+
return {
|
|
2606
|
+
id: `page:${page.slug}`,
|
|
2607
|
+
type: 'page',
|
|
2608
|
+
title: page.title,
|
|
2609
|
+
url,
|
|
2610
|
+
excerpt: normalizeSearchText(page.excerpt),
|
|
2611
|
+
headings: buildSearchHeadings(page.toc),
|
|
2612
|
+
categories: [],
|
|
2613
|
+
tags: [],
|
|
2614
|
+
published_at_iso: '',
|
|
2615
|
+
updated_at_iso: '',
|
|
2616
|
+
content_text: htmlToSearchText(page.html),
|
|
2617
|
+
};
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
function buildSearchHeadings(toc) {
|
|
2621
|
+
return Array.isArray(toc)
|
|
2622
|
+
? toc.map((item) => normalizeSearchText(item?.title)).filter(Boolean)
|
|
2623
|
+
: [];
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
function htmlToSearchText(html) {
|
|
2627
|
+
return normalizeSearchText(decodeHtmlEntities(
|
|
2628
|
+
String(html || '')
|
|
2629
|
+
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, ' ')
|
|
2630
|
+
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, ' ')
|
|
2631
|
+
.replace(/<!--[\s\S]*?-->/g, ' ')
|
|
2632
|
+
.replace(/<[^>]+>/g, ' '),
|
|
2633
|
+
));
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
function decodeHtmlEntities(value) {
|
|
2637
|
+
const namedEntities = {
|
|
2638
|
+
amp: '&',
|
|
2639
|
+
lt: '<',
|
|
2640
|
+
gt: '>',
|
|
2641
|
+
quot: '"',
|
|
2642
|
+
apos: "'",
|
|
2643
|
+
nbsp: ' ',
|
|
2644
|
+
};
|
|
2645
|
+
|
|
2646
|
+
return String(value || '').replace(/&(#x[0-9a-fA-F]+|#\d+|[a-zA-Z][a-zA-Z0-9]+);/g, (match, entity) => {
|
|
2647
|
+
if (entity.startsWith('#x')) {
|
|
2648
|
+
const codePoint = Number.parseInt(entity.slice(2), 16);
|
|
2649
|
+
return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : match;
|
|
2650
|
+
}
|
|
2651
|
+
if (entity.startsWith('#')) {
|
|
2652
|
+
const codePoint = Number.parseInt(entity.slice(1), 10);
|
|
2653
|
+
return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : match;
|
|
2654
|
+
}
|
|
2655
|
+
return Object.prototype.hasOwnProperty.call(namedEntities, entity) ? namedEntities[entity] : match;
|
|
2656
|
+
});
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
function normalizeSearchText(value) {
|
|
2660
|
+
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
function buildSearchAdapterJs() {
|
|
2664
|
+
const fieldWeightsJson = JSON.stringify(SEARCH_FIELD_WEIGHTS, null, 2);
|
|
2665
|
+
return `const FIELD_WEIGHTS = ${fieldWeightsJson};
|
|
2666
|
+
const FIELD_NAMES = Object.keys(FIELD_WEIGHTS);
|
|
2667
|
+
const RECENCY_BOOST_MAX = ${SEARCH_RECENCY_BOOST_MAX};
|
|
2668
|
+
const DEFAULT_LIMIT = 20;
|
|
2669
|
+
const BM25_K1 = 1.2;
|
|
2670
|
+
const BM25_B = 0.75;
|
|
2671
|
+
|
|
2672
|
+
let preparedIndexPromise;
|
|
2673
|
+
|
|
2674
|
+
export async function preload() {
|
|
2675
|
+
await loadPreparedIndex();
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
export async function search(query, options = {}) {
|
|
2679
|
+
const prepared = await loadPreparedIndex();
|
|
2680
|
+
const terms = tokenize(query);
|
|
2681
|
+
const phrase = normalizeText(query);
|
|
2682
|
+
if (terms.length === 0 && !phrase) {
|
|
2683
|
+
return { results: [] };
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
const hits = [];
|
|
2687
|
+
for (const document of prepared.documents) {
|
|
2688
|
+
const score = scoreDocument(document, terms, phrase, prepared);
|
|
2689
|
+
if (score > 0) {
|
|
2690
|
+
hits.push({ document, score });
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
const limit = normalizeLimit(options.limit);
|
|
2695
|
+
hits.sort((left, right) => right.score - left.score || left.document.raw.title.localeCompare(right.document.raw.title));
|
|
2696
|
+
|
|
2697
|
+
return {
|
|
2698
|
+
results: hits.slice(0, limit).map((hit) => ({
|
|
2699
|
+
id: hit.document.raw.id,
|
|
2700
|
+
score: Number(hit.score.toFixed(6)),
|
|
2701
|
+
data: async () => buildResultData(hit.document.raw, query),
|
|
2702
|
+
})),
|
|
2703
|
+
};
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
async function loadPreparedIndex() {
|
|
2707
|
+
if (!preparedIndexPromise) {
|
|
2708
|
+
preparedIndexPromise = fetch(new URL('./search.json', import.meta.url))
|
|
2709
|
+
.then((response) => {
|
|
2710
|
+
if (!response.ok) {
|
|
2711
|
+
throw new Error('ZeroPress search index not found');
|
|
2712
|
+
}
|
|
2713
|
+
return response.json();
|
|
2714
|
+
})
|
|
2715
|
+
.then((items) => prepareIndex(Array.isArray(items) ? items : []));
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
return preparedIndexPromise;
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
function prepareIndex(items) {
|
|
2722
|
+
const documents = items.map((item) => prepareDocument(item));
|
|
2723
|
+
const documentFrequencies = new Map();
|
|
2724
|
+
const averageLengths = Object.fromEntries(FIELD_NAMES.map((fieldName) => [fieldName, 1]));
|
|
2725
|
+
const newestPostTime = documents.reduce((newest, document) => {
|
|
2726
|
+
if (document.raw.type !== 'post') {
|
|
2727
|
+
return newest;
|
|
2728
|
+
}
|
|
2729
|
+
return Math.max(newest, document.publishedTime || 0);
|
|
2730
|
+
}, 0);
|
|
2731
|
+
|
|
2732
|
+
for (const document of documents) {
|
|
2733
|
+
const seenTerms = new Set();
|
|
2734
|
+
for (const fieldName of FIELD_NAMES) {
|
|
2735
|
+
for (const term of document.fieldTokens[fieldName]) {
|
|
2736
|
+
seenTerms.add(term);
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
for (const term of seenTerms) {
|
|
2740
|
+
documentFrequencies.set(term, (documentFrequencies.get(term) || 0) + 1);
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
for (const fieldName of FIELD_NAMES) {
|
|
2745
|
+
const total = documents.reduce((sum, document) => sum + document.fieldLengths[fieldName], 0);
|
|
2746
|
+
averageLengths[fieldName] = documents.length > 0 ? Math.max(1, total / documents.length) : 1;
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
return {
|
|
2750
|
+
documents,
|
|
2751
|
+
documentFrequencies,
|
|
2752
|
+
averageLengths,
|
|
2753
|
+
documentCount: documents.length,
|
|
2754
|
+
newestPostTime,
|
|
2755
|
+
};
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
function prepareDocument(item) {
|
|
2759
|
+
const raw = normalizeItem(item);
|
|
2760
|
+
const fields = {
|
|
2761
|
+
title: raw.title,
|
|
2762
|
+
headings: raw.headings.join(' '),
|
|
2763
|
+
tags: raw.tags.join(' '),
|
|
2764
|
+
categories: raw.categories.join(' '),
|
|
2765
|
+
excerpt: raw.excerpt,
|
|
2766
|
+
content_text: raw.content_text,
|
|
2767
|
+
};
|
|
2768
|
+
const fieldTexts = {};
|
|
2769
|
+
const fieldTokens = {};
|
|
2770
|
+
const fieldTermCounts = {};
|
|
2771
|
+
const fieldLengths = {};
|
|
2772
|
+
|
|
2773
|
+
for (const [fieldName, value] of Object.entries(fields)) {
|
|
2774
|
+
const normalizedText = normalizeText(value);
|
|
2775
|
+
const tokens = tokenize(normalizedText);
|
|
2776
|
+
fieldTexts[fieldName] = normalizedText;
|
|
2777
|
+
fieldTokens[fieldName] = tokens;
|
|
2778
|
+
fieldTermCounts[fieldName] = countTerms(tokens);
|
|
2779
|
+
fieldLengths[fieldName] = Math.max(1, tokens.length);
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
return {
|
|
2783
|
+
raw,
|
|
2784
|
+
fieldTexts,
|
|
2785
|
+
fieldTokens,
|
|
2786
|
+
fieldTermCounts,
|
|
2787
|
+
fieldLengths,
|
|
2788
|
+
publishedTime: Date.parse(raw.published_at_iso) || 0,
|
|
2789
|
+
};
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
function normalizeItem(item) {
|
|
2793
|
+
return {
|
|
2794
|
+
id: String(item && item.id || ''),
|
|
2795
|
+
type: item && item.type === 'page' ? 'page' : 'post',
|
|
2796
|
+
title: String(item && item.title || ''),
|
|
2797
|
+
url: String(item && item.url || ''),
|
|
2798
|
+
excerpt: String(item && item.excerpt || ''),
|
|
2799
|
+
headings: Array.isArray(item && item.headings) ? item.headings.map(String) : [],
|
|
2800
|
+
categories: Array.isArray(item && item.categories) ? item.categories.map(String) : [],
|
|
2801
|
+
tags: Array.isArray(item && item.tags) ? item.tags.map(String) : [],
|
|
2802
|
+
published_at_iso: String(item && item.published_at_iso || ''),
|
|
2803
|
+
updated_at_iso: String(item && item.updated_at_iso || ''),
|
|
2804
|
+
content_text: String(item && item.content_text || ''),
|
|
2805
|
+
};
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
function scoreDocument(document, terms, phrase, prepared) {
|
|
2809
|
+
let score = 0;
|
|
2810
|
+
const uniqueTerms = Array.from(new Set(terms));
|
|
2811
|
+
|
|
2812
|
+
for (const term of uniqueTerms) {
|
|
2813
|
+
const documentFrequency = prepared.documentFrequencies.get(term) || 0;
|
|
2814
|
+
const idf = Math.log(1 + (prepared.documentCount - documentFrequency + 0.5) / (documentFrequency + 0.5));
|
|
2815
|
+
|
|
2816
|
+
for (const fieldName of FIELD_NAMES) {
|
|
2817
|
+
const termFrequency = document.fieldTermCounts[fieldName].get(term) || 0;
|
|
2818
|
+
if (termFrequency === 0) {
|
|
2819
|
+
continue;
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
const fieldLength = document.fieldLengths[fieldName];
|
|
2823
|
+
const averageLength = prepared.averageLengths[fieldName];
|
|
2824
|
+
const denominator = termFrequency + BM25_K1 * (1 - BM25_B + BM25_B * (fieldLength / averageLength));
|
|
2825
|
+
score += FIELD_WEIGHTS[fieldName] * idf * ((termFrequency * (BM25_K1 + 1)) / denominator);
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
if (phrase && phrase.length > 1) {
|
|
2830
|
+
for (const fieldName of FIELD_NAMES) {
|
|
2831
|
+
if (document.fieldTexts[fieldName].includes(phrase)) {
|
|
2832
|
+
score += FIELD_WEIGHTS[fieldName] * 0.6;
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
if (score <= 0) {
|
|
2838
|
+
return 0;
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
return score * (1 + recencyBoost(document, prepared.newestPostTime));
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
function recencyBoost(document, newestPostTime) {
|
|
2845
|
+
if (document.raw.type !== 'post' || !document.publishedTime || !newestPostTime) {
|
|
2846
|
+
return 0;
|
|
2847
|
+
}
|
|
2848
|
+
const ageMs = Math.max(0, newestPostTime - document.publishedTime);
|
|
2849
|
+
const halfLifeMs = 180 * 24 * 60 * 60 * 1000;
|
|
2850
|
+
return RECENCY_BOOST_MAX * Math.exp(-ageMs / halfLifeMs);
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
function buildResultData(item, query) {
|
|
2854
|
+
const excerpt = buildExcerpt(item, query);
|
|
2855
|
+
return {
|
|
2856
|
+
url: item.url,
|
|
2857
|
+
excerpt,
|
|
2858
|
+
plain_excerpt: excerpt,
|
|
2859
|
+
meta: {
|
|
2860
|
+
title: item.title,
|
|
2861
|
+
type: item.type,
|
|
2862
|
+
published_at_iso: item.published_at_iso,
|
|
2863
|
+
updated_at_iso: item.updated_at_iso,
|
|
2864
|
+
categories: item.categories,
|
|
2865
|
+
tags: item.tags,
|
|
2866
|
+
},
|
|
2867
|
+
sub_results: [],
|
|
2868
|
+
};
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
function buildExcerpt(item, query) {
|
|
2872
|
+
const explicitExcerpt = String(item.excerpt || '').trim();
|
|
2873
|
+
if (explicitExcerpt) {
|
|
2874
|
+
return explicitExcerpt;
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
const text = String(item.content_text || '').replace(/\\s+/g, ' ').trim();
|
|
2878
|
+
if (!text) {
|
|
2879
|
+
return '';
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
const normalizedText = normalizeText(text);
|
|
2883
|
+
const terms = tokenize(query);
|
|
2884
|
+
const firstMatch = terms.map((term) => normalizedText.indexOf(term)).filter((index) => index >= 0).sort((a, b) => a - b)[0];
|
|
2885
|
+
const start = Math.max(0, (firstMatch || 0) - 80);
|
|
2886
|
+
const end = Math.min(text.length, start + 180);
|
|
2887
|
+
const prefix = start > 0 ? '...' : '';
|
|
2888
|
+
const suffix = end < text.length ? '...' : '';
|
|
2889
|
+
return prefix + text.slice(start, end).trim() + suffix;
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
function countTerms(tokens) {
|
|
2893
|
+
const counts = new Map();
|
|
2894
|
+
for (const token of tokens) {
|
|
2895
|
+
counts.set(token, (counts.get(token) || 0) + 1);
|
|
2896
|
+
}
|
|
2897
|
+
return counts;
|
|
2898
|
+
}
|
|
2899
|
+
|
|
2900
|
+
function tokenize(value) {
|
|
2901
|
+
const text = normalizeText(value);
|
|
2902
|
+
if (!text) {
|
|
2903
|
+
return [];
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
const tokens = [];
|
|
2907
|
+
if (typeof Intl !== 'undefined' && typeof Intl.Segmenter === 'function') {
|
|
2908
|
+
try {
|
|
2909
|
+
const segmenter = new Intl.Segmenter(undefined, { granularity: 'word' });
|
|
2910
|
+
for (const part of segmenter.segment(text)) {
|
|
2911
|
+
if (part.isWordLike && isUsefulToken(part.segment)) {
|
|
2912
|
+
tokens.push(part.segment);
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
} catch {
|
|
2916
|
+
// Fall through to regex tokenization.
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
for (const match of text.matchAll(/[\\p{Letter}\\p{Number}]+/gu)) {
|
|
2921
|
+
if (isUsefulToken(match[0])) {
|
|
2922
|
+
tokens.push(match[0]);
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
for (const match of text.matchAll(/[\\p{Script=Han}\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Hangul}]+/gu)) {
|
|
2927
|
+
tokens.push(...buildNgrams(match[0], 2));
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
return tokens;
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
function buildNgrams(value, size) {
|
|
2934
|
+
const normalized = Array.from(value);
|
|
2935
|
+
if (normalized.length <= size) {
|
|
2936
|
+
return isUsefulToken(value) ? [value] : [];
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
const tokens = [];
|
|
2940
|
+
for (let index = 0; index <= normalized.length - size; index += 1) {
|
|
2941
|
+
tokens.push(normalized.slice(index, index + size).join(''));
|
|
2942
|
+
}
|
|
2943
|
+
tokens.push(value);
|
|
2944
|
+
return tokens;
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
function isUsefulToken(value) {
|
|
2948
|
+
const token = String(value || '').trim();
|
|
2949
|
+
return token.length > 1 || /^\\d$/.test(token);
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2952
|
+
function normalizeText(value) {
|
|
2953
|
+
return String(value || '')
|
|
2954
|
+
.normalize('NFKC')
|
|
2955
|
+
.toLowerCase()
|
|
2956
|
+
.replace(/[\\u2018\\u2019]/g, "'")
|
|
2957
|
+
.replace(/[_-]+/g, ' ')
|
|
2958
|
+
.replace(/\\s+/g, ' ')
|
|
2959
|
+
.trim();
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
function normalizeLimit(value) {
|
|
2963
|
+
return Number.isInteger(value) && value > 0 ? Math.min(value, 100) : DEFAULT_LIMIT;
|
|
2964
|
+
}
|
|
2965
|
+
`;
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
function buildSitemapXml(site, emitted, generatedAt, stylesheetHref = '') {
|
|
2111
2969
|
const entries = [
|
|
2112
|
-
...(emitted.frontPage
|
|
2970
|
+
...(emitted.frontPage && emitted.frontPage.includeInSitemap !== false
|
|
2113
2971
|
? [{
|
|
2114
2972
|
url: emitted.frontPage.url,
|
|
2115
2973
|
changefreq: 'daily',
|
|
@@ -2146,7 +3004,12 @@ function buildSitemapXml(site, emitted, generatedAt) {
|
|
|
2146
3004
|
return ` <url>\n <loc>${loc}</loc>${lastmod}\n <changefreq>${entry.changefreq}</changefreq>\n <priority>${entry.priority.toFixed(1)}</priority>\n </url>`;
|
|
2147
3005
|
}).join('\n');
|
|
2148
3006
|
|
|
2149
|
-
|
|
3007
|
+
const normalizedStylesheetHref = normalizeOptionalString(stylesheetHref);
|
|
3008
|
+
const stylesheet = normalizedStylesheetHref
|
|
3009
|
+
? `\n<?xml-stylesheet type="text/xsl" href="${escapeXml(normalizedStylesheetHref)}"?>`
|
|
3010
|
+
: '';
|
|
3011
|
+
|
|
3012
|
+
return `<?xml version="1.0" encoding="UTF-8"?>${stylesheet}\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${body}\n</urlset>`;
|
|
2150
3013
|
}
|
|
2151
3014
|
|
|
2152
3015
|
function buildFeedXml(site, emitted, generatedAt) {
|
|
@@ -2165,13 +3028,23 @@ function buildFeedXml(site, emitted, generatedAt) {
|
|
|
2165
3028
|
}
|
|
2166
3029
|
|
|
2167
3030
|
function buildRobotsTxt(site) {
|
|
2168
|
-
const lines = ['User-agent: *'
|
|
3031
|
+
const lines = ['User-agent: *'];
|
|
3032
|
+
if (site.indexing === false) {
|
|
3033
|
+
lines.push('Disallow: /');
|
|
3034
|
+
return `${lines.join('\n')}\n`;
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
lines.push('Allow: /');
|
|
2169
3038
|
if (site.url) {
|
|
2170
3039
|
lines.push('', `Sitemap: ${resolveSiteUrl(site.url, '/sitemap.xml')}`);
|
|
2171
3040
|
}
|
|
2172
3041
|
return `${lines.join('\n')}\n`;
|
|
2173
3042
|
}
|
|
2174
3043
|
|
|
3044
|
+
function shouldGenerateRobotsTxt(options) {
|
|
3045
|
+
return options.generateSpecialFiles && options.generateRobotsTxt !== false;
|
|
3046
|
+
}
|
|
3047
|
+
|
|
2175
3048
|
function getContentType(assetPath) {
|
|
2176
3049
|
const ext = assetPath.split('.').pop()?.toLowerCase();
|
|
2177
3050
|
const contentTypes = {
|