@zeropress/build-core 0.1.14 → 0.1.16
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 +6 -4
- package/package.json +3 -3
- package/src/api/build-site.js +360 -338
- package/src/index.d.ts +2 -18
- package/src/render/control-flow-renderer.js +221 -0
- package/src/render/partial-resolver.js +23 -0
- package/src/render/variable-resolver.js +47 -4
- package/src/render/zeropress-engine.js +30 -2
- package/src/theme/load-theme-dir.js +5 -10
package/src/api/build-site.js
CHANGED
|
@@ -9,7 +9,6 @@ import { ZeroPressEngine } from '../render/zeropress-engine.js';
|
|
|
9
9
|
const DEFAULT_OPTIONS = {
|
|
10
10
|
assetHashing: true,
|
|
11
11
|
generateSpecialFiles: true,
|
|
12
|
-
injectHtmx: true,
|
|
13
12
|
writeManifest: false,
|
|
14
13
|
};
|
|
15
14
|
|
|
@@ -18,7 +17,10 @@ const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD';
|
|
|
18
17
|
const DEFAULT_TIME_FORMAT = 'HH:mm';
|
|
19
18
|
const DEFAULT_TIMEZONE = 'UTC';
|
|
20
19
|
const DEFAULT_LOCALE = 'en-US';
|
|
20
|
+
const COMMENT_POLICY_OUTPUT_PATH = '_zeropress/comment-policy.json';
|
|
21
21
|
const OUTPUT_PATH_CONTROL_CHAR_PATTERN = /[\u0000-\u001F\u007F]/;
|
|
22
|
+
const SAFE_MEDIA_PROTOCOLS = new Set(['http:', 'https:']);
|
|
23
|
+
const SAFE_LINK_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:']);
|
|
22
24
|
export async function buildSite(input) {
|
|
23
25
|
const options = { ...DEFAULT_OPTIONS, ...(input.options || {}) };
|
|
24
26
|
const state = await createBuildState(input, options);
|
|
@@ -58,6 +60,14 @@ export async function buildSite(input) {
|
|
|
58
60
|
await writeOutput(state.writer, state.summaries, assetOutput.path, assetOutput.content, assetOutput.contentType);
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
await writeOutput(
|
|
64
|
+
state.writer,
|
|
65
|
+
state.summaries,
|
|
66
|
+
COMMENT_POLICY_OUTPUT_PATH,
|
|
67
|
+
state.commentPolicyContent,
|
|
68
|
+
'application/json',
|
|
69
|
+
);
|
|
70
|
+
|
|
61
71
|
if (options.generateSpecialFiles) {
|
|
62
72
|
await maybeRenderNotFoundPage(state);
|
|
63
73
|
if (hasCanonicalSiteUrl(state.previewData.site.url)) {
|
|
@@ -77,14 +87,13 @@ async function createBuildState(input, options) {
|
|
|
77
87
|
}
|
|
78
88
|
|
|
79
89
|
assertPreviewData(input.previewData);
|
|
80
|
-
await
|
|
90
|
+
const themePackage = await normalizeAndValidateThemePackage(input.themePackage);
|
|
81
91
|
|
|
82
92
|
const engine = new ZeroPressEngine();
|
|
83
93
|
const assetProcessor = new AssetProcessor();
|
|
84
94
|
const summaries = [];
|
|
85
|
-
const themePackage = input.themePackage;
|
|
86
95
|
const previewData = normalizePreviewData(input.previewData);
|
|
87
|
-
const renderData = createRenderData(previewData);
|
|
96
|
+
const renderData = createRenderData(previewData, themePackage.metadata);
|
|
88
97
|
|
|
89
98
|
engine.initialize(themePackage);
|
|
90
99
|
|
|
@@ -101,13 +110,14 @@ async function createBuildState(input, options) {
|
|
|
101
110
|
writer: input.writer,
|
|
102
111
|
previewData,
|
|
103
112
|
renderData,
|
|
104
|
-
|
|
113
|
+
widgets: resolveWidgetAreas(previewData, renderData),
|
|
105
114
|
engine,
|
|
106
115
|
assetProcessor,
|
|
107
116
|
summaries,
|
|
108
117
|
assetOutputs,
|
|
109
118
|
assetMap,
|
|
110
119
|
customCssHref: customCssAsset ? `/${customCssAsset.path}` : '',
|
|
120
|
+
commentPolicyContent: buildCommentPolicyManifest(renderData.posts),
|
|
111
121
|
options,
|
|
112
122
|
generatedAt: new Date(),
|
|
113
123
|
emitted: {
|
|
@@ -149,7 +159,7 @@ async function renderRoute(state, templateName, route) {
|
|
|
149
159
|
templateName,
|
|
150
160
|
{
|
|
151
161
|
menus: state.previewData.menus,
|
|
152
|
-
widgets: state.
|
|
162
|
+
widgets: state.widgets,
|
|
153
163
|
...route,
|
|
154
164
|
meta: buildPageMeta(state.previewData.site, {
|
|
155
165
|
currentUrl,
|
|
@@ -172,7 +182,7 @@ async function renderPost(state, post) {
|
|
|
172
182
|
'post',
|
|
173
183
|
{
|
|
174
184
|
menus: state.previewData.menus,
|
|
175
|
-
widgets: state.
|
|
185
|
+
widgets: state.widgets,
|
|
176
186
|
post,
|
|
177
187
|
meta: buildPageMeta(state.previewData.site, {
|
|
178
188
|
currentUrl,
|
|
@@ -188,9 +198,6 @@ async function renderPost(state, post) {
|
|
|
188
198
|
);
|
|
189
199
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
190
200
|
html = injectCustomCssAssetLink(html, state.customCssHref);
|
|
191
|
-
if (state.options.injectHtmx) {
|
|
192
|
-
html = injectHtmxScript(html);
|
|
193
|
-
}
|
|
194
201
|
await writeOutput(state.writer, state.summaries, `posts/${encodeSlugSegment(post.slug)}/index.html`, html, 'text/html');
|
|
195
202
|
state.emitted.posts.push({
|
|
196
203
|
url: currentUrl,
|
|
@@ -208,7 +215,7 @@ async function renderPage(state, page) {
|
|
|
208
215
|
'page',
|
|
209
216
|
{
|
|
210
217
|
menus: state.previewData.menus,
|
|
211
|
-
widgets: state.
|
|
218
|
+
widgets: state.widgets,
|
|
212
219
|
page,
|
|
213
220
|
meta: buildPageMeta(state.previewData.site, {
|
|
214
221
|
currentUrl,
|
|
@@ -240,7 +247,7 @@ async function maybeRenderNotFoundPage(state) {
|
|
|
240
247
|
'404',
|
|
241
248
|
{
|
|
242
249
|
menus: state.previewData.menus,
|
|
243
|
-
widgets: state.
|
|
250
|
+
widgets: state.widgets,
|
|
244
251
|
meta: buildPageMeta(state.previewData.site, {
|
|
245
252
|
currentUrl: '/404.html',
|
|
246
253
|
title: state.previewData.site.title,
|
|
@@ -320,7 +327,7 @@ function normalizeWidgetAreas(widgetAreas, mediaBaseUrl) {
|
|
|
320
327
|
function normalizeWidgetItem(item, mediaBaseUrl) {
|
|
321
328
|
const normalizedItem = {
|
|
322
329
|
...item,
|
|
323
|
-
title:
|
|
330
|
+
title: typeof item?.title === 'string' ? item.title.trim() : '',
|
|
324
331
|
};
|
|
325
332
|
|
|
326
333
|
if (item?.type === 'profile' && item?.settings && typeof item.settings === 'object') {
|
|
@@ -341,7 +348,8 @@ function normalizeCustomCss(customCss) {
|
|
|
341
348
|
return content ? { content } : undefined;
|
|
342
349
|
}
|
|
343
350
|
|
|
344
|
-
function createRenderData(previewData) {
|
|
351
|
+
function createRenderData(previewData, themeMetadata = {}) {
|
|
352
|
+
const themeSupportsComments = themeMetadata?.features?.comments === true;
|
|
345
353
|
const authorsById = new Map(previewData.content.authors.map((author) => [author.id, author]));
|
|
346
354
|
const categoriesBySlug = new Map(previewData.content.categories.map((category) => [category.slug, category]));
|
|
347
355
|
const tagsBySlug = new Map(previewData.content.tags.map((tag) => [tag.slug, tag]));
|
|
@@ -362,11 +370,14 @@ function createRenderData(previewData) {
|
|
|
362
370
|
}
|
|
363
371
|
}
|
|
364
372
|
|
|
365
|
-
const
|
|
373
|
+
const preparedPosts = previewData.content.posts.map((post) => preparePost(post, previewData.site, authorsById, categoriesBySlug, tagsBySlug, themeSupportsComments));
|
|
374
|
+
const posts = preparedPosts.map((post, index) => ({
|
|
375
|
+
...post,
|
|
376
|
+
prev: index > 0 ? buildAdjacentPostSummary(preparedPosts[index - 1]) : null,
|
|
377
|
+
next: index < preparedPosts.length - 1 ? buildAdjacentPostSummary(preparedPosts[index + 1]) : null,
|
|
378
|
+
}));
|
|
366
379
|
const pages = previewData.content.pages.map((page) => preparePage(page));
|
|
367
380
|
const postBySlug = new Map(posts.map((post) => [post.slug, post]));
|
|
368
|
-
const categoryLinks = renderCategoryLinks(previewData.content.categories, categoryCountBySlug);
|
|
369
|
-
const tagLinks = renderTagLinks(previewData.content.tags, tagCountBySlug);
|
|
370
381
|
|
|
371
382
|
return {
|
|
372
383
|
posts,
|
|
@@ -380,10 +391,8 @@ function createRenderData(previewData) {
|
|
|
380
391
|
path: entry.path,
|
|
381
392
|
page: entry.page,
|
|
382
393
|
totalPages: entry.totalPages,
|
|
383
|
-
posts:
|
|
384
|
-
|
|
385
|
-
tags: tagLinks,
|
|
386
|
-
pagination: renderPagination(entry.paginationData),
|
|
394
|
+
posts: buildStructuredPostCollection(entry.items, postBySlug),
|
|
395
|
+
pagination: buildStructuredPagination(entry.paginationData),
|
|
387
396
|
})),
|
|
388
397
|
archiveRoutes: buildPaginatedCollection({
|
|
389
398
|
items: previewData.content.posts,
|
|
@@ -393,29 +402,35 @@ function createRenderData(previewData) {
|
|
|
393
402
|
path: entry.path,
|
|
394
403
|
page: entry.page,
|
|
395
404
|
totalPages: entry.totalPages,
|
|
396
|
-
|
|
397
|
-
|
|
405
|
+
archive: {
|
|
406
|
+
groups: buildArchiveGroups(entry.items, postBySlug),
|
|
407
|
+
},
|
|
408
|
+
pagination: buildStructuredPagination(entry.paginationData),
|
|
398
409
|
})),
|
|
399
410
|
categoryRoutes: buildTaxonomyRoutes({
|
|
400
411
|
items: previewData.content.categories,
|
|
401
412
|
postsBySlug: categoryPostsBySlug,
|
|
413
|
+
postBySlug,
|
|
402
414
|
postsPerPage: previewData.site.postsPerPage,
|
|
403
415
|
buildBasePath: (category) => `/categories/${encodeSlugSegment(category.slug)}/`,
|
|
404
|
-
|
|
405
|
-
|
|
416
|
+
renderExtras: (category) => ({
|
|
417
|
+
taxonomy: buildTaxonomyRouteData('category', category, categoryCountBySlug),
|
|
418
|
+
}),
|
|
406
419
|
}),
|
|
407
420
|
tagRoutes: buildTaxonomyRoutes({
|
|
408
421
|
items: previewData.content.tags,
|
|
409
422
|
postsBySlug: tagPostsBySlug,
|
|
423
|
+
postBySlug,
|
|
410
424
|
postsPerPage: previewData.site.postsPerPage,
|
|
411
425
|
buildBasePath: (tag) => `/tags/${encodeSlugSegment(tag.slug)}/`,
|
|
412
|
-
|
|
413
|
-
|
|
426
|
+
renderExtras: (tag) => ({
|
|
427
|
+
taxonomy: buildTaxonomyRouteData('tag', tag, tagCountBySlug),
|
|
428
|
+
}),
|
|
414
429
|
}),
|
|
415
430
|
};
|
|
416
431
|
}
|
|
417
432
|
|
|
418
|
-
function
|
|
433
|
+
function resolveWidgetAreas(previewData, renderData) {
|
|
419
434
|
if (!previewData.widgets || typeof previewData.widgets !== 'object') {
|
|
420
435
|
return {};
|
|
421
436
|
}
|
|
@@ -423,243 +438,223 @@ function renderWidgetAreas(previewData, renderData) {
|
|
|
423
438
|
return Object.fromEntries(
|
|
424
439
|
Object.entries(previewData.widgets).map(([widgetAreaId, widgetArea]) => [
|
|
425
440
|
widgetAreaId,
|
|
426
|
-
|
|
441
|
+
resolveWidgetArea(widgetArea, previewData, renderData, widgetAreaId),
|
|
427
442
|
]),
|
|
428
443
|
);
|
|
429
444
|
}
|
|
430
445
|
|
|
431
|
-
function
|
|
432
|
-
if (!widgetArea || !Array.isArray(widgetArea.items)
|
|
433
|
-
return
|
|
446
|
+
function resolveWidgetArea(widgetArea, previewData, renderData, widgetAreaId) {
|
|
447
|
+
if (!widgetArea || !Array.isArray(widgetArea.items)) {
|
|
448
|
+
return {
|
|
449
|
+
name: normalizeNonEmptyString(widgetArea?.name, widgetAreaId),
|
|
450
|
+
items: [],
|
|
451
|
+
};
|
|
434
452
|
}
|
|
435
453
|
|
|
436
|
-
return
|
|
437
|
-
|
|
438
|
-
.
|
|
439
|
-
|
|
454
|
+
return {
|
|
455
|
+
name: normalizeNonEmptyString(widgetArea?.name, widgetAreaId),
|
|
456
|
+
items: widgetArea.items
|
|
457
|
+
.map((item, index) => resolveWidgetItem(item, previewData, renderData, widgetAreaId, index))
|
|
458
|
+
.filter(Boolean),
|
|
459
|
+
};
|
|
440
460
|
}
|
|
441
461
|
|
|
442
|
-
function
|
|
443
|
-
if (!item || typeof item !== 'object') {
|
|
444
|
-
return
|
|
462
|
+
function resolveWidgetItem(item, previewData, renderData, widgetAreaId, index) {
|
|
463
|
+
if (!item || typeof item !== 'object' || typeof item.type !== 'string') {
|
|
464
|
+
return null;
|
|
445
465
|
}
|
|
446
466
|
|
|
447
|
-
const
|
|
467
|
+
const baseWidget = {
|
|
468
|
+
id: `${widgetAreaId}-${index + 1}`,
|
|
469
|
+
type: item.type,
|
|
470
|
+
title: typeof item.title === 'string' ? item.title : '',
|
|
471
|
+
empty: false,
|
|
472
|
+
};
|
|
448
473
|
|
|
449
474
|
switch (item.type) {
|
|
450
475
|
case 'recent-posts':
|
|
451
|
-
return
|
|
476
|
+
return resolveRecentPostsWidget(baseWidget, item.settings, renderData);
|
|
452
477
|
case 'categories':
|
|
453
|
-
return
|
|
478
|
+
return resolveCategoriesWidget(baseWidget, item.settings, previewData);
|
|
454
479
|
case 'tags':
|
|
455
|
-
return
|
|
480
|
+
return resolveTagsWidget(baseWidget, item.settings, previewData);
|
|
456
481
|
case 'archives':
|
|
457
|
-
return
|
|
458
|
-
case 'image':
|
|
459
|
-
return renderImageWidget(title, item.settings);
|
|
482
|
+
return resolveArchivesWidget(baseWidget, item.settings, previewData);
|
|
460
483
|
case 'text':
|
|
461
|
-
return
|
|
484
|
+
return resolveTextWidget(baseWidget, item.settings);
|
|
462
485
|
case 'link-list':
|
|
463
|
-
return
|
|
486
|
+
return resolveLinkListWidget(baseWidget, item.settings);
|
|
464
487
|
case 'search':
|
|
465
|
-
return
|
|
488
|
+
return resolveSearchWidget(baseWidget, item.settings, widgetAreaId, index);
|
|
466
489
|
case 'profile':
|
|
467
|
-
return
|
|
490
|
+
return resolveProfileWidget(baseWidget, item.settings);
|
|
468
491
|
default:
|
|
469
|
-
return
|
|
492
|
+
return null;
|
|
470
493
|
}
|
|
471
494
|
}
|
|
472
495
|
|
|
473
|
-
function
|
|
496
|
+
function resolveRecentPostsWidget(baseWidget, settings, renderData) {
|
|
474
497
|
const limit = clampInteger(settings?.limit, 5, 1, 20);
|
|
475
|
-
const showDate = settings?.show_date !== false;
|
|
476
498
|
const items = renderData.posts
|
|
477
499
|
.slice(0, limit)
|
|
478
|
-
.map((post) => {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
})
|
|
485
|
-
.join('');
|
|
500
|
+
.map((post) => ({
|
|
501
|
+
title: post.title,
|
|
502
|
+
url: `/posts/${encodeSlugSegment(post.slug)}/`,
|
|
503
|
+
published_at: post.published_at,
|
|
504
|
+
published_at_iso: post.published_at_iso,
|
|
505
|
+
}));
|
|
486
506
|
|
|
487
|
-
if (
|
|
488
|
-
return
|
|
507
|
+
if (items.length === 0) {
|
|
508
|
+
return null;
|
|
489
509
|
}
|
|
490
510
|
|
|
491
|
-
return
|
|
511
|
+
return {
|
|
512
|
+
...baseWidget,
|
|
513
|
+
show_date: settings?.show_date !== false,
|
|
514
|
+
items,
|
|
515
|
+
};
|
|
492
516
|
}
|
|
493
517
|
|
|
494
|
-
function
|
|
518
|
+
function resolveCategoriesWidget(baseWidget, settings, previewData) {
|
|
495
519
|
const countBySlug = buildTaxonomyCountMap(previewData.content.posts, 'category_slugs');
|
|
496
520
|
const items = previewData.content.categories
|
|
497
|
-
.map((category) => {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
})
|
|
506
|
-
.filter(Boolean)
|
|
507
|
-
.join('');
|
|
521
|
+
.map((category) => ({
|
|
522
|
+
name: category.name,
|
|
523
|
+
slug: category.slug,
|
|
524
|
+
url: `/categories/${encodeSlugSegment(category.slug)}/`,
|
|
525
|
+
count: countBySlug.get(category.slug) || 0,
|
|
526
|
+
depth: 0,
|
|
527
|
+
}))
|
|
528
|
+
.filter((category) => category.count > 0);
|
|
508
529
|
|
|
509
|
-
if (
|
|
510
|
-
return
|
|
530
|
+
if (items.length === 0) {
|
|
531
|
+
return null;
|
|
511
532
|
}
|
|
512
533
|
|
|
513
|
-
return
|
|
534
|
+
return {
|
|
535
|
+
...baseWidget,
|
|
536
|
+
show_count: settings?.show_count === true,
|
|
537
|
+
hierarchical: settings?.hierarchical === true,
|
|
538
|
+
items,
|
|
539
|
+
};
|
|
514
540
|
}
|
|
515
541
|
|
|
516
|
-
function
|
|
542
|
+
function resolveTagsWidget(baseWidget, settings, previewData) {
|
|
517
543
|
const countBySlug = buildTaxonomyCountMap(previewData.content.posts, 'tag_slugs');
|
|
518
544
|
const limit = clampInteger(settings?.limit, 20, 1, 100);
|
|
519
545
|
const items = previewData.content.tags
|
|
520
546
|
.map((tag) => ({
|
|
521
|
-
|
|
547
|
+
name: tag.name,
|
|
548
|
+
slug: tag.slug,
|
|
549
|
+
url: `/tags/${encodeSlugSegment(tag.slug)}/`,
|
|
522
550
|
count: countBySlug.get(tag.slug) || 0,
|
|
523
551
|
}))
|
|
524
552
|
.filter((tag) => tag.count > 0)
|
|
525
|
-
.slice(0, limit)
|
|
526
|
-
.map((tag) => {
|
|
527
|
-
const suffix = settings?.show_count === true ? ` <span class="widget-taxonomy__count">(${tag.count})</span>` : '';
|
|
528
|
-
return `<a href="/tags/${escapeHtml(encodeSlugSegment(tag.slug))}/" class="tag-link">${escapeHtml(tag.name)}${suffix}</a>`;
|
|
529
|
-
})
|
|
530
|
-
.join('');
|
|
553
|
+
.slice(0, limit);
|
|
531
554
|
|
|
532
|
-
if (
|
|
533
|
-
return
|
|
555
|
+
if (items.length === 0) {
|
|
556
|
+
return null;
|
|
534
557
|
}
|
|
535
558
|
|
|
536
|
-
return
|
|
559
|
+
return {
|
|
560
|
+
...baseWidget,
|
|
561
|
+
show_count: settings?.show_count === true,
|
|
562
|
+
items,
|
|
563
|
+
};
|
|
537
564
|
}
|
|
538
565
|
|
|
539
|
-
function
|
|
566
|
+
function resolveArchivesWidget(baseWidget, settings, previewData) {
|
|
540
567
|
const limit = clampInteger(settings?.limit, 12, 1, 120);
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
.map((entry) =>
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
}
|
|
568
|
+
const items = buildArchiveEntries(previewData.content.posts, previewData.site)
|
|
569
|
+
.slice(0, limit)
|
|
570
|
+
.map((entry) => ({
|
|
571
|
+
label: entry.label,
|
|
572
|
+
url: '/archive/',
|
|
573
|
+
count: entry.count,
|
|
574
|
+
year: entry.year,
|
|
575
|
+
month: entry.month,
|
|
576
|
+
meta: `${entry.count} posts`,
|
|
577
|
+
}));
|
|
552
578
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
if (!src) {
|
|
556
|
-
return '';
|
|
579
|
+
if (items.length === 0) {
|
|
580
|
+
return null;
|
|
557
581
|
}
|
|
558
582
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
const rel = target === '_blank' ? ' rel="noreferrer noopener"' : '';
|
|
564
|
-
const image = `<img class="widget-image__media" src="${escapeHtml(src)}" alt="${alt}">`;
|
|
565
|
-
const linkedImage = href ? `<a href="${escapeHtml(href)}" target="${target}"${rel}>${image}</a>` : image;
|
|
566
|
-
const captionHtml = caption ? `<figcaption class="widget-image__caption">${escapeHtml(caption)}</figcaption>` : '';
|
|
567
|
-
|
|
568
|
-
return renderWidgetCard(title, 'image', `<figure class="widget-image">${linkedImage}${captionHtml}</figure>`);
|
|
583
|
+
return {
|
|
584
|
+
...baseWidget,
|
|
585
|
+
items,
|
|
586
|
+
};
|
|
569
587
|
}
|
|
570
588
|
|
|
571
|
-
function
|
|
589
|
+
function resolveTextWidget(baseWidget, settings) {
|
|
572
590
|
const content = typeof settings?.content === 'string' ? settings.content : '';
|
|
573
591
|
const html = renderDocumentContent(content, normalizeDocumentType(settings?.document_type));
|
|
592
|
+
|
|
574
593
|
if (!normalizeOptionalString(html)) {
|
|
575
|
-
return
|
|
594
|
+
return null;
|
|
576
595
|
}
|
|
577
596
|
|
|
578
|
-
return
|
|
597
|
+
return {
|
|
598
|
+
...baseWidget,
|
|
599
|
+
html,
|
|
600
|
+
};
|
|
579
601
|
}
|
|
580
602
|
|
|
581
|
-
function
|
|
582
|
-
const
|
|
583
|
-
const items = links
|
|
603
|
+
function resolveLinkListWidget(baseWidget, settings) {
|
|
604
|
+
const items = (Array.isArray(settings?.links) ? settings.links : [])
|
|
584
605
|
.map((link) => {
|
|
585
606
|
const label = normalizeOptionalString(link?.label);
|
|
586
|
-
const url =
|
|
607
|
+
const url = normalizeThemeLinkUrl(link?.url);
|
|
587
608
|
if (!label || !url) {
|
|
588
|
-
return
|
|
609
|
+
return null;
|
|
589
610
|
}
|
|
590
611
|
|
|
591
612
|
const target = normalizeLinkTarget(link?.target);
|
|
592
|
-
|
|
593
|
-
|
|
613
|
+
return {
|
|
614
|
+
label,
|
|
615
|
+
url,
|
|
616
|
+
target,
|
|
617
|
+
rel: target === '_blank' ? 'noreferrer noopener' : '',
|
|
618
|
+
};
|
|
594
619
|
})
|
|
595
|
-
.filter(Boolean)
|
|
596
|
-
.join('');
|
|
620
|
+
.filter(Boolean);
|
|
597
621
|
|
|
598
|
-
if (
|
|
599
|
-
return
|
|
622
|
+
if (items.length === 0) {
|
|
623
|
+
return null;
|
|
600
624
|
}
|
|
601
625
|
|
|
602
|
-
return
|
|
626
|
+
return {
|
|
627
|
+
...baseWidget,
|
|
628
|
+
items,
|
|
629
|
+
};
|
|
603
630
|
}
|
|
604
631
|
|
|
605
|
-
function
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
` <label class="sr-only" for="${escapeHtml(searchId)}">${escapeHtml(title)}</label>`,
|
|
613
|
-
' <form class="search-theme-wrapper" data-search-form>',
|
|
614
|
-
` <input id="${escapeHtml(searchId)}" class="search-input" data-theme-search type="search" placeholder="${placeholder}" autocomplete="off" enterkeyhint="search">`,
|
|
615
|
-
` <button class="widget-search-button" type="submit">${buttonLabel}</button>`,
|
|
616
|
-
' </form>',
|
|
617
|
-
' <div class="search-results" data-search-results hidden></div>',
|
|
618
|
-
' <p class="search-feedback search-feedback--sidebar" data-search-feedback aria-live="polite"></p>',
|
|
619
|
-
'</div>',
|
|
620
|
-
].join('\n'));
|
|
632
|
+
function resolveSearchWidget(baseWidget, settings, widgetAreaId, index) {
|
|
633
|
+
return {
|
|
634
|
+
...baseWidget,
|
|
635
|
+
placeholder: normalizeNonEmptyString(settings?.placeholder, 'Search...'),
|
|
636
|
+
button_label: normalizeNonEmptyString(settings?.button_label, 'Search'),
|
|
637
|
+
dom_id: `widget-search-${widgetAreaId}-${index + 1}`,
|
|
638
|
+
};
|
|
621
639
|
}
|
|
622
640
|
|
|
623
|
-
function
|
|
641
|
+
function resolveProfileWidget(baseWidget, settings) {
|
|
624
642
|
const displayName = normalizeOptionalString(settings?.display_name);
|
|
625
643
|
const affiliation = normalizeOptionalString(settings?.affiliation);
|
|
626
|
-
const
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
if (!displayName && !bioShort && !avatar) {
|
|
630
|
-
return '';
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
const avatarHtml = avatar
|
|
634
|
-
? `<img class="widget-profile__avatar" src="${escapeHtml(avatar)}" alt="${escapeHtml(displayName || title)}">`
|
|
635
|
-
: '';
|
|
636
|
-
const affiliationHtml = affiliation ? `<p class="widget-profile__affiliation">${escapeHtml(affiliation)}</p>` : '';
|
|
637
|
-
const bioHtml = bioShort ? `<p class="sidebar-copy">${escapeHtml(bioShort)}</p>` : '';
|
|
638
|
-
|
|
639
|
-
return renderWidgetCard(title, 'profile', [
|
|
640
|
-
'<div class="widget-profile">',
|
|
641
|
-
avatarHtml,
|
|
642
|
-
' <div class="widget-profile__body">',
|
|
643
|
-
displayName ? ` <p class="widget-profile__name">${escapeHtml(displayName)}</p>` : '',
|
|
644
|
-
affiliationHtml ? ` ${affiliationHtml}` : '',
|
|
645
|
-
bioHtml ? ` ${bioHtml}` : '',
|
|
646
|
-
' </div>',
|
|
647
|
-
'</div>',
|
|
648
|
-
].filter(Boolean).join('\n'));
|
|
649
|
-
}
|
|
644
|
+
const bioText = normalizeOptionalString(settings?.bio_short);
|
|
645
|
+
const avatarUrl = normalizeMediaField(settings?.avatar);
|
|
650
646
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
return '';
|
|
647
|
+
if (!displayName && !affiliation && !bioText && !avatarUrl) {
|
|
648
|
+
return null;
|
|
654
649
|
}
|
|
655
650
|
|
|
656
|
-
return
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
651
|
+
return {
|
|
652
|
+
...baseWidget,
|
|
653
|
+
display_name: displayName,
|
|
654
|
+
affiliation,
|
|
655
|
+
avatar_url: avatarUrl,
|
|
656
|
+
bio_text: bioText,
|
|
657
|
+
};
|
|
663
658
|
}
|
|
664
659
|
|
|
665
660
|
function preparePage(page) {
|
|
@@ -672,66 +667,68 @@ function preparePage(page) {
|
|
|
672
667
|
};
|
|
673
668
|
}
|
|
674
669
|
|
|
675
|
-
function preparePost(post, site, authorsById, categoriesBySlug, tagsBySlug) {
|
|
670
|
+
function preparePost(post, site, authorsById, categoriesBySlug, tagsBySlug, themeSupportsComments) {
|
|
676
671
|
const documentType = normalizeDocumentType(post.document_type);
|
|
677
672
|
const html = renderDocumentContent(post.content, documentType);
|
|
678
673
|
const author = authorsById.get(post.author_id);
|
|
674
|
+
const categories = post.category_slugs
|
|
675
|
+
.map((slug) => categoriesBySlug.get(slug))
|
|
676
|
+
.filter(Boolean)
|
|
677
|
+
.map((category) => ({
|
|
678
|
+
name: category.name,
|
|
679
|
+
slug: category.slug,
|
|
680
|
+
url: `/categories/${encodeSlugSegment(category.slug)}/`,
|
|
681
|
+
}));
|
|
682
|
+
const tags = post.tag_slugs
|
|
683
|
+
.map((slug) => tagsBySlug.get(slug))
|
|
684
|
+
.filter(Boolean)
|
|
685
|
+
.map((tag) => ({
|
|
686
|
+
name: tag.name,
|
|
687
|
+
slug: tag.slug,
|
|
688
|
+
url: `/tags/${encodeSlugSegment(tag.slug)}/`,
|
|
689
|
+
}));
|
|
679
690
|
|
|
680
691
|
return {
|
|
681
|
-
|
|
692
|
+
public_id: post.public_id,
|
|
693
|
+
title: post.title,
|
|
694
|
+
slug: post.slug,
|
|
695
|
+
url: `/posts/${encodeSlugSegment(post.slug)}/`,
|
|
696
|
+
content: post.content,
|
|
682
697
|
document_type: documentType,
|
|
698
|
+
excerpt: post.excerpt,
|
|
699
|
+
published_at_iso: post.published_at_iso,
|
|
700
|
+
updated_at_iso: post.updated_at_iso,
|
|
701
|
+
author_id: post.author_id,
|
|
702
|
+
featured_image: post.featured_image,
|
|
703
|
+
status: post.status,
|
|
704
|
+
allow_comments: post.allow_comments,
|
|
705
|
+
category_slugs: post.category_slugs,
|
|
706
|
+
tag_slugs: post.tag_slugs,
|
|
707
|
+
author: {
|
|
708
|
+
id: post.author_id,
|
|
709
|
+
display_name: normalizeNonEmptyString(author?.display_name, post.author_id),
|
|
710
|
+
avatar: author?.avatar || '',
|
|
711
|
+
},
|
|
712
|
+
categories,
|
|
713
|
+
tags,
|
|
683
714
|
html,
|
|
684
|
-
author_name: normalizeNonEmptyString(author?.display_name, post.author_id),
|
|
685
|
-
author_avatar: author?.avatar,
|
|
686
715
|
published_at: formatTimestamp(post.published_at_iso, site),
|
|
687
716
|
updated_at: formatTimestamp(post.updated_at_iso, site),
|
|
688
717
|
reading_time: calculateReadingTime(html),
|
|
689
|
-
|
|
690
|
-
tags_html: renderInlineTaxonomyLinks(post.tag_slugs, tagsBySlug, 'tag'),
|
|
691
|
-
comments_html: renderCommentsContainer(site, post),
|
|
718
|
+
comments_enabled: themeSupportsComments && site.disallowComments !== true && post.allow_comments === true,
|
|
692
719
|
};
|
|
693
720
|
}
|
|
694
721
|
|
|
695
|
-
function
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
722
|
+
function buildCommentPolicyManifest(posts) {
|
|
723
|
+
const commentablePosts = posts
|
|
724
|
+
.filter((post) => post.status === 'published' && post.comments_enabled === true)
|
|
725
|
+
.map((post) => post.public_id)
|
|
726
|
+
.filter((value) => Number.isInteger(value) && value > 0);
|
|
699
727
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
hx-get="/api/comments/${safePostId}"
|
|
705
|
-
hx-trigger="load"
|
|
706
|
-
hx-swap="innerHTML">
|
|
707
|
-
<p class="zp-comments-loading">댓글을 불러오는 중...</p>
|
|
708
|
-
</div>
|
|
709
|
-
<form id="comment-form"
|
|
710
|
-
hx-post="/api/comments/${safePostId}"
|
|
711
|
-
hx-target="#comment-form-result"
|
|
712
|
-
hx-swap="innerHTML">
|
|
713
|
-
<div id="comment-form-result"></div>
|
|
714
|
-
<div>
|
|
715
|
-
<label for="author_name">이름</label>
|
|
716
|
-
<input type="text" id="author_name" name="author_name" required>
|
|
717
|
-
</div>
|
|
718
|
-
<div>
|
|
719
|
-
<label for="author_email">이메일</label>
|
|
720
|
-
<input type="email" id="author_email" name="author_email" required>
|
|
721
|
-
</div>
|
|
722
|
-
<div>
|
|
723
|
-
<label for="content">댓글</label>
|
|
724
|
-
<textarea id="content" name="content" required minlength="1" maxlength="5000"></textarea>
|
|
725
|
-
</div>
|
|
726
|
-
<div style="position:absolute;left:-9999px;" aria-hidden="true">
|
|
727
|
-
<input type="text" name="website" tabindex="-1" autocomplete="off">
|
|
728
|
-
</div>
|
|
729
|
-
<button type="submit">댓글 작성</button>
|
|
730
|
-
</form>
|
|
731
|
-
<noscript>
|
|
732
|
-
<p>댓글 기능을 사용하려면 JavaScript를 활성화해 주세요.</p>
|
|
733
|
-
</noscript>
|
|
734
|
-
</section>`;
|
|
728
|
+
return JSON.stringify({
|
|
729
|
+
version: 1,
|
|
730
|
+
commentable_posts: commentablePosts,
|
|
731
|
+
}, null, 2);
|
|
735
732
|
}
|
|
736
733
|
|
|
737
734
|
function buildTaxonomyRoutes(options) {
|
|
@@ -755,8 +752,8 @@ function buildTaxonomyRoutes(options) {
|
|
|
755
752
|
slug: item.slug,
|
|
756
753
|
page: entry.page,
|
|
757
754
|
totalPages: entry.totalPages,
|
|
758
|
-
posts:
|
|
759
|
-
pagination:
|
|
755
|
+
posts: buildStructuredPostCollection(entry.items, options.postBySlug),
|
|
756
|
+
pagination: buildStructuredPagination(entry.paginationData),
|
|
760
757
|
...options.renderExtras(item),
|
|
761
758
|
});
|
|
762
759
|
}
|
|
@@ -788,6 +785,12 @@ function buildPaginatedCollection(options) {
|
|
|
788
785
|
return pages;
|
|
789
786
|
}
|
|
790
787
|
|
|
788
|
+
function buildStructuredPostCollection(posts, postBySlug) {
|
|
789
|
+
return {
|
|
790
|
+
items: buildStructuredPostItems(posts, postBySlug),
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
|
|
791
794
|
function buildPaginatedPath(basePath, page) {
|
|
792
795
|
const normalizedBasePath = normalizePaginationBasePath(basePath);
|
|
793
796
|
if (page <= 1) {
|
|
@@ -828,124 +831,99 @@ function buildPaginationData(currentPage, totalPages, totalPosts, basePath) {
|
|
|
828
831
|
};
|
|
829
832
|
}
|
|
830
833
|
|
|
831
|
-
function
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
834
|
+
function buildStructuredPagination(paginationData) {
|
|
835
|
+
return {
|
|
836
|
+
current_page: paginationData.currentPage,
|
|
837
|
+
total_pages: paginationData.totalPages,
|
|
838
|
+
total_items: paginationData.totalPosts,
|
|
839
|
+
has_prev: paginationData.hasPrev,
|
|
840
|
+
has_next: paginationData.hasNext,
|
|
841
|
+
has_multiple_pages: paginationData.totalPages > 1,
|
|
842
|
+
prev_url: paginationData.prevUrl || '',
|
|
843
|
+
next_url: paginationData.nextUrl || '',
|
|
844
|
+
pages: paginationData.pages.map((page) => ({
|
|
845
|
+
number: page.number,
|
|
846
|
+
url: page.url,
|
|
847
|
+
current: page.current,
|
|
848
|
+
})),
|
|
849
|
+
};
|
|
850
|
+
}
|
|
835
851
|
|
|
852
|
+
function buildStructuredPostItems(posts, postBySlug) {
|
|
836
853
|
return posts
|
|
837
|
-
.map((post) =>
|
|
838
|
-
const prepared = postBySlug.get(post.slug);
|
|
839
|
-
if (!prepared) {
|
|
840
|
-
return '';
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
return [
|
|
844
|
-
'<article class="post-list-item">',
|
|
845
|
-
` <h2><a href="/posts/${escapeHtml(encodeSlugSegment(post.slug))}/">${escapeHtml(post.title)}</a></h2>`,
|
|
846
|
-
` <div class="post-meta"><time datetime="${escapeHtml(prepared.published_at_iso)}">${escapeHtml(prepared.published_at)}</time> • ${escapeHtml(prepared.reading_time)}</div>`,
|
|
847
|
-
` <div class="post-excerpt">${escapeHtml(prepared.excerpt)}</div>`,
|
|
848
|
-
'</article>',
|
|
849
|
-
].join('\n');
|
|
850
|
-
})
|
|
854
|
+
.map((post) => postBySlug.get(post.slug))
|
|
851
855
|
.filter(Boolean)
|
|
852
|
-
.
|
|
856
|
+
.map((post) => buildStructuredPostSummary(post));
|
|
853
857
|
}
|
|
854
858
|
|
|
855
|
-
function
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
.map((
|
|
871
|
-
|
|
872
|
-
.map((post) => {
|
|
873
|
-
const prepared = postBySlug.get(post.slug);
|
|
874
|
-
if (!prepared) {
|
|
875
|
-
return '';
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
return `<li><a href="/posts/${escapeHtml(encodeSlugSegment(post.slug))}/">${escapeHtml(post.title)}</a> <time datetime="${escapeHtml(prepared.published_at_iso)}">${escapeHtml(prepared.published_at)}</time></li>`;
|
|
879
|
-
})
|
|
880
|
-
.filter(Boolean)
|
|
881
|
-
.join('\n');
|
|
882
|
-
|
|
883
|
-
return `<section class="archive-group"><h2>${escapeHtml(key)}</h2><ul>${items}</ul></section>`;
|
|
884
|
-
})
|
|
885
|
-
.join('\n');
|
|
859
|
+
function buildStructuredPostSummary(post) {
|
|
860
|
+
return {
|
|
861
|
+
title: post.title,
|
|
862
|
+
slug: post.slug,
|
|
863
|
+
url: post.url,
|
|
864
|
+
excerpt: post.excerpt,
|
|
865
|
+
published_at: post.published_at,
|
|
866
|
+
published_at_iso: post.published_at_iso,
|
|
867
|
+
reading_time: post.reading_time,
|
|
868
|
+
featured_image: post.featured_image,
|
|
869
|
+
author: {
|
|
870
|
+
display_name: post.author?.display_name || '',
|
|
871
|
+
avatar: post.author?.avatar || '',
|
|
872
|
+
},
|
|
873
|
+
categories: Array.isArray(post.categories) ? post.categories.map((category) => ({ ...category })) : [],
|
|
874
|
+
tags: Array.isArray(post.tags) ? post.tags.map((tag) => ({ ...tag })) : [],
|
|
875
|
+
};
|
|
886
876
|
}
|
|
887
877
|
|
|
888
|
-
function
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
if (count === 0) {
|
|
893
|
-
return null;
|
|
894
|
-
}
|
|
878
|
+
function buildAdjacentPostSummary(post) {
|
|
879
|
+
if (!post) {
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
895
882
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
.
|
|
899
|
-
.
|
|
883
|
+
return {
|
|
884
|
+
title: post.title,
|
|
885
|
+
slug: post.slug,
|
|
886
|
+
url: post.url,
|
|
887
|
+
excerpt: post.excerpt,
|
|
888
|
+
published_at: post.published_at,
|
|
889
|
+
published_at_iso: post.published_at_iso,
|
|
890
|
+
};
|
|
900
891
|
}
|
|
901
892
|
|
|
902
|
-
function
|
|
903
|
-
|
|
904
|
-
.map((tag) => {
|
|
905
|
-
const count = countBySlug.get(tag.slug) || 0;
|
|
906
|
-
if (count === 0) {
|
|
907
|
-
return null;
|
|
908
|
-
}
|
|
893
|
+
function buildArchiveGroups(posts, postBySlug) {
|
|
894
|
+
const groups = new Map();
|
|
909
895
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
}
|
|
896
|
+
for (const post of posts) {
|
|
897
|
+
const prepared = postBySlug.get(post.slug);
|
|
898
|
+
if (!prepared?.published_at_iso) {
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
915
901
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
.
|
|
919
|
-
.
|
|
920
|
-
.
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
902
|
+
const date = toDate(prepared.published_at_iso);
|
|
903
|
+
const year = date.getUTCFullYear();
|
|
904
|
+
const month = date.getUTCMonth() + 1;
|
|
905
|
+
const label = `${year}-${String(month).padStart(2, '0')}`;
|
|
906
|
+
const current = groups.get(label) || {
|
|
907
|
+
label,
|
|
908
|
+
year,
|
|
909
|
+
month,
|
|
910
|
+
items: [],
|
|
911
|
+
};
|
|
925
912
|
|
|
926
|
-
|
|
927
|
-
|
|
913
|
+
current.items.push(buildStructuredPostSummary(prepared));
|
|
914
|
+
groups.set(label, current);
|
|
928
915
|
}
|
|
929
916
|
|
|
930
|
-
return
|
|
917
|
+
return Array.from(groups.values());
|
|
931
918
|
}
|
|
932
919
|
|
|
933
|
-
function
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
: '';
|
|
941
|
-
const nextLink = paginationData.hasNext && paginationData.nextUrl
|
|
942
|
-
? `<a href="${paginationData.nextUrl}" class="next">Next</a>`
|
|
943
|
-
: '';
|
|
944
|
-
const pageLinks = paginationData.pages
|
|
945
|
-
.map((page) => `<a href="${page.url}" class="page-link ${page.current ? 'current' : ''}">${page.number}</a>`)
|
|
946
|
-
.join(' ');
|
|
947
|
-
|
|
948
|
-
return `<nav class="pagination">\n ${prevLink}\n <span class="pages">${pageLinks}</span>\n ${nextLink}\n</nav>`;
|
|
920
|
+
function buildTaxonomyRouteData(kind, item, countBySlug) {
|
|
921
|
+
return {
|
|
922
|
+
kind,
|
|
923
|
+
slug: item.slug,
|
|
924
|
+
name: item.name,
|
|
925
|
+
count: countBySlug.get(item.slug) || 0,
|
|
926
|
+
};
|
|
949
927
|
}
|
|
950
928
|
|
|
951
929
|
function buildTaxonomyCountMap(posts, fieldName) {
|
|
@@ -982,6 +960,8 @@ function buildArchiveEntries(posts, site) {
|
|
|
982
960
|
.map((entry) => ({
|
|
983
961
|
label: formatArchiveLabel(entry.date, site),
|
|
984
962
|
count: entry.count,
|
|
963
|
+
year: entry.date.getUTCFullYear(),
|
|
964
|
+
month: entry.date.getUTCMonth() + 1,
|
|
985
965
|
}));
|
|
986
966
|
}
|
|
987
967
|
|
|
@@ -1115,7 +1095,7 @@ function recordRouteEmission(state, templateName, route, currentUrl) {
|
|
|
1115
1095
|
});
|
|
1116
1096
|
}
|
|
1117
1097
|
|
|
1118
|
-
async function
|
|
1098
|
+
async function normalizeAndValidateThemePackage(themePackage) {
|
|
1119
1099
|
if (!themePackage?.templates || !themePackage?.partials || !themePackage?.assets || !themePackage?.metadata) {
|
|
1120
1100
|
throw new Error('Invalid themePackage: expected metadata, templates, partials, and assets');
|
|
1121
1101
|
}
|
|
@@ -1131,6 +1111,7 @@ async function assertValidThemePackage(themePackage) {
|
|
|
1131
1111
|
...(themePackage.metadata.author ? { author: themePackage.metadata.author } : {}),
|
|
1132
1112
|
...(themePackage.metadata.description ? { description: themePackage.metadata.description } : {}),
|
|
1133
1113
|
...(themePackage.metadata.thumbnail ? { thumbnail: themePackage.metadata.thumbnail } : {}),
|
|
1114
|
+
...(themePackage.metadata.features ? { features: themePackage.metadata.features } : {}),
|
|
1134
1115
|
...(themePackage.metadata.menuSlots ? { menuSlots: themePackage.metadata.menuSlots } : {}),
|
|
1135
1116
|
...(themePackage.metadata.widgetAreas ? { widgetAreas: themePackage.metadata.widgetAreas } : {}),
|
|
1136
1117
|
settings: themePackage.metadata.settings || {},
|
|
@@ -1152,6 +1133,25 @@ async function assertValidThemePackage(themePackage) {
|
|
|
1152
1133
|
if (!validation.ok) {
|
|
1153
1134
|
throw new Error(`Theme validation failed: ${validation.errors[0]?.message || 'Unknown error'}`);
|
|
1154
1135
|
}
|
|
1136
|
+
|
|
1137
|
+
if (!validation.manifest) {
|
|
1138
|
+
throw new Error('Theme validation failed: normalized manifest not available');
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
return {
|
|
1142
|
+
metadata: normalizeThemePackageMetadata(themePackage.metadata, validation.manifest),
|
|
1143
|
+
templates: themePackage.templates,
|
|
1144
|
+
partials: themePackage.partials,
|
|
1145
|
+
assets: themePackage.assets,
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function normalizeThemePackageMetadata(sourceMetadata, manifest) {
|
|
1150
|
+
return {
|
|
1151
|
+
...manifest,
|
|
1152
|
+
...(sourceMetadata?.thumbnail ? { thumbnail: sourceMetadata.thumbnail } : {}),
|
|
1153
|
+
settings: sourceMetadata?.settings || {},
|
|
1154
|
+
};
|
|
1155
1155
|
}
|
|
1156
1156
|
|
|
1157
1157
|
async function buildAssetOutputs(assets, assetProcessor, options) {
|
|
@@ -1292,7 +1292,7 @@ function resolveMetaImageUrl(image) {
|
|
|
1292
1292
|
}
|
|
1293
1293
|
|
|
1294
1294
|
if (isAbsoluteUrl(normalizedImage)) {
|
|
1295
|
-
return normalizedImage;
|
|
1295
|
+
return normalizeAbsoluteUrl(normalizedImage, SAFE_MEDIA_PROTOCOLS);
|
|
1296
1296
|
}
|
|
1297
1297
|
|
|
1298
1298
|
return '';
|
|
@@ -1309,7 +1309,7 @@ function normalizeMediaField(value, mediaBaseUrl) {
|
|
|
1309
1309
|
}
|
|
1310
1310
|
|
|
1311
1311
|
if (isAbsoluteUrl(normalizedValue)) {
|
|
1312
|
-
return normalizedValue;
|
|
1312
|
+
return normalizeAbsoluteUrl(normalizedValue, SAFE_MEDIA_PROTOCOLS);
|
|
1313
1313
|
}
|
|
1314
1314
|
|
|
1315
1315
|
const normalizedBaseUrl = normalizeOptionalString(mediaBaseUrl);
|
|
@@ -1333,6 +1333,31 @@ function isAbsoluteUrl(value) {
|
|
|
1333
1333
|
}
|
|
1334
1334
|
}
|
|
1335
1335
|
|
|
1336
|
+
function normalizeAbsoluteUrl(value, allowedProtocols) {
|
|
1337
|
+
try {
|
|
1338
|
+
const url = new URL(value);
|
|
1339
|
+
if (!allowedProtocols.has(url.protocol)) {
|
|
1340
|
+
return '';
|
|
1341
|
+
}
|
|
1342
|
+
return decodeURI(url.toString());
|
|
1343
|
+
} catch {
|
|
1344
|
+
return '';
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
function normalizeThemeLinkUrl(value) {
|
|
1349
|
+
const normalizedValue = normalizeOptionalString(value);
|
|
1350
|
+
if (!normalizedValue || normalizedValue.startsWith('//')) {
|
|
1351
|
+
return '';
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
if (isAbsoluteUrl(normalizedValue)) {
|
|
1355
|
+
return normalizeAbsoluteUrl(normalizedValue, SAFE_LINK_PROTOCOLS);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
return normalizedValue;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1336
1361
|
async function writeOutput(writer, summaries, path, content, contentType) {
|
|
1337
1362
|
const rawPath = String(path || '');
|
|
1338
1363
|
const normalizedPath = normalizeOutputPath(rawPath);
|
|
@@ -1391,6 +1416,7 @@ function assertPlannedOutputPathsSafe(state) {
|
|
|
1391
1416
|
...state.renderData.posts.map((post) => `posts/${encodeSlugSegment(post.slug)}/index.html`),
|
|
1392
1417
|
...state.renderData.pages.map((page) => `${encodeSlugSegment(page.slug)}/index.html`),
|
|
1393
1418
|
...state.assetOutputs.map((assetOutput) => assetOutput.path),
|
|
1419
|
+
COMMENT_POLICY_OUTPUT_PATH,
|
|
1394
1420
|
];
|
|
1395
1421
|
|
|
1396
1422
|
if (state.options.generateSpecialFiles) {
|
|
@@ -1465,10 +1491,6 @@ function sha256(content) {
|
|
|
1465
1491
|
return hash.digest('hex');
|
|
1466
1492
|
}
|
|
1467
1493
|
|
|
1468
|
-
function injectHtmxScript(html) {
|
|
1469
|
-
return html.replace('</body>', '<script src="https://unpkg.com/htmx.org@2.0.4" crossorigin="anonymous"></script>\n</body>');
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
1494
|
function injectCustomCssAssetLink(html, href) {
|
|
1473
1495
|
if (!normalizeOptionalString(href)) {
|
|
1474
1496
|
return html;
|