@zeropress/build-core 0.1.1 → 0.1.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 +23 -0
- package/package.json +1 -1
- package/src/api/build-site.js +133 -52
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 the canonical input graph. 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
|
|
@@ -109,6 +114,15 @@ Renders only selected post and list routes while preserving full-build parity fo
|
|
|
109
114
|
|
|
110
115
|
This is intended for selective publish and other partial-render workflows.
|
|
111
116
|
|
|
117
|
+
Optional route templates are respected during both full and partial renders:
|
|
118
|
+
|
|
119
|
+
- missing `archive.html` skips archive outputs
|
|
120
|
+
- missing `category.html` skips category outputs
|
|
121
|
+
- missing `tag.html` skips tag outputs
|
|
122
|
+
- missing `404.html` skips `404.html` emission
|
|
123
|
+
|
|
124
|
+
These cases do not fail the build.
|
|
125
|
+
|
|
112
126
|
```js
|
|
113
127
|
await buildSelectedRoutes({
|
|
114
128
|
previewData,
|
|
@@ -177,6 +191,15 @@ Theme validation is enforced through:
|
|
|
177
191
|
|
|
178
192
|
- [`@zeropress/theme-validator`](https://www.npmjs.com/package/@zeropress/theme-validator)
|
|
179
193
|
|
|
194
|
+
Optional route templates behave as rendering capabilities, not guaranteed outputs:
|
|
195
|
+
|
|
196
|
+
- `archive.html`
|
|
197
|
+
- `category.html`
|
|
198
|
+
- `tag.html`
|
|
199
|
+
- `404.html`
|
|
200
|
+
|
|
201
|
+
If preview-data includes archive/category/tag route arrays 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.
|
|
202
|
+
|
|
180
203
|
---
|
|
181
204
|
|
|
182
205
|
## Build Options
|
package/package.json
CHANGED
package/src/api/build-site.js
CHANGED
|
@@ -27,16 +27,22 @@ export async function buildSite(input) {
|
|
|
27
27
|
await renderPage(state, page);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
if (hasTemplate(state, 'category')) {
|
|
31
|
+
for (const route of input.previewData.routes.categories) {
|
|
32
|
+
await renderRoute(state, 'category', route);
|
|
33
|
+
}
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
if (hasTemplate(state, 'tag')) {
|
|
37
|
+
for (const route of input.previewData.routes.tags) {
|
|
38
|
+
await renderRoute(state, 'tag', route);
|
|
39
|
+
}
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
if (hasTemplate(state, 'archive')) {
|
|
43
|
+
for (const route of input.previewData.routes.archive) {
|
|
44
|
+
await renderRoute(state, 'archive', route);
|
|
45
|
+
}
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
for (const assetOutput of state.assetOutputs) {
|
|
@@ -44,10 +50,11 @@ export async function buildSite(input) {
|
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
if (options.generateSpecialFiles) {
|
|
47
|
-
await
|
|
48
|
-
await writeOutput(state.writer, state.summaries, '
|
|
53
|
+
await maybeRenderNotFoundPage(state);
|
|
54
|
+
await writeOutput(state.writer, state.summaries, 'sitemap.xml', buildSitemapXml(state.previewData.site, state.emitted, state.generatedAt), 'application/xml');
|
|
55
|
+
await writeOutput(state.writer, state.summaries, 'feed.xml', buildFeedXml(state.previewData.site, state.emitted, state.generatedAt), 'application/rss+xml');
|
|
49
56
|
await writeOutput(state.writer, state.summaries, 'robots.txt', buildRobotsTxt(input.previewData), 'text/plain');
|
|
50
|
-
await writeOutput(state.writer, state.summaries, 'meta.json', buildMetaJson(
|
|
57
|
+
await writeOutput(state.writer, state.summaries, 'meta.json', buildMetaJson(state.previewData.site, state.emitted, state.generatedAt), 'application/json');
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
return finalizeBuildResult(state.writer, state.summaries, options);
|
|
@@ -76,21 +83,27 @@ export async function buildSelectedRoutes(input) {
|
|
|
76
83
|
}
|
|
77
84
|
}
|
|
78
85
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
86
|
+
if (hasTemplate(state, 'archive')) {
|
|
87
|
+
for (const route of input.previewData.routes.archive) {
|
|
88
|
+
if (selectedArchiveRoutes.has(normalizeRoutePath(route.path))) {
|
|
89
|
+
await renderRoute(state, 'archive', route);
|
|
90
|
+
}
|
|
82
91
|
}
|
|
83
92
|
}
|
|
84
93
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
94
|
+
if (hasTemplate(state, 'category')) {
|
|
95
|
+
for (const route of input.previewData.routes.categories) {
|
|
96
|
+
if (selectedCategoryRoutes.has(normalizeRoutePath(route.path))) {
|
|
97
|
+
await renderRoute(state, 'category', route);
|
|
98
|
+
}
|
|
88
99
|
}
|
|
89
100
|
}
|
|
90
101
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
102
|
+
if (hasTemplate(state, 'tag')) {
|
|
103
|
+
for (const route of input.previewData.routes.tags) {
|
|
104
|
+
if (selectedTagRoutes.has(normalizeRoutePath(route.path))) {
|
|
105
|
+
await renderRoute(state, 'tag', route);
|
|
106
|
+
}
|
|
94
107
|
}
|
|
95
108
|
}
|
|
96
109
|
|
|
@@ -132,6 +145,15 @@ async function createBuildState(input, options) {
|
|
|
132
145
|
assetOutputs,
|
|
133
146
|
assetMap,
|
|
134
147
|
options,
|
|
148
|
+
generatedAt: new Date(),
|
|
149
|
+
emitted: {
|
|
150
|
+
indexRoutes: [],
|
|
151
|
+
archiveRoutes: [],
|
|
152
|
+
categoryRoutes: [],
|
|
153
|
+
tagRoutes: [],
|
|
154
|
+
posts: [],
|
|
155
|
+
pages: [],
|
|
156
|
+
},
|
|
135
157
|
};
|
|
136
158
|
}
|
|
137
159
|
|
|
@@ -166,6 +188,7 @@ async function renderRoute(state, templateName, route) {
|
|
|
166
188
|
);
|
|
167
189
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
168
190
|
await writeOutput(state.writer, state.summaries, routePathToOutputPath(route.path), html, 'text/html');
|
|
191
|
+
recordRouteEmission(state, templateName, route, currentUrl);
|
|
169
192
|
}
|
|
170
193
|
|
|
171
194
|
async function renderPost(state, post) {
|
|
@@ -179,6 +202,14 @@ async function renderPost(state, post) {
|
|
|
179
202
|
html = injectHtmxScript(html);
|
|
180
203
|
}
|
|
181
204
|
await writeOutput(state.writer, state.summaries, `posts/${encodeSlugSegment(post.slug)}/index.html`, html, 'text/html');
|
|
205
|
+
state.emitted.posts.push({
|
|
206
|
+
url: `/posts/${encodeSlugSegment(post.slug)}/`,
|
|
207
|
+
title: post.title,
|
|
208
|
+
description: post.excerpt,
|
|
209
|
+
publishedAt: post.published_at_iso,
|
|
210
|
+
updatedAt: post.updated_at_iso,
|
|
211
|
+
status: post.status,
|
|
212
|
+
});
|
|
182
213
|
}
|
|
183
214
|
|
|
184
215
|
async function renderPage(state, page) {
|
|
@@ -189,6 +220,25 @@ async function renderPage(state, page) {
|
|
|
189
220
|
);
|
|
190
221
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
191
222
|
await writeOutput(state.writer, state.summaries, `${encodeSlugSegment(page.slug)}/index.html`, html, 'text/html');
|
|
223
|
+
state.emitted.pages.push({
|
|
224
|
+
url: `/${encodeSlugSegment(page.slug)}/`,
|
|
225
|
+
title: page.title,
|
|
226
|
+
status: page.status,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function maybeRenderNotFoundPage(state) {
|
|
231
|
+
if (!state.engine.themePackage?.templates?.has('404')) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let html = await state.engine.render(
|
|
236
|
+
'404',
|
|
237
|
+
{},
|
|
238
|
+
createRenderContext(state.previewData.site, '/404.html'),
|
|
239
|
+
);
|
|
240
|
+
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
241
|
+
await writeOutput(state.writer, state.summaries, '404.html', html, 'text/html');
|
|
192
242
|
}
|
|
193
243
|
|
|
194
244
|
function validateSelection(selection) {
|
|
@@ -208,6 +258,30 @@ function validateSelection(selection) {
|
|
|
208
258
|
}
|
|
209
259
|
}
|
|
210
260
|
|
|
261
|
+
function hasTemplate(state, templateName) {
|
|
262
|
+
return state.engine.themePackage?.templates?.has(templateName) === true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function recordRouteEmission(state, templateName, route, currentUrl) {
|
|
266
|
+
const targetMap = {
|
|
267
|
+
index: state.emitted.indexRoutes,
|
|
268
|
+
archive: state.emitted.archiveRoutes,
|
|
269
|
+
category: state.emitted.categoryRoutes,
|
|
270
|
+
tag: state.emitted.tagRoutes,
|
|
271
|
+
}[templateName];
|
|
272
|
+
|
|
273
|
+
if (!targetMap) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
targetMap.push({
|
|
278
|
+
url: currentUrl,
|
|
279
|
+
page: route.page,
|
|
280
|
+
totalPages: route.totalPages,
|
|
281
|
+
slug: route.slug,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
211
285
|
async function assertValidThemePackage(themePackage) {
|
|
212
286
|
if (!themePackage?.templates || !themePackage?.partials || !themePackage?.assets || !themePackage?.metadata) {
|
|
213
287
|
throw new Error('Invalid themePackage: expected metadata, templates, partials, and assets');
|
|
@@ -328,37 +402,44 @@ function injectHtmxScript(html) {
|
|
|
328
402
|
return html.replace('</body>', '<script src="https://unpkg.com/htmx.org@2.0.4" crossorigin="anonymous"></script>\n</body>');
|
|
329
403
|
}
|
|
330
404
|
|
|
331
|
-
function buildSitemapXml(
|
|
405
|
+
function buildSitemapXml(site, emitted, generatedAt) {
|
|
332
406
|
const entries = [
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
407
|
+
...emitted.indexRoutes
|
|
408
|
+
.filter((route) => route.url === '/')
|
|
409
|
+
.map((route) => ({
|
|
410
|
+
url: route.url,
|
|
411
|
+
lastmod: generatedAt,
|
|
412
|
+
changefreq: 'daily',
|
|
413
|
+
priority: 1.0,
|
|
414
|
+
})),
|
|
415
|
+
...emitted.posts.map((post) => ({
|
|
416
|
+
url: post.url,
|
|
417
|
+
lastmod: toDate(post.updatedAt),
|
|
337
418
|
changefreq: 'weekly',
|
|
338
419
|
priority: 0.8,
|
|
339
420
|
})),
|
|
340
|
-
...
|
|
341
|
-
url:
|
|
342
|
-
lastmod:
|
|
421
|
+
...emitted.pages.map((page) => ({
|
|
422
|
+
url: page.url,
|
|
423
|
+
lastmod: generatedAt,
|
|
343
424
|
changefreq: 'monthly',
|
|
344
425
|
priority: 0.7,
|
|
345
426
|
})),
|
|
346
|
-
...
|
|
347
|
-
url:
|
|
348
|
-
lastmod:
|
|
427
|
+
...emitted.categoryRoutes.filter((route) => route.page === 1).map((route) => ({
|
|
428
|
+
url: route.url,
|
|
429
|
+
lastmod: generatedAt,
|
|
349
430
|
changefreq: 'weekly',
|
|
350
431
|
priority: 0.6,
|
|
351
432
|
})),
|
|
352
|
-
...
|
|
353
|
-
url:
|
|
354
|
-
lastmod:
|
|
433
|
+
...emitted.tagRoutes.filter((route) => route.page === 1).map((route) => ({
|
|
434
|
+
url: route.url,
|
|
435
|
+
lastmod: generatedAt,
|
|
355
436
|
changefreq: 'weekly',
|
|
356
437
|
priority: 0.5,
|
|
357
438
|
})),
|
|
358
439
|
];
|
|
359
440
|
|
|
360
441
|
const body = entries.map((entry) => {
|
|
361
|
-
const loc = escapeXml(resolveSiteUrl(
|
|
442
|
+
const loc = escapeXml(resolveSiteUrl(site.url, entry.url));
|
|
362
443
|
const lastmod = entry.lastmod.toISOString().split('T')[0];
|
|
363
444
|
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
445
|
}).join('\n');
|
|
@@ -366,36 +447,36 @@ function buildSitemapXml(previewData) {
|
|
|
366
447
|
return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${body}\n</urlset>`;
|
|
367
448
|
}
|
|
368
449
|
|
|
369
|
-
function buildFeedXml(
|
|
370
|
-
const items = [...
|
|
371
|
-
.sort((a, b) => toDate(b.
|
|
450
|
+
function buildFeedXml(site, emitted, generatedAt) {
|
|
451
|
+
const items = [...emitted.posts]
|
|
452
|
+
.sort((a, b) => toDate(b.publishedAt).getTime() - toDate(a.publishedAt).getTime())
|
|
372
453
|
.slice(0, 20)
|
|
373
454
|
.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.
|
|
455
|
+
const url = resolveSiteUrl(site.url, post.url);
|
|
456
|
+
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
457
|
})
|
|
377
458
|
.join('\n');
|
|
378
459
|
|
|
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(
|
|
460
|
+
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(site.url)}</link>\n <description>${escapeXml(site.description)}</description>\n <language>${site.language}</language>\n <lastBuildDate>${generatedAt.toUTCString()}</lastBuildDate>\n <atom:link href="${escapeXml(new URL('/feed.xml', site.url).toString())}" rel="self" type="application/rss+xml" />\n${items}\n </channel>\n</rss>`;
|
|
380
461
|
}
|
|
381
462
|
|
|
382
463
|
function buildRobotsTxt(previewData) {
|
|
383
464
|
return `User-agent: *\nAllow: /\n\nSitemap: ${previewData.site.url}/sitemap.xml\n`;
|
|
384
465
|
}
|
|
385
466
|
|
|
386
|
-
function buildMetaJson(
|
|
467
|
+
function buildMetaJson(site, emitted, generatedAt) {
|
|
387
468
|
const pages = [
|
|
388
|
-
...
|
|
389
|
-
url:
|
|
469
|
+
...emitted.posts.map((post) => ({
|
|
470
|
+
url: post.url,
|
|
390
471
|
title: post.title,
|
|
391
|
-
description: post.
|
|
392
|
-
publishedAt: post.
|
|
393
|
-
updatedAt: post.
|
|
472
|
+
description: post.description,
|
|
473
|
+
publishedAt: post.publishedAt,
|
|
474
|
+
updatedAt: post.updatedAt,
|
|
394
475
|
type: 'post',
|
|
395
476
|
metadata: { status: post.status },
|
|
396
477
|
})),
|
|
397
|
-
...
|
|
398
|
-
url:
|
|
478
|
+
...emitted.pages.map((page) => ({
|
|
479
|
+
url: page.url,
|
|
399
480
|
title: page.title,
|
|
400
481
|
type: 'page',
|
|
401
482
|
metadata: { status: page.status },
|
|
@@ -403,12 +484,12 @@ function buildMetaJson(previewData) {
|
|
|
403
484
|
];
|
|
404
485
|
|
|
405
486
|
return JSON.stringify({
|
|
406
|
-
generated:
|
|
487
|
+
generated: generatedAt.toISOString(),
|
|
407
488
|
site: {
|
|
408
|
-
title:
|
|
409
|
-
description:
|
|
410
|
-
url:
|
|
411
|
-
language:
|
|
489
|
+
title: site.title,
|
|
490
|
+
description: site.description,
|
|
491
|
+
url: site.url,
|
|
492
|
+
language: site.language,
|
|
412
493
|
},
|
|
413
494
|
pages,
|
|
414
495
|
}, null, 2);
|