@zeropress/build-core 0.5.0 → 0.5.2
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 +8 -2
- package/package.json +4 -4
- package/src/api/build-site.js +654 -47
- package/src/assets/asset-processor.js +1 -6
- package/src/index.d.ts +7 -1
- package/src/render/content-renderer.js +201 -3
- package/src/theme/load-theme-dir.js +0 -1
package/src/api/build-site.js
CHANGED
|
@@ -9,6 +9,7 @@ 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
|
|
|
@@ -24,16 +25,31 @@ const DEFAULT_PERMALINKS = Object.freeze({
|
|
|
24
25
|
categories: '/categories/:slug/',
|
|
25
26
|
tags: '/tags/:slug/',
|
|
26
27
|
});
|
|
28
|
+
const DEFAULT_FRONT_PAGE = Object.freeze({
|
|
29
|
+
type: 'theme_index',
|
|
30
|
+
});
|
|
31
|
+
const DEFAULT_POST_INDEX = Object.freeze({
|
|
32
|
+
enabled: true,
|
|
33
|
+
path: '/',
|
|
34
|
+
paginate: true,
|
|
35
|
+
});
|
|
27
36
|
const PERMALINK_OUTPUT_STYLES = new Set(['directory', 'html-extension']);
|
|
28
37
|
const COMMENT_POLICY_OUTPUT_PATH = '_zeropress/comment-policy.json';
|
|
29
38
|
const OUTPUT_PATH_CONTROL_CHAR_PATTERN = /[\u0000-\u001F\u007F]/;
|
|
30
39
|
const SAFE_MEDIA_PROTOCOLS = new Set(['http:', 'https:']);
|
|
31
40
|
const SAFE_LINK_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:']);
|
|
41
|
+
const MEDIA_DELIVERY_MODES = new Set(['none', 'media_domain']);
|
|
42
|
+
const RESPONSIVE_IMAGE_WIDTHS = [320, 480, 768, 1024, 1280, 1600, 1920];
|
|
43
|
+
const RESPONSIVE_IMAGE_EXTENSIONS = new Set(['jpg', 'jpeg', 'png', 'webp', 'avif']);
|
|
32
44
|
export async function buildSite(input) {
|
|
33
45
|
const options = { ...DEFAULT_OPTIONS, ...(input.options || {}) };
|
|
34
46
|
const state = await createBuildState(input, options);
|
|
35
47
|
assertPlannedOutputPathsSafe(state);
|
|
36
48
|
|
|
49
|
+
if (state.renderData.frontPageRoute) {
|
|
50
|
+
await renderFrontPage(state, state.renderData.frontPageRoute);
|
|
51
|
+
}
|
|
52
|
+
|
|
37
53
|
for (const route of state.renderData.indexRoutes) {
|
|
38
54
|
await renderRoute(state, 'index', route);
|
|
39
55
|
}
|
|
@@ -82,7 +98,9 @@ export async function buildSite(input) {
|
|
|
82
98
|
await writeOutput(state.writer, state.summaries, 'sitemap.xml', buildSitemapXml(state.previewData.site, state.emitted, state.generatedAt), 'application/xml');
|
|
83
99
|
await writeOutput(state.writer, state.summaries, 'feed.xml', buildFeedXml(state.previewData.site, state.emitted, state.generatedAt), 'application/rss+xml');
|
|
84
100
|
}
|
|
85
|
-
|
|
101
|
+
if (shouldGenerateRobotsTxt(options)) {
|
|
102
|
+
await writeOutput(state.writer, state.summaries, 'robots.txt', buildRobotsTxt(state.previewData.site), 'text/plain');
|
|
103
|
+
}
|
|
86
104
|
}
|
|
87
105
|
|
|
88
106
|
return finalizeBuildResult(state.writer, state.summaries, options);
|
|
@@ -99,7 +117,7 @@ async function createBuildState(input, options) {
|
|
|
99
117
|
const engine = new ZeroPressEngine();
|
|
100
118
|
const assetProcessor = new AssetProcessor();
|
|
101
119
|
const summaries = [];
|
|
102
|
-
const previewData = normalizePreviewData(input.previewData);
|
|
120
|
+
const previewData = normalizePreviewData(input.previewData, options);
|
|
103
121
|
const renderData = createRenderData(previewData, themePackage.metadata);
|
|
104
122
|
|
|
105
123
|
engine.initialize(themePackage);
|
|
@@ -125,10 +143,12 @@ async function createBuildState(input, options) {
|
|
|
125
143
|
assetMap,
|
|
126
144
|
customCssHref: customCssAsset ? `/${customCssAsset.path}` : '',
|
|
127
145
|
customHtml: previewData.custom_html,
|
|
146
|
+
favicon: previewData.site.favicon,
|
|
128
147
|
commentPolicyContent: buildCommentPolicyManifest(renderData.posts),
|
|
129
148
|
options,
|
|
130
149
|
generatedAt: new Date(),
|
|
131
150
|
emitted: {
|
|
151
|
+
frontPage: null,
|
|
132
152
|
indexRoutes: [],
|
|
133
153
|
archiveRoutes: [],
|
|
134
154
|
categoryRoutes: [],
|
|
@@ -175,13 +195,19 @@ async function finalizeBuildResult(writer, summaries, options) {
|
|
|
175
195
|
|
|
176
196
|
async function renderRoute(state, templateName, route) {
|
|
177
197
|
const currentUrl = routePathToPublicUrl(route.path, state.previewData.site.permalinks.output_style);
|
|
198
|
+
const routeContext = buildRouteContext(route.route_type || templateName, currentUrl, {
|
|
199
|
+
isFrontPage: route.is_front_page === true,
|
|
200
|
+
isPostIndex: route.is_post_index === true,
|
|
201
|
+
});
|
|
178
202
|
let html = await state.engine.render(
|
|
179
203
|
templateName,
|
|
180
204
|
{
|
|
181
205
|
menus: state.previewData.menus,
|
|
182
206
|
widgets: state.widgets,
|
|
207
|
+
collections: state.renderData.collections,
|
|
183
208
|
taxonomies: state.renderData.taxonomies,
|
|
184
209
|
...route,
|
|
210
|
+
route: routeContext,
|
|
185
211
|
meta: buildPageMeta(state.previewData.site, {
|
|
186
212
|
currentUrl,
|
|
187
213
|
title: state.previewData.site.title,
|
|
@@ -197,6 +223,89 @@ async function renderRoute(state, templateName, route) {
|
|
|
197
223
|
recordRouteEmission(state, templateName, route, currentUrl);
|
|
198
224
|
}
|
|
199
225
|
|
|
226
|
+
async function renderFrontPage(state, route) {
|
|
227
|
+
const currentUrl = '/';
|
|
228
|
+
const routeContext = buildRouteContext('front_page', currentUrl, {
|
|
229
|
+
isFrontPage: true,
|
|
230
|
+
isPostIndex: false,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
if (route.front_page_type === 'standalone_html') {
|
|
234
|
+
await writeOutput(state.writer, state.summaries, 'index.html', route.html, 'text/html');
|
|
235
|
+
state.emitted.frontPage = {
|
|
236
|
+
url: currentUrl,
|
|
237
|
+
title: state.previewData.site.title,
|
|
238
|
+
description: state.previewData.site.description,
|
|
239
|
+
includeInFeed: false,
|
|
240
|
+
};
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (route.front_page_type === 'page') {
|
|
245
|
+
const page = {
|
|
246
|
+
...route.page,
|
|
247
|
+
url: currentUrl,
|
|
248
|
+
};
|
|
249
|
+
let html = await state.engine.render(
|
|
250
|
+
'page',
|
|
251
|
+
{
|
|
252
|
+
menus: state.previewData.menus,
|
|
253
|
+
widgets: state.widgets,
|
|
254
|
+
collections: state.renderData.collections,
|
|
255
|
+
taxonomies: state.renderData.taxonomies,
|
|
256
|
+
page,
|
|
257
|
+
route: routeContext,
|
|
258
|
+
meta: buildPageMeta(state.previewData.site, {
|
|
259
|
+
currentUrl,
|
|
260
|
+
title: buildDocumentTitle(page.title, state.previewData.site.title),
|
|
261
|
+
description: page.excerpt,
|
|
262
|
+
ogType: 'website',
|
|
263
|
+
image: page.featured_image,
|
|
264
|
+
}),
|
|
265
|
+
},
|
|
266
|
+
createRenderContext(state.previewData.site, currentUrl),
|
|
267
|
+
);
|
|
268
|
+
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
269
|
+
html = injectSiteCustomizations(html, state);
|
|
270
|
+
await writeOutput(state.writer, state.summaries, 'index.html', html, 'text/html');
|
|
271
|
+
state.emitted.frontPage = {
|
|
272
|
+
url: currentUrl,
|
|
273
|
+
title: page.title,
|
|
274
|
+
description: page.excerpt,
|
|
275
|
+
includeInFeed: false,
|
|
276
|
+
};
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
let html = await state.engine.render(
|
|
281
|
+
'index',
|
|
282
|
+
{
|
|
283
|
+
menus: state.previewData.menus,
|
|
284
|
+
widgets: state.widgets,
|
|
285
|
+
collections: state.renderData.collections,
|
|
286
|
+
taxonomies: state.renderData.taxonomies,
|
|
287
|
+
...route,
|
|
288
|
+
route: routeContext,
|
|
289
|
+
meta: buildPageMeta(state.previewData.site, {
|
|
290
|
+
currentUrl,
|
|
291
|
+
title: state.previewData.site.title,
|
|
292
|
+
description: state.previewData.site.description,
|
|
293
|
+
ogType: 'website',
|
|
294
|
+
}),
|
|
295
|
+
},
|
|
296
|
+
createRenderContext(state.previewData.site, currentUrl),
|
|
297
|
+
);
|
|
298
|
+
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
299
|
+
html = injectSiteCustomizations(html, state);
|
|
300
|
+
await writeOutput(state.writer, state.summaries, 'index.html', html, 'text/html');
|
|
301
|
+
state.emitted.frontPage = {
|
|
302
|
+
url: currentUrl,
|
|
303
|
+
title: state.previewData.site.title,
|
|
304
|
+
description: state.previewData.site.description,
|
|
305
|
+
includeInFeed: false,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
200
309
|
async function renderPost(state, post) {
|
|
201
310
|
const currentUrl = post.url;
|
|
202
311
|
let html = await state.engine.render(
|
|
@@ -204,8 +313,10 @@ async function renderPost(state, post) {
|
|
|
204
313
|
{
|
|
205
314
|
menus: state.previewData.menus,
|
|
206
315
|
widgets: state.widgets,
|
|
316
|
+
collections: state.renderData.collections,
|
|
207
317
|
taxonomies: state.renderData.taxonomies,
|
|
208
318
|
post,
|
|
319
|
+
route: buildRouteContext('post', currentUrl),
|
|
209
320
|
meta: buildPageMeta(state.previewData.site, {
|
|
210
321
|
currentUrl,
|
|
211
322
|
title: buildDocumentTitle(post.title, state.previewData.site.title),
|
|
@@ -233,15 +344,20 @@ async function renderPost(state, post) {
|
|
|
233
344
|
|
|
234
345
|
async function renderPage(state, page) {
|
|
235
346
|
const currentUrl = page.url;
|
|
347
|
+
const outputPath = pageToOutputPath(page, state.previewData.site.permalinks.output_style);
|
|
348
|
+
const canonicalUrl = normalizeOptionalString(page.canonical_url) || currentUrl;
|
|
236
349
|
let html = await state.engine.render(
|
|
237
350
|
'page',
|
|
238
351
|
{
|
|
239
352
|
menus: state.previewData.menus,
|
|
240
353
|
widgets: state.widgets,
|
|
354
|
+
collections: state.renderData.collections,
|
|
241
355
|
taxonomies: state.renderData.taxonomies,
|
|
242
356
|
page,
|
|
357
|
+
route: buildRouteContext('page', currentUrl),
|
|
243
358
|
meta: buildPageMeta(state.previewData.site, {
|
|
244
359
|
currentUrl,
|
|
360
|
+
canonicalUrl,
|
|
245
361
|
title: buildDocumentTitle(page.title, state.previewData.site.title),
|
|
246
362
|
description: page.excerpt,
|
|
247
363
|
ogType: 'website',
|
|
@@ -252,12 +368,13 @@ async function renderPage(state, page) {
|
|
|
252
368
|
);
|
|
253
369
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
254
370
|
html = injectSiteCustomizations(html, state);
|
|
255
|
-
await writeOutput(state.writer, state.summaries,
|
|
371
|
+
await writeOutput(state.writer, state.summaries, outputPath, html, 'text/html');
|
|
256
372
|
state.emitted.pages.push({
|
|
257
373
|
url: currentUrl,
|
|
258
374
|
title: page.title,
|
|
259
375
|
description: page.excerpt,
|
|
260
376
|
status: page.status,
|
|
377
|
+
includeInSitemap: page.omit_from_sitemap !== true,
|
|
261
378
|
});
|
|
262
379
|
}
|
|
263
380
|
|
|
@@ -271,7 +388,9 @@ async function maybeRenderNotFoundPage(state) {
|
|
|
271
388
|
{
|
|
272
389
|
menus: state.previewData.menus,
|
|
273
390
|
widgets: state.widgets,
|
|
391
|
+
collections: state.renderData.collections,
|
|
274
392
|
taxonomies: state.renderData.taxonomies,
|
|
393
|
+
route: buildRouteContext('not_found', '/404.html'),
|
|
275
394
|
meta: buildPageMeta(state.previewData.site, {
|
|
276
395
|
currentUrl: '/404.html',
|
|
277
396
|
title: state.previewData.site.title,
|
|
@@ -286,10 +405,14 @@ async function maybeRenderNotFoundPage(state) {
|
|
|
286
405
|
await writeOutput(state.writer, state.summaries, '404.html', html, 'text/html');
|
|
287
406
|
}
|
|
288
407
|
|
|
289
|
-
function normalizePreviewData(previewData) {
|
|
408
|
+
function normalizePreviewData(previewData, options = {}) {
|
|
290
409
|
const normalizedSite = {
|
|
291
410
|
...previewData.site,
|
|
292
411
|
mediaBaseUrl: normalizeOptionalString(previewData.site.mediaBaseUrl),
|
|
412
|
+
mediaDeliveryMode: MEDIA_DELIVERY_MODES.has(previewData.site.mediaDeliveryMode)
|
|
413
|
+
? previewData.site.mediaDeliveryMode
|
|
414
|
+
: 'none',
|
|
415
|
+
favicon: normalizeSiteFavicon(previewData.site.favicon || options.favicon),
|
|
293
416
|
postsPerPage: Number.isInteger(previewData.site.postsPerPage) && previewData.site.postsPerPage > 0
|
|
294
417
|
? previewData.site.postsPerPage
|
|
295
418
|
: DEFAULT_POSTS_PER_PAGE,
|
|
@@ -298,39 +421,195 @@ function normalizePreviewData(previewData) {
|
|
|
298
421
|
timezone: normalizeNonEmptyString(previewData.site.timezone, DEFAULT_TIMEZONE),
|
|
299
422
|
locale: normalizeLocale(previewData.site.locale || DEFAULT_LOCALE),
|
|
300
423
|
disallowComments: previewData.site.disallowComments === true,
|
|
424
|
+
indexing: previewData.site.indexing !== false,
|
|
301
425
|
permalinks: normalizePermalinks(previewData.site.permalinks),
|
|
426
|
+
front_page: normalizeFrontPage(previewData.site.front_page),
|
|
427
|
+
post_index: normalizePostIndex(previewData.site.post_index),
|
|
428
|
+
footer: normalizeSiteFooter(previewData.site.footer),
|
|
302
429
|
};
|
|
430
|
+
const media = normalizeContentMedia(previewData.content.media, normalizedSite);
|
|
431
|
+
const mediaRegistry = buildMediaRegistry(media);
|
|
303
432
|
|
|
304
433
|
return {
|
|
305
434
|
...previewData,
|
|
306
435
|
site: normalizedSite,
|
|
436
|
+
menus: normalizeRecordMap(previewData.menus),
|
|
437
|
+
collections: normalizeCollections(previewData.collections),
|
|
307
438
|
widgets: normalizeWidgetAreas(previewData.widgets, normalizedSite.mediaBaseUrl),
|
|
308
439
|
custom_css: normalizeCustomCss(previewData.custom_css),
|
|
309
440
|
custom_html: normalizeCustomHtml(previewData.custom_html),
|
|
310
441
|
content: {
|
|
311
442
|
...previewData.content,
|
|
312
|
-
authors: previewData.content.authors.map((author) =>
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
443
|
+
authors: previewData.content.authors.map((author) => {
|
|
444
|
+
const avatar = normalizeMediaField(author.avatar, normalizedSite.mediaBaseUrl);
|
|
445
|
+
const avatarMedia = deriveManagedMedia(avatar, mediaRegistry, normalizedSite);
|
|
446
|
+
return {
|
|
447
|
+
...author,
|
|
448
|
+
avatar,
|
|
449
|
+
...(avatarMedia ? { avatar_media: avatarMedia } : {}),
|
|
450
|
+
};
|
|
451
|
+
}),
|
|
316
452
|
posts: previewData.content.posts
|
|
317
|
-
.map((post) =>
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
453
|
+
.map((post) => {
|
|
454
|
+
const featuredImage = normalizeMediaField(post.featured_image, normalizedSite.mediaBaseUrl);
|
|
455
|
+
const featuredMedia = deriveManagedMedia(featuredImage, mediaRegistry, normalizedSite);
|
|
456
|
+
return {
|
|
457
|
+
...post,
|
|
458
|
+
published_at_iso: normalizeIsoTimestamp(post.published_at_iso),
|
|
459
|
+
updated_at_iso: normalizeIsoTimestamp(post.updated_at_iso),
|
|
460
|
+
featured_image: featuredImage,
|
|
461
|
+
...(featuredMedia ? { featured_media: featuredMedia } : {}),
|
|
462
|
+
};
|
|
463
|
+
})
|
|
323
464
|
.sort((left, right) => toDate(right.published_at_iso).getTime() - toDate(left.published_at_iso).getTime()),
|
|
324
|
-
pages: previewData.content.pages.map((page) =>
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
465
|
+
pages: previewData.content.pages.map((page) => {
|
|
466
|
+
const featuredImage = normalizeMediaField(page.featured_image, normalizedSite.mediaBaseUrl);
|
|
467
|
+
const featuredMedia = deriveManagedMedia(featuredImage, mediaRegistry, normalizedSite);
|
|
468
|
+
return {
|
|
469
|
+
...page,
|
|
470
|
+
featured_image: featuredImage,
|
|
471
|
+
...(featuredMedia ? { featured_media: featuredMedia } : {}),
|
|
472
|
+
};
|
|
473
|
+
}),
|
|
328
474
|
categories: [...previewData.content.categories],
|
|
329
475
|
tags: [...previewData.content.tags],
|
|
476
|
+
media,
|
|
330
477
|
},
|
|
331
478
|
};
|
|
332
479
|
}
|
|
333
480
|
|
|
481
|
+
function normalizeRecordMap(value) {
|
|
482
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
483
|
+
return {};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return { ...value };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function normalizeContentMedia(mediaItems, site) {
|
|
490
|
+
if (!Array.isArray(mediaItems)) {
|
|
491
|
+
return [];
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return mediaItems
|
|
495
|
+
.map((item) => {
|
|
496
|
+
if (!item || typeof item !== 'object') {
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
const src = normalizeMediaField(item.src, site.mediaBaseUrl);
|
|
500
|
+
const width = Number.isInteger(item.width) && item.width > 0 ? item.width : 0;
|
|
501
|
+
const height = Number.isInteger(item.height) && item.height > 0 ? item.height : 0;
|
|
502
|
+
if (!src || !width || !height) {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
return {
|
|
506
|
+
src,
|
|
507
|
+
width,
|
|
508
|
+
height,
|
|
509
|
+
alt: typeof item.alt === 'string' ? item.alt : '',
|
|
510
|
+
};
|
|
511
|
+
})
|
|
512
|
+
.filter(Boolean);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function buildMediaRegistry(mediaItems) {
|
|
516
|
+
const registry = new Map();
|
|
517
|
+
for (const item of mediaItems) {
|
|
518
|
+
if (!registry.has(item.src)) {
|
|
519
|
+
registry.set(item.src, item);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return registry;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function deriveManagedMedia(src, mediaRegistry, site) {
|
|
526
|
+
const normalizedSrc = normalizeOptionalString(src);
|
|
527
|
+
if (!normalizedSrc) {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
const media = mediaRegistry.get(normalizedSrc);
|
|
531
|
+
if (!media) {
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
...media,
|
|
536
|
+
srcset: buildResponsiveImageSrcset(media, site),
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function buildResponsiveImageSrcset(media, site) {
|
|
541
|
+
if (site.mediaDeliveryMode !== 'media_domain') {
|
|
542
|
+
return '';
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const mediaBaseUrl = normalizeOptionalString(site.mediaBaseUrl);
|
|
546
|
+
if (!mediaBaseUrl || !isUrlUnderMediaBase(media.src, mediaBaseUrl) || !isResponsiveRasterImage(media.src)) {
|
|
547
|
+
return '';
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const widths = RESPONSIVE_IMAGE_WIDTHS.filter((width) => width <= media.width);
|
|
551
|
+
if (!widths.includes(media.width)) {
|
|
552
|
+
widths.push(media.width);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return widths
|
|
556
|
+
.filter((width, index, values) => width > 0 && values.indexOf(width) === index)
|
|
557
|
+
.map((width) => `${buildResponsiveImageVariantUrl(media.src, width)} ${width}w`)
|
|
558
|
+
.join(', ');
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function buildResponsiveImageVariantUrl(src, width) {
|
|
562
|
+
try {
|
|
563
|
+
const url = new URL(src);
|
|
564
|
+
url.searchParams.set('w', String(width));
|
|
565
|
+
url.searchParams.set('fit', 'scale-down');
|
|
566
|
+
url.searchParams.set('format', 'auto');
|
|
567
|
+
return decodeURI(url.toString());
|
|
568
|
+
} catch {
|
|
569
|
+
return src;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function isUrlUnderMediaBase(src, mediaBaseUrl) {
|
|
574
|
+
try {
|
|
575
|
+
const sourceUrl = new URL(src);
|
|
576
|
+
const baseUrl = new URL(mediaBaseUrl);
|
|
577
|
+
const basePath = baseUrl.pathname.endsWith('/') ? baseUrl.pathname : `${baseUrl.pathname}/`;
|
|
578
|
+
return sourceUrl.origin === baseUrl.origin && sourceUrl.pathname.startsWith(basePath);
|
|
579
|
+
} catch {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function isResponsiveRasterImage(src) {
|
|
585
|
+
try {
|
|
586
|
+
const url = new URL(src);
|
|
587
|
+
const lastSegment = url.pathname.split('/').pop() || '';
|
|
588
|
+
const extension = lastSegment.includes('.') ? lastSegment.split('.').pop().toLowerCase() : '';
|
|
589
|
+
return RESPONSIVE_IMAGE_EXTENSIONS.has(extension);
|
|
590
|
+
} catch {
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function normalizeCollections(collections) {
|
|
596
|
+
if (!collections || typeof collections !== 'object' || Array.isArray(collections)) {
|
|
597
|
+
return {};
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return Object.fromEntries(
|
|
601
|
+
Object.entries(collections).map(([collectionId, collection]) => [
|
|
602
|
+
collectionId,
|
|
603
|
+
{
|
|
604
|
+
...collection,
|
|
605
|
+
title: normalizeOptionalString(collection?.title),
|
|
606
|
+
description: normalizeOptionalString(collection?.description),
|
|
607
|
+
items: Array.isArray(collection?.items) ? collection.items.map((item) => ({ ...item })) : [],
|
|
608
|
+
},
|
|
609
|
+
]),
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
|
|
334
613
|
function normalizeWidgetAreas(widgetAreas, mediaBaseUrl) {
|
|
335
614
|
if (!widgetAreas || typeof widgetAreas !== 'object') {
|
|
336
615
|
return {};
|
|
@@ -350,6 +629,22 @@ function normalizeWidgetAreas(widgetAreas, mediaBaseUrl) {
|
|
|
350
629
|
);
|
|
351
630
|
}
|
|
352
631
|
|
|
632
|
+
function normalizeSiteFooter(footer) {
|
|
633
|
+
const source = footer && typeof footer === 'object' && !Array.isArray(footer) ? footer : {};
|
|
634
|
+
const attribution = source.attribution && typeof source.attribution === 'object' && !Array.isArray(source.attribution)
|
|
635
|
+
? source.attribution
|
|
636
|
+
: {};
|
|
637
|
+
|
|
638
|
+
return {
|
|
639
|
+
...source,
|
|
640
|
+
copyright_text: normalizeOptionalString(source.copyright_text),
|
|
641
|
+
attribution: {
|
|
642
|
+
...attribution,
|
|
643
|
+
enabled: attribution.enabled !== false,
|
|
644
|
+
},
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
353
648
|
function normalizeWidgetItem(item, mediaBaseUrl) {
|
|
354
649
|
const normalizedItem = {
|
|
355
650
|
...item,
|
|
@@ -391,6 +686,22 @@ function normalizeCustomHtml(customHtml) {
|
|
|
391
686
|
};
|
|
392
687
|
}
|
|
393
688
|
|
|
689
|
+
function normalizeSiteFavicon(favicon) {
|
|
690
|
+
if (!favicon || typeof favicon !== 'object') {
|
|
691
|
+
return undefined;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const normalized = {};
|
|
695
|
+
for (const key of ['icon', 'svg', 'png', 'apple_touch_icon']) {
|
|
696
|
+
const value = normalizeOptionalString(favicon[key]);
|
|
697
|
+
if (value) {
|
|
698
|
+
normalized[key] = value;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
return Object.keys(normalized).length ? normalized : undefined;
|
|
703
|
+
}
|
|
704
|
+
|
|
394
705
|
function normalizePermalinks(permalinks) {
|
|
395
706
|
const source = permalinks && typeof permalinks === 'object' ? permalinks : {};
|
|
396
707
|
const outputStyle = typeof source.output_style === 'string' && PERMALINK_OUTPUT_STYLES.has(source.output_style)
|
|
@@ -406,8 +717,37 @@ function normalizePermalinks(permalinks) {
|
|
|
406
717
|
};
|
|
407
718
|
}
|
|
408
719
|
|
|
720
|
+
function normalizeFrontPage(frontPage) {
|
|
721
|
+
if (!frontPage || typeof frontPage !== 'object') {
|
|
722
|
+
return { ...DEFAULT_FRONT_PAGE };
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const type = ['theme_index', 'page', 'standalone_html'].includes(frontPage.type)
|
|
726
|
+
? frontPage.type
|
|
727
|
+
: DEFAULT_FRONT_PAGE.type;
|
|
728
|
+
|
|
729
|
+
return {
|
|
730
|
+
type,
|
|
731
|
+
...(type === 'page' ? { page_slug: normalizeOptionalString(frontPage.page_slug) } : {}),
|
|
732
|
+
...(type === 'standalone_html' ? { html: normalizeOptionalRawString(frontPage.html) } : {}),
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function normalizePostIndex(postIndex) {
|
|
737
|
+
if (!postIndex || typeof postIndex !== 'object') {
|
|
738
|
+
return { ...DEFAULT_POST_INDEX };
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
return {
|
|
742
|
+
enabled: postIndex.enabled !== false,
|
|
743
|
+
path: normalizeNonEmptyString(postIndex.path, DEFAULT_POST_INDEX.path),
|
|
744
|
+
paginate: postIndex.paginate !== false,
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
|
|
409
748
|
function createRenderData(previewData, themeMetadata = {}) {
|
|
410
749
|
const themeSupportsComments = themeMetadata?.features?.comments === true;
|
|
750
|
+
const themeSupportsPostIndex = themeMetadata?.features?.postIndex !== false;
|
|
411
751
|
const authorsById = new Map(previewData.content.authors.map((author) => [author.id, author]));
|
|
412
752
|
const categoriesBySlug = new Map(previewData.content.categories.map((category) => [category.slug, category]));
|
|
413
753
|
const tagsBySlug = new Map(previewData.content.tags.map((tag) => [tag.slug, tag]));
|
|
@@ -436,24 +776,40 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
436
776
|
}));
|
|
437
777
|
const pages = previewData.content.pages.map((page) => preparePage(page, previewData.site));
|
|
438
778
|
const postBySlug = new Map(posts.map((post) => [post.slug, post]));
|
|
779
|
+
const pageBySlug = new Map(pages.map((page) => [page.slug, page]));
|
|
780
|
+
const frontPage = previewData.site.front_page;
|
|
781
|
+
const postIndex = previewData.site.post_index;
|
|
782
|
+
const effectivePostIndexEnabled = postIndex.enabled !== false && themeSupportsPostIndex;
|
|
783
|
+
const effectivePostIndexPaginate = effectivePostIndexEnabled && postIndex.paginate !== false;
|
|
784
|
+
const postIndexBasePath = normalizeRoutePath(postIndex.path || DEFAULT_POST_INDEX.path);
|
|
785
|
+
|
|
786
|
+
if (frontPage.type !== 'theme_index' && effectivePostIndexEnabled && postIndexBasePath === '/') {
|
|
787
|
+
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.');
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const frontPageRoute = buildFrontPageRoute(frontPage, pages, effectivePostIndexEnabled, postIndexBasePath);
|
|
791
|
+
const pageFrontPageSlug = frontPage.type === 'page' ? frontPage.page_slug : '';
|
|
792
|
+
const preparedPages = pageFrontPageSlug
|
|
793
|
+
? pages.filter((page) => page.slug !== pageFrontPageSlug)
|
|
794
|
+
: pages;
|
|
439
795
|
|
|
440
796
|
return {
|
|
441
797
|
posts,
|
|
442
|
-
pages,
|
|
798
|
+
pages: preparedPages,
|
|
443
799
|
postBySlug,
|
|
800
|
+
collections: resolveCollections(previewData.collections, postBySlug, pageBySlug, frontPage),
|
|
444
801
|
taxonomies: buildGlobalTaxonomies(previewData, categoryCountBySlug, tagCountBySlug),
|
|
445
|
-
|
|
802
|
+
frontPageRoute,
|
|
803
|
+
indexRoutes: buildPostIndexRoutes({
|
|
804
|
+
enabled: effectivePostIndexEnabled,
|
|
805
|
+
paginate: effectivePostIndexPaginate,
|
|
446
806
|
items: previewData.content.posts,
|
|
447
807
|
postsPerPage: previewData.site.postsPerPage,
|
|
448
|
-
basePath:
|
|
808
|
+
basePath: postIndexBasePath,
|
|
449
809
|
outputStyle: previewData.site.permalinks.output_style,
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
totalPages: entry.totalPages,
|
|
454
|
-
posts: buildStructuredPostCollection(entry.items, postBySlug),
|
|
455
|
-
pagination: buildStructuredPagination(entry.paginationData),
|
|
456
|
-
})),
|
|
810
|
+
postBySlug,
|
|
811
|
+
frontPage,
|
|
812
|
+
}),
|
|
457
813
|
archiveRoutes: buildPaginatedCollection({
|
|
458
814
|
items: previewData.content.posts,
|
|
459
815
|
postsPerPage: previewData.site.postsPerPage,
|
|
@@ -500,6 +856,147 @@ function buildGlobalTaxonomies(previewData, categoryCountBySlug, tagCountBySlug)
|
|
|
500
856
|
};
|
|
501
857
|
}
|
|
502
858
|
|
|
859
|
+
function resolveCollections(collections, postBySlug, pageBySlug, frontPage) {
|
|
860
|
+
if (!collections || typeof collections !== 'object') {
|
|
861
|
+
return {};
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
return Object.fromEntries(
|
|
865
|
+
Object.entries(collections).map(([collectionId, collection]) => [
|
|
866
|
+
collectionId,
|
|
867
|
+
{
|
|
868
|
+
id: collectionId,
|
|
869
|
+
title: normalizeOptionalString(collection?.title),
|
|
870
|
+
description: normalizeOptionalString(collection?.description),
|
|
871
|
+
items: resolveCollectionItems(collectionId, collection?.items, postBySlug, pageBySlug, frontPage),
|
|
872
|
+
},
|
|
873
|
+
]),
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function resolveCollectionItems(collectionId, items, postBySlug, pageBySlug, frontPage) {
|
|
878
|
+
if (!Array.isArray(items)) {
|
|
879
|
+
return [];
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
return items.map((item, index) => resolveCollectionItem(collectionId, item, index, postBySlug, pageBySlug, frontPage));
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function resolveCollectionItem(collectionId, item, index, postBySlug, pageBySlug, frontPage) {
|
|
886
|
+
if (item?.type === 'post') {
|
|
887
|
+
const post = postBySlug.get(item.slug);
|
|
888
|
+
if (!post) {
|
|
889
|
+
throw new Error(`Invalid collection "${collectionId}": item ${index + 1} references missing post slug "${item.slug}".`);
|
|
890
|
+
}
|
|
891
|
+
return {
|
|
892
|
+
type: 'post',
|
|
893
|
+
meta: post.meta,
|
|
894
|
+
...buildStructuredPostSummary(post),
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
if (item?.type === 'page') {
|
|
899
|
+
const page = pageBySlug.get(item.slug);
|
|
900
|
+
if (!page) {
|
|
901
|
+
throw new Error(`Invalid collection "${collectionId}": item ${index + 1} references missing page slug "${item.slug}".`);
|
|
902
|
+
}
|
|
903
|
+
return buildCollectionPageSummary(page, frontPage);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
throw new Error(`Invalid collection "${collectionId}": item ${index + 1} has unsupported type "${item?.type}".`);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function buildCollectionPageSummary(page, frontPage) {
|
|
910
|
+
return {
|
|
911
|
+
type: 'page',
|
|
912
|
+
title: page.title,
|
|
913
|
+
slug: page.slug,
|
|
914
|
+
url: frontPage?.type === 'page' && frontPage.page_slug === page.slug ? '/' : page.url,
|
|
915
|
+
excerpt: page.excerpt || '',
|
|
916
|
+
featured_image: page.featured_image || '',
|
|
917
|
+
...(page.featured_media ? { featured_media: { ...page.featured_media } } : {}),
|
|
918
|
+
meta: page.meta,
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function buildFrontPageRoute(frontPage, pages, effectivePostIndexEnabled, postIndexBasePath) {
|
|
923
|
+
if (frontPage.type === 'theme_index') {
|
|
924
|
+
if (effectivePostIndexEnabled && postIndexBasePath === '/') {
|
|
925
|
+
return null;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
return {
|
|
929
|
+
path: '/',
|
|
930
|
+
front_page_type: 'theme_index',
|
|
931
|
+
posts: buildStructuredPostCollection([], new Map()),
|
|
932
|
+
pagination: buildStructuredPagination(buildDisabledPaginationData(0)),
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
if (frontPage.type === 'page') {
|
|
937
|
+
const page = pages.find((entry) => entry.slug === frontPage.page_slug);
|
|
938
|
+
if (!page) {
|
|
939
|
+
throw new Error(`Invalid front page configuration: site.front_page.page_slug "${frontPage.page_slug}" does not match a page.`);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
return {
|
|
943
|
+
path: '/',
|
|
944
|
+
front_page_type: 'page',
|
|
945
|
+
page,
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
if (frontPage.type === 'standalone_html') {
|
|
950
|
+
if (!normalizeOptionalRawString(frontPage.html)) {
|
|
951
|
+
throw new Error('Invalid front page configuration: site.front_page.html is required for standalone_html.');
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
return {
|
|
955
|
+
path: '/',
|
|
956
|
+
front_page_type: 'standalone_html',
|
|
957
|
+
html: frontPage.html,
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function buildPostIndexRoutes(options) {
|
|
965
|
+
if (!options.enabled) {
|
|
966
|
+
return [];
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
if (!options.paginate) {
|
|
970
|
+
const items = options.items.slice(0, options.postsPerPage);
|
|
971
|
+
return [{
|
|
972
|
+
path: options.basePath,
|
|
973
|
+
route_type: 'post_index',
|
|
974
|
+
is_front_page: options.basePath === '/' && options.frontPage.type === 'theme_index',
|
|
975
|
+
is_post_index: true,
|
|
976
|
+
page: 1,
|
|
977
|
+
totalPages: 1,
|
|
978
|
+
posts: buildStructuredPostCollection(items, options.postBySlug),
|
|
979
|
+
pagination: buildStructuredPagination(buildDisabledPaginationData(options.items.length)),
|
|
980
|
+
}];
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
return buildPaginatedCollection({
|
|
984
|
+
items: options.items,
|
|
985
|
+
postsPerPage: options.postsPerPage,
|
|
986
|
+
basePath: options.basePath,
|
|
987
|
+
outputStyle: options.outputStyle,
|
|
988
|
+
}).map((entry) => ({
|
|
989
|
+
path: entry.path,
|
|
990
|
+
route_type: 'post_index',
|
|
991
|
+
is_front_page: entry.path === '/' && options.frontPage.type === 'theme_index',
|
|
992
|
+
is_post_index: true,
|
|
993
|
+
page: entry.page,
|
|
994
|
+
totalPages: entry.totalPages,
|
|
995
|
+
posts: buildStructuredPostCollection(entry.items, options.postBySlug),
|
|
996
|
+
pagination: buildStructuredPagination(entry.paginationData),
|
|
997
|
+
}));
|
|
998
|
+
}
|
|
999
|
+
|
|
503
1000
|
function buildGlobalTaxonomyItems(site, permalinkKind, items, countBySlug) {
|
|
504
1001
|
return items.map((item) => ({
|
|
505
1002
|
name: item.name,
|
|
@@ -785,6 +1282,7 @@ function preparePost(post, site, authorsById, categoriesBySlug, tagsBySlug, them
|
|
|
785
1282
|
updated_at_iso: post.updated_at_iso,
|
|
786
1283
|
author_id: post.author_id,
|
|
787
1284
|
featured_image: post.featured_image,
|
|
1285
|
+
...(post.featured_media ? { featured_media: { ...post.featured_media } } : {}),
|
|
788
1286
|
meta: post.meta,
|
|
789
1287
|
status: post.status,
|
|
790
1288
|
allow_comments: post.allow_comments,
|
|
@@ -794,6 +1292,7 @@ function preparePost(post, site, authorsById, categoriesBySlug, tagsBySlug, them
|
|
|
794
1292
|
id: post.author_id,
|
|
795
1293
|
display_name: normalizeNonEmptyString(author?.display_name, post.author_id),
|
|
796
1294
|
avatar: author?.avatar || '',
|
|
1295
|
+
...(author?.avatar_media ? { avatar_media: { ...author.avatar_media } } : {}),
|
|
797
1296
|
},
|
|
798
1297
|
categories,
|
|
799
1298
|
tags,
|
|
@@ -904,6 +1403,7 @@ function buildPaginationData(currentPage, totalPages, totalPosts, basePath, outp
|
|
|
904
1403
|
const buildPageUrl = (page) => routePathToPublicUrl(buildPaginatedPath(basePath, page), outputStyle);
|
|
905
1404
|
|
|
906
1405
|
return {
|
|
1406
|
+
enabled: true,
|
|
907
1407
|
currentPage,
|
|
908
1408
|
totalPages,
|
|
909
1409
|
totalPosts,
|
|
@@ -922,8 +1422,23 @@ function buildPaginationData(currentPage, totalPages, totalPosts, basePath, outp
|
|
|
922
1422
|
};
|
|
923
1423
|
}
|
|
924
1424
|
|
|
1425
|
+
function buildDisabledPaginationData(totalPosts) {
|
|
1426
|
+
return {
|
|
1427
|
+
enabled: false,
|
|
1428
|
+
currentPage: 1,
|
|
1429
|
+
totalPages: 1,
|
|
1430
|
+
totalPosts,
|
|
1431
|
+
hasNext: false,
|
|
1432
|
+
hasPrev: false,
|
|
1433
|
+
nextUrl: undefined,
|
|
1434
|
+
prevUrl: undefined,
|
|
1435
|
+
pages: [],
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
|
|
925
1439
|
function buildStructuredPagination(paginationData) {
|
|
926
1440
|
return {
|
|
1441
|
+
enabled: paginationData.enabled !== false,
|
|
927
1442
|
current_page: paginationData.currentPage,
|
|
928
1443
|
total_pages: paginationData.totalPages,
|
|
929
1444
|
total_items: paginationData.totalPosts,
|
|
@@ -1019,9 +1534,11 @@ function buildStructuredPostSummary(post) {
|
|
|
1019
1534
|
published_at_iso: post.published_at_iso,
|
|
1020
1535
|
reading_time: post.reading_time,
|
|
1021
1536
|
featured_image: post.featured_image,
|
|
1537
|
+
...(post.featured_media ? { featured_media: { ...post.featured_media } } : {}),
|
|
1022
1538
|
author: {
|
|
1023
1539
|
display_name: post.author?.display_name || '',
|
|
1024
1540
|
avatar: post.author?.avatar || '',
|
|
1541
|
+
...(post.author?.avatar_media ? { avatar_media: { ...post.author.avatar_media } } : {}),
|
|
1025
1542
|
},
|
|
1026
1543
|
categories: Array.isArray(post.categories) ? post.categories.map((category) => ({ ...category })) : [],
|
|
1027
1544
|
tags: Array.isArray(post.tags) ? post.tags.map((tag) => ({ ...tag })) : [],
|
|
@@ -1271,7 +1788,8 @@ async function normalizeAndValidateThemePackage(themePackage) {
|
|
|
1271
1788
|
...(themePackage.metadata.features ? { features: themePackage.metadata.features } : {}),
|
|
1272
1789
|
...(themePackage.metadata.menuSlots ? { menuSlots: themePackage.metadata.menuSlots } : {}),
|
|
1273
1790
|
...(themePackage.metadata.widgetAreas ? { widgetAreas: themePackage.metadata.widgetAreas } : {}),
|
|
1274
|
-
|
|
1791
|
+
...(themePackage.metadata.siteMeta ? { siteMeta: themePackage.metadata.siteMeta } : {}),
|
|
1792
|
+
...(themePackage.metadata.collectionSlots ? { collectionSlots: themePackage.metadata.collectionSlots } : {}),
|
|
1275
1793
|
}));
|
|
1276
1794
|
|
|
1277
1795
|
for (const [templateName, templateContent] of themePackage.templates.entries()) {
|
|
@@ -1307,7 +1825,6 @@ function normalizeThemePackageMetadata(sourceMetadata, manifest) {
|
|
|
1307
1825
|
return {
|
|
1308
1826
|
...manifest,
|
|
1309
1827
|
...(sourceMetadata?.thumbnail ? { thumbnail: sourceMetadata.thumbnail } : {}),
|
|
1310
|
-
settings: sourceMetadata?.settings || {},
|
|
1311
1828
|
};
|
|
1312
1829
|
}
|
|
1313
1830
|
|
|
@@ -1367,10 +1884,20 @@ function createRenderContext(site, currentUrl) {
|
|
|
1367
1884
|
};
|
|
1368
1885
|
}
|
|
1369
1886
|
|
|
1887
|
+
function buildRouteContext(type, url, options = {}) {
|
|
1888
|
+
return {
|
|
1889
|
+
type,
|
|
1890
|
+
is_front_page: options.isFrontPage === true,
|
|
1891
|
+
is_post_index: options.isPostIndex === true,
|
|
1892
|
+
path: url,
|
|
1893
|
+
url,
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1370
1897
|
function buildPageMeta(site, options = {}) {
|
|
1371
1898
|
const resolvedTitle = normalizeNonEmptyString(options.title, site.title);
|
|
1372
1899
|
const resolvedDescription = normalizeOptionalString(options.description);
|
|
1373
|
-
const canonicalUrl = resolveMetaCanonicalUrl(site, options.currentUrl);
|
|
1900
|
+
const canonicalUrl = resolveMetaCanonicalUrl(site, options.canonicalUrl || options.currentUrl);
|
|
1374
1901
|
const ogImage = resolveMetaImageUrl(options.image);
|
|
1375
1902
|
const ogType = normalizeNonEmptyString(options.ogType, 'website');
|
|
1376
1903
|
const publishedTime = normalizeOptionalString(options.publishedTime);
|
|
@@ -1471,7 +1998,7 @@ function normalizeMediaField(value, mediaBaseUrl) {
|
|
|
1471
1998
|
|
|
1472
1999
|
const normalizedBaseUrl = normalizeOptionalString(mediaBaseUrl);
|
|
1473
2000
|
if (!normalizedBaseUrl) {
|
|
1474
|
-
return
|
|
2001
|
+
return normalizedValue;
|
|
1475
2002
|
}
|
|
1476
2003
|
|
|
1477
2004
|
try {
|
|
@@ -1550,6 +2077,22 @@ function routePathToPublicUrl(routePath, outputStyle = DEFAULT_PERMALINKS.output
|
|
|
1550
2077
|
return normalizedPath;
|
|
1551
2078
|
}
|
|
1552
2079
|
|
|
2080
|
+
function pagePathToPublicUrl(routePath, outputStyle = DEFAULT_PERMALINKS.output_style) {
|
|
2081
|
+
const normalizedPath = normalizeRoutePath(routePath);
|
|
2082
|
+
if (outputStyle !== 'html-extension') {
|
|
2083
|
+
return routePathToPublicUrl(normalizedPath, outputStyle);
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
const withoutTrailingSlash = normalizedPath.replace(/\/$/, '');
|
|
2087
|
+
if (withoutTrailingSlash === '/index') {
|
|
2088
|
+
return '/';
|
|
2089
|
+
}
|
|
2090
|
+
if (withoutTrailingSlash.endsWith('/index')) {
|
|
2091
|
+
return `${withoutTrailingSlash.slice(0, -'/index'.length)}/`;
|
|
2092
|
+
}
|
|
2093
|
+
return withoutTrailingSlash;
|
|
2094
|
+
}
|
|
2095
|
+
|
|
1553
2096
|
function normalizeRoutePath(routePath) {
|
|
1554
2097
|
if (!routePath || routePath === '/') {
|
|
1555
2098
|
return '/';
|
|
@@ -1560,7 +2103,7 @@ function normalizeRoutePath(routePath) {
|
|
|
1560
2103
|
|
|
1561
2104
|
function resolvePagePermalink(site, page) {
|
|
1562
2105
|
if (normalizeOptionalString(page.path)) {
|
|
1563
|
-
return buildRouteInfo(page.path, site.permalinks.output_style);
|
|
2106
|
+
return buildRouteInfo(page.path, site.permalinks.output_style, { pagePath: true });
|
|
1564
2107
|
}
|
|
1565
2108
|
return resolvePermalink(site, 'pages', page);
|
|
1566
2109
|
}
|
|
@@ -1570,15 +2113,21 @@ function resolvePermalink(site, kind, item) {
|
|
|
1570
2113
|
return buildRouteInfo(applyPermalinkPattern(pattern, kind, item, site), site.permalinks.output_style);
|
|
1571
2114
|
}
|
|
1572
2115
|
|
|
1573
|
-
function buildRouteInfo(routePath, outputStyle) {
|
|
2116
|
+
function buildRouteInfo(routePath, outputStyle, options = {}) {
|
|
1574
2117
|
const path = normalizeRoutePath(routePath);
|
|
1575
2118
|
return {
|
|
1576
2119
|
path,
|
|
1577
|
-
url: routePathToPublicUrl(path, outputStyle),
|
|
2120
|
+
url: options.pagePath ? pagePathToPublicUrl(path, outputStyle) : routePathToPublicUrl(path, outputStyle),
|
|
1578
2121
|
outputPath: routePathToOutputPath(path, outputStyle),
|
|
1579
2122
|
};
|
|
1580
2123
|
}
|
|
1581
2124
|
|
|
2125
|
+
function pageToOutputPath(page, outputStyle) {
|
|
2126
|
+
return normalizeOptionalString(page.path)
|
|
2127
|
+
? routePathToOutputPath(page.path, outputStyle)
|
|
2128
|
+
: routePathToOutputPath(page.url, outputStyle);
|
|
2129
|
+
}
|
|
2130
|
+
|
|
1582
2131
|
function applyPermalinkPattern(pattern, kind, item, site) {
|
|
1583
2132
|
const tokenValues = buildPermalinkTokenValues(kind, item, site);
|
|
1584
2133
|
const body = String(pattern || '').replace(/^\/+|\/+$/g, '');
|
|
@@ -1635,7 +2184,7 @@ function assertPlannedOutputPathsSafe(state) {
|
|
|
1635
2184
|
}
|
|
1636
2185
|
|
|
1637
2186
|
for (const page of state.renderData.pages) {
|
|
1638
|
-
assertSafeSlugDerivedOutputPath(page.slug,
|
|
2187
|
+
assertSafeSlugDerivedOutputPath(page.slug, pageToOutputPath(page, outputStyle));
|
|
1639
2188
|
}
|
|
1640
2189
|
|
|
1641
2190
|
for (const category of state.previewData.content.categories) {
|
|
@@ -1647,6 +2196,10 @@ function assertPlannedOutputPathsSafe(state) {
|
|
|
1647
2196
|
}
|
|
1648
2197
|
|
|
1649
2198
|
const routeEntries = [
|
|
2199
|
+
...(state.renderData.frontPageRoute ? [{
|
|
2200
|
+
url: '/',
|
|
2201
|
+
outputPath: 'index.html',
|
|
2202
|
+
}] : []),
|
|
1650
2203
|
...state.renderData.indexRoutes.map((route) => ({
|
|
1651
2204
|
url: routePathToPublicUrl(route.path, outputStyle),
|
|
1652
2205
|
outputPath: routePathToOutputPath(route.path, outputStyle),
|
|
@@ -1669,7 +2222,7 @@ function assertPlannedOutputPathsSafe(state) {
|
|
|
1669
2222
|
})),
|
|
1670
2223
|
...state.renderData.pages.map((page) => ({
|
|
1671
2224
|
url: page.url,
|
|
1672
|
-
outputPath:
|
|
2225
|
+
outputPath: pageToOutputPath(page, outputStyle),
|
|
1673
2226
|
})),
|
|
1674
2227
|
];
|
|
1675
2228
|
assertUniqueRoutes(routeEntries);
|
|
@@ -1681,7 +2234,10 @@ function assertPlannedOutputPathsSafe(state) {
|
|
|
1681
2234
|
];
|
|
1682
2235
|
|
|
1683
2236
|
if (state.options.generateSpecialFiles) {
|
|
1684
|
-
plannedPaths.push('404.html'
|
|
2237
|
+
plannedPaths.push('404.html');
|
|
2238
|
+
if (shouldGenerateRobotsTxt(state.options)) {
|
|
2239
|
+
plannedPaths.push('robots.txt');
|
|
2240
|
+
}
|
|
1685
2241
|
if (hasCanonicalSiteUrl(state.previewData.site.url)) {
|
|
1686
2242
|
plannedPaths.push('sitemap.xml', 'feed.xml');
|
|
1687
2243
|
}
|
|
@@ -1781,11 +2337,43 @@ function sha256(content) {
|
|
|
1781
2337
|
}
|
|
1782
2338
|
|
|
1783
2339
|
function injectSiteCustomizations(html, state) {
|
|
1784
|
-
let next =
|
|
2340
|
+
let next = injectFaviconLinks(html, state.favicon);
|
|
2341
|
+
next = injectCustomCssAssetLink(next, state.customCssHref);
|
|
1785
2342
|
next = injectCustomHtml(next, state.customHtml);
|
|
1786
2343
|
return next;
|
|
1787
2344
|
}
|
|
1788
2345
|
|
|
2346
|
+
function injectFaviconLinks(html, favicon) {
|
|
2347
|
+
const links = buildFaviconLinks(favicon);
|
|
2348
|
+
if (!links) {
|
|
2349
|
+
return html;
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
return html.replace('</head>', `${links}\n</head>`);
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
function buildFaviconLinks(favicon) {
|
|
2356
|
+
if (!favicon || typeof favicon !== 'object') {
|
|
2357
|
+
return '';
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
const lines = [];
|
|
2361
|
+
if (normalizeOptionalString(favicon.icon)) {
|
|
2362
|
+
lines.push(` <link rel="icon" href="${escapeHtml(favicon.icon)}" sizes="any">`);
|
|
2363
|
+
}
|
|
2364
|
+
if (normalizeOptionalString(favicon.svg)) {
|
|
2365
|
+
lines.push(` <link rel="icon" href="${escapeHtml(favicon.svg)}" type="image/svg+xml">`);
|
|
2366
|
+
}
|
|
2367
|
+
if (normalizeOptionalString(favicon.png)) {
|
|
2368
|
+
lines.push(` <link rel="icon" href="${escapeHtml(favicon.png)}" type="image/png">`);
|
|
2369
|
+
}
|
|
2370
|
+
if (normalizeOptionalString(favicon.apple_touch_icon)) {
|
|
2371
|
+
lines.push(` <link rel="apple-touch-icon" href="${escapeHtml(favicon.apple_touch_icon)}">`);
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
return lines.join('\n');
|
|
2375
|
+
}
|
|
2376
|
+
|
|
1789
2377
|
function injectCustomCssAssetLink(html, href) {
|
|
1790
2378
|
if (!normalizeOptionalString(href)) {
|
|
1791
2379
|
return html;
|
|
@@ -1811,12 +2399,19 @@ function injectCustomHtml(html, customHtml) {
|
|
|
1811
2399
|
|
|
1812
2400
|
function buildSitemapXml(site, emitted, generatedAt) {
|
|
1813
2401
|
const entries = [
|
|
2402
|
+
...(emitted.frontPage
|
|
2403
|
+
? [{
|
|
2404
|
+
url: emitted.frontPage.url,
|
|
2405
|
+
changefreq: 'daily',
|
|
2406
|
+
priority: 1.0,
|
|
2407
|
+
}]
|
|
2408
|
+
: []),
|
|
1814
2409
|
...emitted.indexRoutes
|
|
1815
|
-
.filter((route) => route.
|
|
2410
|
+
.filter((route) => route.page === 1)
|
|
1816
2411
|
.map((route) => ({
|
|
1817
2412
|
url: route.url,
|
|
1818
2413
|
changefreq: 'daily',
|
|
1819
|
-
priority: 1.0,
|
|
2414
|
+
priority: route.url === '/' ? 1.0 : 0.7,
|
|
1820
2415
|
})),
|
|
1821
2416
|
...emitted.posts.map((post) => ({
|
|
1822
2417
|
url: post.url,
|
|
@@ -1824,11 +2419,13 @@ function buildSitemapXml(site, emitted, generatedAt) {
|
|
|
1824
2419
|
changefreq: 'weekly',
|
|
1825
2420
|
priority: 0.8,
|
|
1826
2421
|
})),
|
|
1827
|
-
...emitted.pages
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
2422
|
+
...emitted.pages
|
|
2423
|
+
.filter((page) => page.includeInSitemap !== false)
|
|
2424
|
+
.map((page) => ({
|
|
2425
|
+
url: page.url,
|
|
2426
|
+
changefreq: 'monthly',
|
|
2427
|
+
priority: 0.7,
|
|
2428
|
+
})),
|
|
1832
2429
|
];
|
|
1833
2430
|
|
|
1834
2431
|
const body = entries.map((entry) => {
|
|
@@ -1858,13 +2455,23 @@ function buildFeedXml(site, emitted, generatedAt) {
|
|
|
1858
2455
|
}
|
|
1859
2456
|
|
|
1860
2457
|
function buildRobotsTxt(site) {
|
|
1861
|
-
const lines = ['User-agent: *'
|
|
2458
|
+
const lines = ['User-agent: *'];
|
|
2459
|
+
if (site.indexing === false) {
|
|
2460
|
+
lines.push('Disallow: /');
|
|
2461
|
+
return `${lines.join('\n')}\n`;
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
lines.push('Allow: /');
|
|
1862
2465
|
if (site.url) {
|
|
1863
2466
|
lines.push('', `Sitemap: ${resolveSiteUrl(site.url, '/sitemap.xml')}`);
|
|
1864
2467
|
}
|
|
1865
2468
|
return `${lines.join('\n')}\n`;
|
|
1866
2469
|
}
|
|
1867
2470
|
|
|
2471
|
+
function shouldGenerateRobotsTxt(options) {
|
|
2472
|
+
return options.generateSpecialFiles && options.generateRobotsTxt !== false;
|
|
2473
|
+
}
|
|
2474
|
+
|
|
1868
2475
|
function getContentType(assetPath) {
|
|
1869
2476
|
const ext = assetPath.split('.').pop()?.toLowerCase();
|
|
1870
2477
|
const contentTypes = {
|