@zeropress/build-core 0.1.1 → 0.1.3
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 +37 -0
- package/package.json +2 -2
- package/src/api/build-site.js +582 -62
- package/src/index.d.ts +4 -4
package/README.md
CHANGED
|
@@ -13,6 +13,11 @@ This package is the deterministic rendering engine used by:
|
|
|
13
13
|
|
|
14
14
|
It accepts canonical preview-data plus a validated theme package and produces static HTML artifacts through a writer interface.
|
|
15
15
|
|
|
16
|
+
`preview-data` stays canonical and data-only. Build-core computes the render-ready route state that themes consume at render time. Actual artifact output is the intersection of:
|
|
17
|
+
|
|
18
|
+
- renderable preview-data entries
|
|
19
|
+
- theme template capability
|
|
20
|
+
|
|
16
21
|
---
|
|
17
22
|
|
|
18
23
|
## Install
|
|
@@ -43,6 +48,8 @@ import {
|
|
|
43
48
|
|
|
44
49
|
- validating preview-data input
|
|
45
50
|
- validating in-memory theme packages
|
|
51
|
+
- computing paginated routes from content data
|
|
52
|
+
- computing theme-facing render data such as post lists, taxonomy links, pagination, and formatted timestamps
|
|
46
53
|
- rendering route HTML
|
|
47
54
|
- processing theme assets
|
|
48
55
|
- generating special files such as:
|
|
@@ -109,6 +116,15 @@ Renders only selected post and list routes while preserving full-build parity fo
|
|
|
109
116
|
|
|
110
117
|
This is intended for selective publish and other partial-render workflows.
|
|
111
118
|
|
|
119
|
+
Optional route templates are respected during both full and partial renders:
|
|
120
|
+
|
|
121
|
+
- missing `archive.html` skips archive outputs
|
|
122
|
+
- missing `category.html` skips category outputs
|
|
123
|
+
- missing `tag.html` skips tag outputs
|
|
124
|
+
- missing `404.html` skips `404.html` emission
|
|
125
|
+
|
|
126
|
+
These cases do not fail the build.
|
|
127
|
+
|
|
112
128
|
```js
|
|
113
129
|
await buildSelectedRoutes({
|
|
114
130
|
previewData,
|
|
@@ -177,6 +193,27 @@ Theme validation is enforced through:
|
|
|
177
193
|
|
|
178
194
|
- [`@zeropress/theme-validator`](https://www.npmjs.com/package/@zeropress/theme-validator)
|
|
179
195
|
|
|
196
|
+
As of `preview-data v0.4`, the payload no longer carries `routes` arrays or raw HTML fragments such as `categories_html`.
|
|
197
|
+
|
|
198
|
+
Build-core now derives:
|
|
199
|
+
|
|
200
|
+
- index/archive/category/tag routes
|
|
201
|
+
- post list HTML blocks
|
|
202
|
+
- pagination HTML
|
|
203
|
+
- taxonomy link HTML
|
|
204
|
+
- formatted `published_at` / `updated_at`
|
|
205
|
+
- `reading_time`
|
|
206
|
+
- `comments_html`
|
|
207
|
+
|
|
208
|
+
Optional route templates behave as rendering capabilities, not guaranteed outputs:
|
|
209
|
+
|
|
210
|
+
- `archive.html`
|
|
211
|
+
- `category.html`
|
|
212
|
+
- `tag.html`
|
|
213
|
+
- `404.html`
|
|
214
|
+
|
|
215
|
+
If preview-data includes content that could produce archive/category/tag pages but the theme omits the matching optional template, build-core skips those outputs. Special files are derived from emitted outputs rather than raw preview-data alone.
|
|
216
|
+
|
|
180
217
|
---
|
|
181
218
|
|
|
182
219
|
## Build Options
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeropress/build-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Shared ZeroPress build and partial-render core",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"node": ">=18.18.0"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@zeropress/preview-data-validator": "^0.
|
|
36
|
+
"@zeropress/preview-data-validator": "^0.3.0",
|
|
37
37
|
"@zeropress/theme-validator": "^0.2.2"
|
|
38
38
|
}
|
|
39
39
|
}
|
package/src/api/build-site.js
CHANGED
|
@@ -11,32 +11,45 @@ const DEFAULT_OPTIONS = {
|
|
|
11
11
|
writeManifest: false,
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
const DEFAULT_POSTS_PER_PAGE = 10;
|
|
15
|
+
const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD';
|
|
16
|
+
const DEFAULT_TIME_FORMAT = 'HH:mm';
|
|
17
|
+
const DEFAULT_TIMEZONE = 'UTC';
|
|
18
|
+
const DEFAULT_LOCALE = 'en-US';
|
|
19
|
+
const COMMENTS_HTML = '<section id="comments"></section>';
|
|
20
|
+
|
|
14
21
|
export async function buildSite(input) {
|
|
15
22
|
const options = { ...DEFAULT_OPTIONS, ...(input.options || {}) };
|
|
16
23
|
const state = await createBuildState(input, options);
|
|
17
24
|
|
|
18
|
-
for (const route of
|
|
25
|
+
for (const route of state.renderData.indexRoutes) {
|
|
19
26
|
await renderRoute(state, 'index', route);
|
|
20
27
|
}
|
|
21
28
|
|
|
22
|
-
for (const post of
|
|
29
|
+
for (const post of state.renderData.posts) {
|
|
23
30
|
await renderPost(state, post);
|
|
24
31
|
}
|
|
25
32
|
|
|
26
|
-
for (const page of
|
|
33
|
+
for (const page of state.previewData.content.pages) {
|
|
27
34
|
await renderPage(state, page);
|
|
28
35
|
}
|
|
29
36
|
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
if (hasTemplate(state, 'category')) {
|
|
38
|
+
for (const route of state.renderData.categoryRoutes) {
|
|
39
|
+
await renderRoute(state, 'category', route);
|
|
40
|
+
}
|
|
32
41
|
}
|
|
33
42
|
|
|
34
|
-
|
|
35
|
-
|
|
43
|
+
if (hasTemplate(state, 'tag')) {
|
|
44
|
+
for (const route of state.renderData.tagRoutes) {
|
|
45
|
+
await renderRoute(state, 'tag', route);
|
|
46
|
+
}
|
|
36
47
|
}
|
|
37
48
|
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
if (hasTemplate(state, 'archive')) {
|
|
50
|
+
for (const route of state.renderData.archiveRoutes) {
|
|
51
|
+
await renderRoute(state, 'archive', route);
|
|
52
|
+
}
|
|
40
53
|
}
|
|
41
54
|
|
|
42
55
|
for (const assetOutput of state.assetOutputs) {
|
|
@@ -44,10 +57,11 @@ export async function buildSite(input) {
|
|
|
44
57
|
}
|
|
45
58
|
|
|
46
59
|
if (options.generateSpecialFiles) {
|
|
47
|
-
await
|
|
48
|
-
await writeOutput(state.writer, state.summaries, '
|
|
49
|
-
await writeOutput(state.writer, state.summaries, '
|
|
50
|
-
await writeOutput(state.writer, state.summaries, '
|
|
60
|
+
await maybeRenderNotFoundPage(state);
|
|
61
|
+
await writeOutput(state.writer, state.summaries, 'sitemap.xml', buildSitemapXml(state.previewData.site, state.emitted, state.generatedAt), 'application/xml');
|
|
62
|
+
await writeOutput(state.writer, state.summaries, 'feed.xml', buildFeedXml(state.previewData.site, state.emitted, state.generatedAt), 'application/rss+xml');
|
|
63
|
+
await writeOutput(state.writer, state.summaries, 'robots.txt', buildRobotsTxt(state.previewData.site), 'text/plain');
|
|
64
|
+
await writeOutput(state.writer, state.summaries, 'meta.json', buildMetaJson(state.previewData.site, state.emitted, state.generatedAt), 'application/json');
|
|
51
65
|
}
|
|
52
66
|
|
|
53
67
|
return finalizeBuildResult(state.writer, state.summaries, options);
|
|
@@ -64,33 +78,39 @@ export async function buildSelectedRoutes(input) {
|
|
|
64
78
|
const selectedCategoryRoutes = new Set((input.selection.categoryRoutes || []).map((route) => normalizeRoutePath(route)));
|
|
65
79
|
const selectedTagRoutes = new Set((input.selection.tagRoutes || []).map((route) => normalizeRoutePath(route)));
|
|
66
80
|
|
|
67
|
-
for (const post of
|
|
81
|
+
for (const post of state.renderData.posts) {
|
|
68
82
|
if (selectedPostSlugs.has(post.slug)) {
|
|
69
83
|
await renderPost(state, post);
|
|
70
84
|
}
|
|
71
85
|
}
|
|
72
86
|
|
|
73
|
-
for (const route of
|
|
87
|
+
for (const route of state.renderData.indexRoutes) {
|
|
74
88
|
if (selectedIndexRoutes.has(normalizeRoutePath(route.path))) {
|
|
75
89
|
await renderRoute(state, 'index', route);
|
|
76
90
|
}
|
|
77
91
|
}
|
|
78
92
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
93
|
+
if (hasTemplate(state, 'archive')) {
|
|
94
|
+
for (const route of state.renderData.archiveRoutes) {
|
|
95
|
+
if (selectedArchiveRoutes.has(normalizeRoutePath(route.path))) {
|
|
96
|
+
await renderRoute(state, 'archive', route);
|
|
97
|
+
}
|
|
82
98
|
}
|
|
83
99
|
}
|
|
84
100
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
101
|
+
if (hasTemplate(state, 'category')) {
|
|
102
|
+
for (const route of state.renderData.categoryRoutes) {
|
|
103
|
+
if (selectedCategoryRoutes.has(normalizeRoutePath(route.path))) {
|
|
104
|
+
await renderRoute(state, 'category', route);
|
|
105
|
+
}
|
|
88
106
|
}
|
|
89
107
|
}
|
|
90
108
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
109
|
+
if (hasTemplate(state, 'tag')) {
|
|
110
|
+
for (const route of state.renderData.tagRoutes) {
|
|
111
|
+
if (selectedTagRoutes.has(normalizeRoutePath(route.path))) {
|
|
112
|
+
await renderRoute(state, 'tag', route);
|
|
113
|
+
}
|
|
94
114
|
}
|
|
95
115
|
}
|
|
96
116
|
|
|
@@ -115,6 +135,7 @@ async function createBuildState(input, options) {
|
|
|
115
135
|
const assetProcessor = new AssetProcessor();
|
|
116
136
|
const summaries = [];
|
|
117
137
|
const themePackage = input.themePackage;
|
|
138
|
+
const previewData = normalizePreviewData(input.previewData);
|
|
118
139
|
|
|
119
140
|
engine.initialize(themePackage);
|
|
120
141
|
|
|
@@ -125,13 +146,23 @@ async function createBuildState(input, options) {
|
|
|
125
146
|
|
|
126
147
|
return {
|
|
127
148
|
writer: input.writer,
|
|
128
|
-
previewData
|
|
149
|
+
previewData,
|
|
150
|
+
renderData: createRenderData(previewData),
|
|
129
151
|
engine,
|
|
130
152
|
assetProcessor,
|
|
131
153
|
summaries,
|
|
132
154
|
assetOutputs,
|
|
133
155
|
assetMap,
|
|
134
156
|
options,
|
|
157
|
+
generatedAt: new Date(),
|
|
158
|
+
emitted: {
|
|
159
|
+
indexRoutes: [],
|
|
160
|
+
archiveRoutes: [],
|
|
161
|
+
categoryRoutes: [],
|
|
162
|
+
tagRoutes: [],
|
|
163
|
+
posts: [],
|
|
164
|
+
pages: [],
|
|
165
|
+
},
|
|
135
166
|
};
|
|
136
167
|
}
|
|
137
168
|
|
|
@@ -166,19 +197,29 @@ async function renderRoute(state, templateName, route) {
|
|
|
166
197
|
);
|
|
167
198
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
168
199
|
await writeOutput(state.writer, state.summaries, routePathToOutputPath(route.path), html, 'text/html');
|
|
200
|
+
recordRouteEmission(state, templateName, route, currentUrl);
|
|
169
201
|
}
|
|
170
202
|
|
|
171
203
|
async function renderPost(state, post) {
|
|
204
|
+
const currentUrl = `/posts/${encodeSlugSegment(post.slug)}/`;
|
|
172
205
|
let html = await state.engine.render(
|
|
173
206
|
'post',
|
|
174
207
|
{ post },
|
|
175
|
-
createRenderContext(state.previewData.site,
|
|
208
|
+
createRenderContext(state.previewData.site, currentUrl),
|
|
176
209
|
);
|
|
177
210
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
178
211
|
if (state.options.injectHtmx) {
|
|
179
212
|
html = injectHtmxScript(html);
|
|
180
213
|
}
|
|
181
214
|
await writeOutput(state.writer, state.summaries, `posts/${encodeSlugSegment(post.slug)}/index.html`, html, 'text/html');
|
|
215
|
+
state.emitted.posts.push({
|
|
216
|
+
url: currentUrl,
|
|
217
|
+
title: post.title,
|
|
218
|
+
description: post.excerpt,
|
|
219
|
+
publishedAt: post.published_at_iso,
|
|
220
|
+
updatedAt: post.updated_at_iso,
|
|
221
|
+
status: post.status,
|
|
222
|
+
});
|
|
182
223
|
}
|
|
183
224
|
|
|
184
225
|
async function renderPage(state, page) {
|
|
@@ -189,6 +230,25 @@ async function renderPage(state, page) {
|
|
|
189
230
|
);
|
|
190
231
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
191
232
|
await writeOutput(state.writer, state.summaries, `${encodeSlugSegment(page.slug)}/index.html`, html, 'text/html');
|
|
233
|
+
state.emitted.pages.push({
|
|
234
|
+
url: `/${encodeSlugSegment(page.slug)}/`,
|
|
235
|
+
title: page.title,
|
|
236
|
+
status: page.status,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function maybeRenderNotFoundPage(state) {
|
|
241
|
+
if (!state.engine.themePackage?.templates?.has('404')) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let html = await state.engine.render(
|
|
246
|
+
'404',
|
|
247
|
+
{},
|
|
248
|
+
createRenderContext(state.previewData.site, '/404.html'),
|
|
249
|
+
);
|
|
250
|
+
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
251
|
+
await writeOutput(state.writer, state.summaries, '404.html', html, 'text/html');
|
|
192
252
|
}
|
|
193
253
|
|
|
194
254
|
function validateSelection(selection) {
|
|
@@ -208,6 +268,440 @@ function validateSelection(selection) {
|
|
|
208
268
|
}
|
|
209
269
|
}
|
|
210
270
|
|
|
271
|
+
function normalizePreviewData(previewData) {
|
|
272
|
+
return {
|
|
273
|
+
...previewData,
|
|
274
|
+
site: {
|
|
275
|
+
...previewData.site,
|
|
276
|
+
postsPerPage: Number.isInteger(previewData.site.postsPerPage) && previewData.site.postsPerPage > 0
|
|
277
|
+
? previewData.site.postsPerPage
|
|
278
|
+
: DEFAULT_POSTS_PER_PAGE,
|
|
279
|
+
dateFormat: normalizeNonEmptyString(previewData.site.dateFormat, DEFAULT_DATE_FORMAT),
|
|
280
|
+
timeFormat: typeof previewData.site.timeFormat === 'string' ? previewData.site.timeFormat : DEFAULT_TIME_FORMAT,
|
|
281
|
+
siteTimezone: normalizeNonEmptyString(previewData.site.siteTimezone, DEFAULT_TIMEZONE),
|
|
282
|
+
siteLocale: normalizeLocale(previewData.site.siteLocale || previewData.site.language || DEFAULT_LOCALE),
|
|
283
|
+
},
|
|
284
|
+
content: {
|
|
285
|
+
...previewData.content,
|
|
286
|
+
posts: [...previewData.content.posts].sort((left, right) => toDate(right.published_at_iso).getTime() - toDate(left.published_at_iso).getTime()),
|
|
287
|
+
pages: [...previewData.content.pages],
|
|
288
|
+
categories: [...previewData.content.categories],
|
|
289
|
+
tags: [...previewData.content.tags],
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function createRenderData(previewData) {
|
|
295
|
+
const categoriesBySlug = new Map(previewData.content.categories.map((category) => [category.slug, category]));
|
|
296
|
+
const tagsBySlug = new Map(previewData.content.tags.map((tag) => [tag.slug, tag]));
|
|
297
|
+
const categoryPostsBySlug = new Map();
|
|
298
|
+
const tagPostsBySlug = new Map();
|
|
299
|
+
const categoryCountBySlug = new Map();
|
|
300
|
+
const tagCountBySlug = new Map();
|
|
301
|
+
|
|
302
|
+
for (const post of previewData.content.posts) {
|
|
303
|
+
for (const slug of post.category_slugs) {
|
|
304
|
+
pushToSlugMap(categoryPostsBySlug, slug, post);
|
|
305
|
+
categoryCountBySlug.set(slug, (categoryCountBySlug.get(slug) || 0) + 1);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
for (const slug of post.tag_slugs) {
|
|
309
|
+
pushToSlugMap(tagPostsBySlug, slug, post);
|
|
310
|
+
tagCountBySlug.set(slug, (tagCountBySlug.get(slug) || 0) + 1);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const posts = previewData.content.posts.map((post) => preparePost(post, previewData.site, categoriesBySlug, tagsBySlug));
|
|
315
|
+
const postBySlug = new Map(posts.map((post) => [post.slug, post]));
|
|
316
|
+
const categoryLinks = renderCategoryLinks(previewData.content.categories, categoryCountBySlug);
|
|
317
|
+
const tagLinks = renderTagLinks(previewData.content.tags, tagCountBySlug);
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
posts,
|
|
321
|
+
postBySlug,
|
|
322
|
+
indexRoutes: buildPaginatedCollection({
|
|
323
|
+
items: previewData.content.posts,
|
|
324
|
+
postsPerPage: previewData.site.postsPerPage,
|
|
325
|
+
basePath: '/',
|
|
326
|
+
}).map((entry) => ({
|
|
327
|
+
path: entry.path,
|
|
328
|
+
page: entry.page,
|
|
329
|
+
totalPages: entry.totalPages,
|
|
330
|
+
posts: renderPostList(entry.items, postBySlug),
|
|
331
|
+
categories: categoryLinks,
|
|
332
|
+
tags: tagLinks,
|
|
333
|
+
pagination: renderPagination(entry.paginationData),
|
|
334
|
+
})),
|
|
335
|
+
archiveRoutes: buildPaginatedCollection({
|
|
336
|
+
items: previewData.content.posts,
|
|
337
|
+
postsPerPage: previewData.site.postsPerPage,
|
|
338
|
+
basePath: '/archive/',
|
|
339
|
+
}).map((entry) => ({
|
|
340
|
+
path: entry.path,
|
|
341
|
+
page: entry.page,
|
|
342
|
+
totalPages: entry.totalPages,
|
|
343
|
+
posts: renderArchive(entry.items, postBySlug),
|
|
344
|
+
pagination: renderPagination(entry.paginationData),
|
|
345
|
+
})),
|
|
346
|
+
categoryRoutes: buildTaxonomyRoutes({
|
|
347
|
+
items: previewData.content.categories,
|
|
348
|
+
postsBySlug: categoryPostsBySlug,
|
|
349
|
+
postsPerPage: previewData.site.postsPerPage,
|
|
350
|
+
buildBasePath: (category) => `/categories/${encodeSlugSegment(category.slug)}/`,
|
|
351
|
+
renderList: (postsForRoute) => renderPostList(postsForRoute, postBySlug),
|
|
352
|
+
renderExtras: () => ({ categories: categoryLinks }),
|
|
353
|
+
}),
|
|
354
|
+
tagRoutes: buildTaxonomyRoutes({
|
|
355
|
+
items: previewData.content.tags,
|
|
356
|
+
postsBySlug: tagPostsBySlug,
|
|
357
|
+
postsPerPage: previewData.site.postsPerPage,
|
|
358
|
+
buildBasePath: (tag) => `/tags/${encodeSlugSegment(tag.slug)}/`,
|
|
359
|
+
renderList: (postsForRoute) => renderPostList(postsForRoute, postBySlug),
|
|
360
|
+
renderExtras: () => ({ tags: tagLinks }),
|
|
361
|
+
}),
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function preparePost(post, site, categoriesBySlug, tagsBySlug) {
|
|
366
|
+
return {
|
|
367
|
+
...post,
|
|
368
|
+
published_at: formatTimestamp(post.published_at_iso, site),
|
|
369
|
+
updated_at: formatTimestamp(post.updated_at_iso, site),
|
|
370
|
+
reading_time: calculateReadingTime(post.html),
|
|
371
|
+
categories_html: renderInlineTaxonomyLinks(post.category_slugs, categoriesBySlug, 'category'),
|
|
372
|
+
tags_html: renderInlineTaxonomyLinks(post.tag_slugs, tagsBySlug, 'tag'),
|
|
373
|
+
comments_html: COMMENTS_HTML,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function buildTaxonomyRoutes(options) {
|
|
378
|
+
const routes = [];
|
|
379
|
+
|
|
380
|
+
for (const item of options.items) {
|
|
381
|
+
const matchedPosts = options.postsBySlug.get(item.slug) || [];
|
|
382
|
+
if (matchedPosts.length === 0) {
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const paginated = buildPaginatedCollection({
|
|
387
|
+
items: matchedPosts,
|
|
388
|
+
postsPerPage: options.postsPerPage,
|
|
389
|
+
basePath: options.buildBasePath(item),
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
for (const entry of paginated) {
|
|
393
|
+
routes.push({
|
|
394
|
+
path: entry.path,
|
|
395
|
+
slug: item.slug,
|
|
396
|
+
page: entry.page,
|
|
397
|
+
totalPages: entry.totalPages,
|
|
398
|
+
posts: options.renderList(entry.items),
|
|
399
|
+
pagination: renderPagination(entry.paginationData),
|
|
400
|
+
...options.renderExtras(item),
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return routes;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function buildPaginatedCollection(options) {
|
|
409
|
+
const basePath = normalizePaginationBasePath(options.basePath);
|
|
410
|
+
const postsPerPage = Number.isInteger(options.postsPerPage) && options.postsPerPage > 0 ? options.postsPerPage : DEFAULT_POSTS_PER_PAGE;
|
|
411
|
+
const totalPosts = options.items.length;
|
|
412
|
+
const totalPages = Math.max(1, Math.ceil(totalPosts / postsPerPage));
|
|
413
|
+
const pages = [];
|
|
414
|
+
|
|
415
|
+
for (let page = 1; page <= totalPages; page += 1) {
|
|
416
|
+
const start = (page - 1) * postsPerPage;
|
|
417
|
+
const end = start + postsPerPage;
|
|
418
|
+
|
|
419
|
+
pages.push({
|
|
420
|
+
path: buildPaginatedPath(basePath, page),
|
|
421
|
+
page,
|
|
422
|
+
totalPages,
|
|
423
|
+
items: options.items.slice(start, end),
|
|
424
|
+
paginationData: buildPaginationData(page, totalPages, totalPosts, basePath),
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return pages;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function buildPaginatedPath(basePath, page) {
|
|
432
|
+
const normalizedBasePath = normalizePaginationBasePath(basePath);
|
|
433
|
+
if (page <= 1) {
|
|
434
|
+
return normalizedBasePath;
|
|
435
|
+
}
|
|
436
|
+
if (normalizedBasePath === '/') {
|
|
437
|
+
return `/page/${page}/`;
|
|
438
|
+
}
|
|
439
|
+
return `${normalizedBasePath}page/${page}/`;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function normalizePaginationBasePath(basePath) {
|
|
443
|
+
if (!basePath || basePath === '/') {
|
|
444
|
+
return '/';
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const normalized = decodeRoutePath(String(basePath)).replace(/^\/+|\/+$/g, '');
|
|
448
|
+
return `/${normalized}/`;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function buildPaginationData(currentPage, totalPages, totalPosts, basePath) {
|
|
452
|
+
return {
|
|
453
|
+
currentPage,
|
|
454
|
+
totalPages,
|
|
455
|
+
totalPosts,
|
|
456
|
+
hasNext: currentPage < totalPages,
|
|
457
|
+
hasPrev: currentPage > 1,
|
|
458
|
+
nextUrl: currentPage < totalPages ? buildPaginatedPath(basePath, currentPage + 1) : undefined,
|
|
459
|
+
prevUrl: currentPage > 1 ? buildPaginatedPath(basePath, currentPage - 1) : undefined,
|
|
460
|
+
pages: Array.from({ length: totalPages }, (_, index) => {
|
|
461
|
+
const page = index + 1;
|
|
462
|
+
return {
|
|
463
|
+
number: page,
|
|
464
|
+
url: buildPaginatedPath(basePath, page),
|
|
465
|
+
current: page === currentPage,
|
|
466
|
+
};
|
|
467
|
+
}),
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function renderPostList(posts, postBySlug) {
|
|
472
|
+
if (posts.length === 0) {
|
|
473
|
+
return '<p>No posts</p>';
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return posts
|
|
477
|
+
.map((post) => {
|
|
478
|
+
const prepared = postBySlug.get(post.slug);
|
|
479
|
+
if (!prepared) {
|
|
480
|
+
return '';
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return [
|
|
484
|
+
'<article class="post-list-item">',
|
|
485
|
+
` <h2><a href="/posts/${escapeHtml(encodeSlugSegment(post.slug))}/">${escapeHtml(post.title)}</a></h2>`,
|
|
486
|
+
` <div class="post-meta"><time datetime="${escapeHtml(prepared.published_at_iso)}">${escapeHtml(prepared.published_at)}</time> • ${escapeHtml(prepared.reading_time)}</div>`,
|
|
487
|
+
` <div class="post-excerpt">${escapeHtml(prepared.excerpt)}</div>`,
|
|
488
|
+
'</article>',
|
|
489
|
+
].join('\n');
|
|
490
|
+
})
|
|
491
|
+
.filter(Boolean)
|
|
492
|
+
.join('\n');
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function renderArchive(posts, postBySlug) {
|
|
496
|
+
if (posts.length === 0) {
|
|
497
|
+
return '<p>No posts</p>';
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const groups = new Map();
|
|
501
|
+
|
|
502
|
+
for (const post of posts) {
|
|
503
|
+
const key = `${toDate(post.published_at_iso).getUTCFullYear()}-${String(toDate(post.published_at_iso).getUTCMonth() + 1).padStart(2, '0')}`;
|
|
504
|
+
const items = groups.get(key) || [];
|
|
505
|
+
items.push(post);
|
|
506
|
+
groups.set(key, items);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return Array.from(groups.entries())
|
|
510
|
+
.map(([key, groupPosts]) => {
|
|
511
|
+
const items = groupPosts
|
|
512
|
+
.map((post) => {
|
|
513
|
+
const prepared = postBySlug.get(post.slug);
|
|
514
|
+
if (!prepared) {
|
|
515
|
+
return '';
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
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>`;
|
|
519
|
+
})
|
|
520
|
+
.filter(Boolean)
|
|
521
|
+
.join('\n');
|
|
522
|
+
|
|
523
|
+
return `<section class="archive-group"><h2>${escapeHtml(key)}</h2><ul>${items}</ul></section>`;
|
|
524
|
+
})
|
|
525
|
+
.join('\n');
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function renderCategoryLinks(categories, countBySlug) {
|
|
529
|
+
return categories
|
|
530
|
+
.map((category) => {
|
|
531
|
+
const count = countBySlug.get(category.slug) || 0;
|
|
532
|
+
if (count === 0) {
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return `<a href="/categories/${escapeHtml(encodeSlugSegment(category.slug))}/" class="category-link">${escapeHtml(category.name)} (${count})</a>`;
|
|
537
|
+
})
|
|
538
|
+
.filter(Boolean)
|
|
539
|
+
.join('\n');
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function renderTagLinks(tags, countBySlug) {
|
|
543
|
+
return tags
|
|
544
|
+
.map((tag) => {
|
|
545
|
+
const count = countBySlug.get(tag.slug) || 0;
|
|
546
|
+
if (count === 0) {
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return `<a href="/tags/${escapeHtml(encodeSlugSegment(tag.slug))}/" class="tag-link">${escapeHtml(tag.name)} (${count})</a>`;
|
|
551
|
+
})
|
|
552
|
+
.filter(Boolean)
|
|
553
|
+
.join('\n');
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function renderInlineTaxonomyLinks(slugs, taxonomyBySlug, kind) {
|
|
557
|
+
const links = slugs
|
|
558
|
+
.map((slug) => taxonomyBySlug.get(slug))
|
|
559
|
+
.filter(Boolean)
|
|
560
|
+
.map((item) => {
|
|
561
|
+
const base = kind === 'category' ? '/categories/' : '/tags/';
|
|
562
|
+
const className = kind === 'category' ? 'category-link' : 'tag-link';
|
|
563
|
+
return `<a href="${base}${escapeHtml(encodeSlugSegment(item.slug))}/" class="${className}">${escapeHtml(item.name)}</a>`;
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
if (links.length === 0) {
|
|
567
|
+
return '';
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return kind === 'category' ? links.join(', ') : links.join(' ');
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function renderPagination(paginationData) {
|
|
574
|
+
if (paginationData.totalPages <= 1) {
|
|
575
|
+
return '';
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const prevLink = paginationData.hasPrev && paginationData.prevUrl
|
|
579
|
+
? `<a href="${paginationData.prevUrl}" class="prev">Previous</a>`
|
|
580
|
+
: '';
|
|
581
|
+
const nextLink = paginationData.hasNext && paginationData.nextUrl
|
|
582
|
+
? `<a href="${paginationData.nextUrl}" class="next">Next</a>`
|
|
583
|
+
: '';
|
|
584
|
+
const pageLinks = paginationData.pages
|
|
585
|
+
.map((page) => `<a href="${page.url}" class="page-link ${page.current ? 'current' : ''}">${page.number}</a>`)
|
|
586
|
+
.join(' ');
|
|
587
|
+
|
|
588
|
+
return `<nav class="pagination">\n ${prevLink}\n <span class="pages">${pageLinks}</span>\n ${nextLink}\n</nav>`;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function formatTimestamp(value, site) {
|
|
592
|
+
const date = toDate(value);
|
|
593
|
+
const locale = normalizeLocale(site.siteLocale || site.language || DEFAULT_LOCALE);
|
|
594
|
+
const dateFormat = normalizeNonEmptyString(site.dateFormat, DEFAULT_DATE_FORMAT);
|
|
595
|
+
const timeFormat = typeof site.timeFormat === 'string' ? site.timeFormat : DEFAULT_TIME_FORMAT;
|
|
596
|
+
const siteTimezone = normalizeNonEmptyString(site.siteTimezone, DEFAULT_TIMEZONE);
|
|
597
|
+
|
|
598
|
+
const dateParts = new Intl.DateTimeFormat(locale, {
|
|
599
|
+
timeZone: siteTimezone,
|
|
600
|
+
year: 'numeric',
|
|
601
|
+
month: dateFormat === 'MMMM D, YYYY' ? 'long' : '2-digit',
|
|
602
|
+
day: '2-digit',
|
|
603
|
+
}).formatToParts(date);
|
|
604
|
+
|
|
605
|
+
const year = dateParts.find((part) => part.type === 'year')?.value || '';
|
|
606
|
+
const month = dateParts.find((part) => part.type === 'month')?.value || '';
|
|
607
|
+
const day = dateParts.find((part) => part.type === 'day')?.value || '';
|
|
608
|
+
|
|
609
|
+
let formattedDate = `${year}-${month}-${day}`;
|
|
610
|
+
if (dateFormat === 'DD/MM/YYYY') {
|
|
611
|
+
formattedDate = `${day}/${month}/${year}`;
|
|
612
|
+
} else if (dateFormat === 'MM/DD/YYYY') {
|
|
613
|
+
formattedDate = `${month}/${day}/${year}`;
|
|
614
|
+
} else if (dateFormat === 'MMMM D, YYYY') {
|
|
615
|
+
formattedDate = `${month} ${String(Number(day))}, ${year}`;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (!timeFormat) {
|
|
619
|
+
return formattedDate;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const timeParts = new Intl.DateTimeFormat(locale, {
|
|
623
|
+
timeZone: siteTimezone,
|
|
624
|
+
hour: '2-digit',
|
|
625
|
+
minute: '2-digit',
|
|
626
|
+
hour12: timeFormat === 'hh:mm A',
|
|
627
|
+
}).formatToParts(date);
|
|
628
|
+
|
|
629
|
+
const hour = timeParts.find((part) => part.type === 'hour')?.value || '';
|
|
630
|
+
const minute = timeParts.find((part) => part.type === 'minute')?.value || '';
|
|
631
|
+
const dayPeriod = (timeParts.find((part) => part.type === 'dayPeriod')?.value || '').toUpperCase();
|
|
632
|
+
|
|
633
|
+
const formattedTime = timeFormat === 'hh:mm A'
|
|
634
|
+
? `${hour}:${minute}${dayPeriod ? ` ${dayPeriod}` : ''}`
|
|
635
|
+
: `${hour}:${minute}`;
|
|
636
|
+
|
|
637
|
+
return `${formattedDate} ${formattedTime}`.trim();
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function calculateReadingTime(html) {
|
|
641
|
+
const plainText = String(html || '').replace(/<[^>]*>/g, ' ');
|
|
642
|
+
const wordCount = plainText.trim().split(/\s+/).filter(Boolean).length;
|
|
643
|
+
const minutes = Math.max(1, Math.ceil(wordCount / 200));
|
|
644
|
+
return minutes === 1 ? '1 min read' : `${minutes} min read`;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function pushToSlugMap(target, slug, value) {
|
|
648
|
+
const items = target.get(slug) || [];
|
|
649
|
+
items.push(value);
|
|
650
|
+
target.set(slug, items);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function normalizeNonEmptyString(value, fallback) {
|
|
654
|
+
return typeof value === 'string' && value.trim() ? value : fallback;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function normalizeLocale(value) {
|
|
658
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
659
|
+
return DEFAULT_LOCALE;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const parts = value.trim().replace(/_/g, '-').split('-').filter(Boolean);
|
|
663
|
+
if (parts.length === 0) {
|
|
664
|
+
return DEFAULT_LOCALE;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return parts.map((part, index) => {
|
|
668
|
+
if (index === 0) {
|
|
669
|
+
return part.toLowerCase();
|
|
670
|
+
}
|
|
671
|
+
if (part.length === 2 || part.length === 3) {
|
|
672
|
+
return part.toUpperCase();
|
|
673
|
+
}
|
|
674
|
+
if (part.length === 4) {
|
|
675
|
+
return part.charAt(0).toUpperCase() + part.slice(1).toLowerCase();
|
|
676
|
+
}
|
|
677
|
+
return part.toLowerCase();
|
|
678
|
+
}).join('-');
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function hasTemplate(state, templateName) {
|
|
682
|
+
return state.engine.themePackage?.templates?.has(templateName) === true;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function recordRouteEmission(state, templateName, route, currentUrl) {
|
|
686
|
+
const targetMap = {
|
|
687
|
+
index: state.emitted.indexRoutes,
|
|
688
|
+
archive: state.emitted.archiveRoutes,
|
|
689
|
+
category: state.emitted.categoryRoutes,
|
|
690
|
+
tag: state.emitted.tagRoutes,
|
|
691
|
+
}[templateName];
|
|
692
|
+
|
|
693
|
+
if (!targetMap) {
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
targetMap.push({
|
|
698
|
+
url: currentUrl,
|
|
699
|
+
page: route.page,
|
|
700
|
+
totalPages: route.totalPages,
|
|
701
|
+
slug: route.slug,
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
|
|
211
705
|
async function assertValidThemePackage(themePackage) {
|
|
212
706
|
if (!themePackage?.templates || !themePackage?.partials || !themePackage?.assets || !themePackage?.metadata) {
|
|
213
707
|
throw new Error('Invalid themePackage: expected metadata, templates, partials, and assets');
|
|
@@ -328,37 +822,44 @@ function injectHtmxScript(html) {
|
|
|
328
822
|
return html.replace('</body>', '<script src="https://unpkg.com/htmx.org@2.0.4" crossorigin="anonymous"></script>\n</body>');
|
|
329
823
|
}
|
|
330
824
|
|
|
331
|
-
function buildSitemapXml(
|
|
825
|
+
function buildSitemapXml(site, emitted, generatedAt) {
|
|
332
826
|
const entries = [
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
827
|
+
...emitted.indexRoutes
|
|
828
|
+
.filter((route) => route.url === '/')
|
|
829
|
+
.map((route) => ({
|
|
830
|
+
url: route.url,
|
|
831
|
+
lastmod: generatedAt,
|
|
832
|
+
changefreq: 'daily',
|
|
833
|
+
priority: 1.0,
|
|
834
|
+
})),
|
|
835
|
+
...emitted.posts.map((post) => ({
|
|
836
|
+
url: post.url,
|
|
837
|
+
lastmod: toDate(post.updatedAt),
|
|
337
838
|
changefreq: 'weekly',
|
|
338
839
|
priority: 0.8,
|
|
339
840
|
})),
|
|
340
|
-
...
|
|
341
|
-
url:
|
|
342
|
-
lastmod:
|
|
841
|
+
...emitted.pages.map((page) => ({
|
|
842
|
+
url: page.url,
|
|
843
|
+
lastmod: generatedAt,
|
|
343
844
|
changefreq: 'monthly',
|
|
344
845
|
priority: 0.7,
|
|
345
846
|
})),
|
|
346
|
-
...
|
|
347
|
-
url:
|
|
348
|
-
lastmod:
|
|
847
|
+
...emitted.categoryRoutes.filter((route) => route.page === 1).map((route) => ({
|
|
848
|
+
url: route.url,
|
|
849
|
+
lastmod: generatedAt,
|
|
349
850
|
changefreq: 'weekly',
|
|
350
851
|
priority: 0.6,
|
|
351
852
|
})),
|
|
352
|
-
...
|
|
353
|
-
url:
|
|
354
|
-
lastmod:
|
|
853
|
+
...emitted.tagRoutes.filter((route) => route.page === 1).map((route) => ({
|
|
854
|
+
url: route.url,
|
|
855
|
+
lastmod: generatedAt,
|
|
355
856
|
changefreq: 'weekly',
|
|
356
857
|
priority: 0.5,
|
|
357
858
|
})),
|
|
358
859
|
];
|
|
359
860
|
|
|
360
861
|
const body = entries.map((entry) => {
|
|
361
|
-
const loc = escapeXml(resolveSiteUrl(
|
|
862
|
+
const loc = escapeXml(resolveSiteUrl(site.url, entry.url));
|
|
362
863
|
const lastmod = entry.lastmod.toISOString().split('T')[0];
|
|
363
864
|
return ` <url>\n <loc>${loc}</loc>\n <lastmod>${lastmod}</lastmod>\n <changefreq>${entry.changefreq}</changefreq>\n <priority>${entry.priority.toFixed(1)}</priority>\n </url>`;
|
|
364
865
|
}).join('\n');
|
|
@@ -366,36 +867,42 @@ function buildSitemapXml(previewData) {
|
|
|
366
867
|
return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${body}\n</urlset>`;
|
|
367
868
|
}
|
|
368
869
|
|
|
369
|
-
function buildFeedXml(
|
|
370
|
-
const
|
|
371
|
-
|
|
870
|
+
function buildFeedXml(site, emitted, generatedAt) {
|
|
871
|
+
const channelLink = resolveSiteUrl(site.url, '/');
|
|
872
|
+
const selfLink = resolveSiteUrl(site.url, '/feed.xml');
|
|
873
|
+
const items = [...emitted.posts]
|
|
874
|
+
.sort((a, b) => toDate(b.publishedAt).getTime() - toDate(a.publishedAt).getTime())
|
|
372
875
|
.slice(0, 20)
|
|
373
876
|
.map((post) => {
|
|
374
|
-
const url = resolveSiteUrl(
|
|
375
|
-
return ` <item>\n <title>${escapeXml(post.title)}</title>\n <link>${escapeXml(url)}</link>\n <guid>${escapeXml(url)}</guid>\n <pubDate>${toDate(post.
|
|
877
|
+
const url = resolveSiteUrl(site.url, post.url);
|
|
878
|
+
return ` <item>\n <title>${escapeXml(post.title)}</title>\n <link>${escapeXml(url)}</link>\n <guid>${escapeXml(url)}</guid>\n <pubDate>${toDate(post.publishedAt).toUTCString()}</pubDate>\n <description>${escapeXml(post.description)}</description>\n </item>`;
|
|
376
879
|
})
|
|
377
880
|
.join('\n');
|
|
378
881
|
|
|
379
|
-
return `<?xml version="1.0" encoding="UTF-8"?>\n<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">\n <channel>\n <title>${escapeXml(
|
|
882
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">\n <channel>\n <title>${escapeXml(site.title)}</title>\n <link>${escapeXml(channelLink)}</link>\n <description>${escapeXml(site.description)}</description>\n <language>${site.language}</language>\n <lastBuildDate>${generatedAt.toUTCString()}</lastBuildDate>\n <atom:link href="${escapeXml(selfLink)}" rel="self" type="application/rss+xml" />\n${items}\n </channel>\n</rss>`;
|
|
380
883
|
}
|
|
381
884
|
|
|
382
|
-
function buildRobotsTxt(
|
|
383
|
-
|
|
885
|
+
function buildRobotsTxt(site) {
|
|
886
|
+
const lines = ['User-agent: *', 'Allow: /'];
|
|
887
|
+
if (site.url) {
|
|
888
|
+
lines.push('', `Sitemap: ${resolveSiteUrl(site.url, '/sitemap.xml')}`);
|
|
889
|
+
}
|
|
890
|
+
return `${lines.join('\n')}\n`;
|
|
384
891
|
}
|
|
385
892
|
|
|
386
|
-
function buildMetaJson(
|
|
893
|
+
function buildMetaJson(site, emitted, generatedAt) {
|
|
387
894
|
const pages = [
|
|
388
|
-
...
|
|
389
|
-
url:
|
|
895
|
+
...emitted.posts.map((post) => ({
|
|
896
|
+
url: post.url,
|
|
390
897
|
title: post.title,
|
|
391
|
-
description: post.
|
|
392
|
-
publishedAt: post.
|
|
393
|
-
updatedAt: post.
|
|
898
|
+
description: post.description,
|
|
899
|
+
publishedAt: post.publishedAt,
|
|
900
|
+
updatedAt: post.updatedAt,
|
|
394
901
|
type: 'post',
|
|
395
902
|
metadata: { status: post.status },
|
|
396
903
|
})),
|
|
397
|
-
...
|
|
398
|
-
url:
|
|
904
|
+
...emitted.pages.map((page) => ({
|
|
905
|
+
url: page.url,
|
|
399
906
|
title: page.title,
|
|
400
907
|
type: 'page',
|
|
401
908
|
metadata: { status: page.status },
|
|
@@ -403,12 +910,12 @@ function buildMetaJson(previewData) {
|
|
|
403
910
|
];
|
|
404
911
|
|
|
405
912
|
return JSON.stringify({
|
|
406
|
-
generated:
|
|
913
|
+
generated: generatedAt.toISOString(),
|
|
407
914
|
site: {
|
|
408
|
-
title:
|
|
409
|
-
description:
|
|
410
|
-
url:
|
|
411
|
-
language:
|
|
915
|
+
title: site.title,
|
|
916
|
+
description: site.description,
|
|
917
|
+
url: site.url,
|
|
918
|
+
language: site.language,
|
|
412
919
|
},
|
|
413
920
|
pages,
|
|
414
921
|
}, null, 2);
|
|
@@ -469,6 +976,10 @@ function decodeRoutePath(routePath) {
|
|
|
469
976
|
}
|
|
470
977
|
|
|
471
978
|
function resolveSiteUrl(siteUrl, relativePath) {
|
|
979
|
+
if (!siteUrl) {
|
|
980
|
+
return normalizeRoutePath(relativePath);
|
|
981
|
+
}
|
|
982
|
+
|
|
472
983
|
return decodeURI(new URL(relativePath, siteUrl).toString());
|
|
473
984
|
}
|
|
474
985
|
|
|
@@ -484,3 +995,12 @@ function escapeXml(str) {
|
|
|
484
995
|
.replace(/"/g, '"')
|
|
485
996
|
.replace(/'/g, ''');
|
|
486
997
|
}
|
|
998
|
+
|
|
999
|
+
function escapeHtml(value) {
|
|
1000
|
+
return String(value || '')
|
|
1001
|
+
.replaceAll('&', '&')
|
|
1002
|
+
.replaceAll('<', '<')
|
|
1003
|
+
.replaceAll('>', '>')
|
|
1004
|
+
.replaceAll('"', '"')
|
|
1005
|
+
.replaceAll("'", ''');
|
|
1006
|
+
}
|
package/src/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { PreviewDataV04 } from '@zeropress/preview-data-validator';
|
|
2
2
|
|
|
3
3
|
export interface ThemePackage {
|
|
4
4
|
metadata: {
|
|
@@ -62,14 +62,14 @@ export interface BuildSiteResult {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
export function buildSite(input: {
|
|
65
|
-
previewData:
|
|
65
|
+
previewData: PreviewDataV04;
|
|
66
66
|
themePackage: ThemePackage;
|
|
67
67
|
writer: BuildWriter;
|
|
68
68
|
options?: BuildOptions;
|
|
69
69
|
}): Promise<BuildSiteResult>;
|
|
70
70
|
|
|
71
71
|
export function buildSelectedRoutes(input: {
|
|
72
|
-
previewData:
|
|
72
|
+
previewData: PreviewDataV04;
|
|
73
73
|
themePackage: ThemePackage;
|
|
74
74
|
writer: BuildWriter;
|
|
75
75
|
selection: BuildSelection;
|
|
@@ -77,7 +77,7 @@ export function buildSelectedRoutes(input: {
|
|
|
77
77
|
}): Promise<BuildSiteResult>;
|
|
78
78
|
|
|
79
79
|
export function buildSiteFromThemeDir(input: {
|
|
80
|
-
previewData:
|
|
80
|
+
previewData: PreviewDataV04;
|
|
81
81
|
themeDir: string;
|
|
82
82
|
writer: BuildWriter;
|
|
83
83
|
options?: BuildOptions;
|