@zeropress/build-pages 0.6.3 → 0.6.4
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 +41 -9
- package/action.yml +1 -1
- package/dist/action.js +62 -16
- package/dist/prebuild.js +215 -21
- package/package.json +2 -2
- package/schemas/zeropress-build-pages.config.v0.1.schema.json +67 -0
- package/src/index.js +71 -13
- package/src/prebuild.js +244 -22
- package/themes/docs/assets/style.css +6 -7
- package/themes/docs/assets/theme.js +37 -17
- package/themes/docs/layout.html +2 -2
- package/themes/docs/page.html +4 -0
- package/themes/docs/post.html +37 -10
- package/themes/docs/theme.json +1 -0
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ public directory
|
|
|
19
19
|
|
|
|
20
20
|
v
|
|
21
21
|
@zeropress/build-pages
|
|
22
|
-
generates .zeropress/preview-data.json
|
|
22
|
+
generates .zeropress-build-page/preview-data.json
|
|
23
23
|
stages public files
|
|
24
24
|
|
|
|
25
25
|
v
|
|
@@ -40,7 +40,7 @@ flowchart TD
|
|
|
40
40
|
config --> buildPages
|
|
41
41
|
publicFiles --> buildPages
|
|
42
42
|
|
|
43
|
-
buildPages --> previewData[".zeropress/preview-data.json<br/>internal generated build input"]
|
|
43
|
+
buildPages --> previewData[".zeropress-build-page/preview-data.json<br/>internal generated build input"]
|
|
44
44
|
buildPages --> stagedPublic["Staged public files"]
|
|
45
45
|
|
|
46
46
|
previewData --> build["@zeropress/build"]
|
|
@@ -154,7 +154,7 @@ In the action inputs:
|
|
|
154
154
|
- `source` is the directory that contains your Markdown pages and optional `.zeropress/config.json`. The default is `./docs`.
|
|
155
155
|
- `public-dir` is the directory copied as public passthrough files. The default is `source`. If you set it explicitly, the directory must exist.
|
|
156
156
|
- `destination` is the directory where the generated static site is written. The default is `./_site`.
|
|
157
|
-
- `theme` is the bundled theme name. The default is `docs`.
|
|
157
|
+
- `theme` is the bundled theme name. The default is `docs`; `docs1` is an alias for `docs`.
|
|
158
158
|
- `theme-path` is a custom local ZeroPress theme directory. It takes precedence over `theme`.
|
|
159
159
|
- `config` is the config file path. The default is `<source>/.zeropress/config.json`.
|
|
160
160
|
- `site-url` overrides the canonical site URL from config.
|
|
@@ -235,7 +235,7 @@ The CLI requires explicit input and output paths. The GitHub Action keeps safe d
|
|
|
235
235
|
| `--source <dir>` | required | Dedicated source directory containing Markdown and optional config |
|
|
236
236
|
| `--public-dir <dir>` | source | Public passthrough directory. Explicit paths must exist. |
|
|
237
237
|
| `--destination <dir>` | required | Output directory |
|
|
238
|
-
| `--theme <name>` | `docs` | Bundled theme name |
|
|
238
|
+
| `--theme <name>` | `docs` | Bundled theme name. `docs1` aliases `docs`. |
|
|
239
239
|
| `--theme-path <dir>` | none | Custom ZeroPress theme directory |
|
|
240
240
|
| `--config <path>` | `<source>/.zeropress/config.json` | Build Pages config |
|
|
241
241
|
| `--site-url <url>` | config `site.url` | Canonical URL override |
|
|
@@ -270,7 +270,7 @@ Root-level public files named `favicon.ico`, `favicon.svg`, `favicon.png`, and `
|
|
|
270
270
|
|
|
271
271
|
A root-level public `sitemap.xsl` is copied to the destination. When ZeroPress generates `sitemap.xml`, it auto-discovers that file and adds an XML stylesheet processing instruction for `/sitemap.xsl`.
|
|
272
272
|
|
|
273
|
-
The source directory must not overlap the destination directory, the selected theme directory, or the internal `.zeropress/` working directory. An explicit public directory must be an existing dedicated directory and must not be a file, symlink, repository root, destination directory, selected theme directory, or internal `.zeropress/` working directory.
|
|
273
|
+
The source directory must not overlap the destination directory, the selected theme directory, or the internal `.zeropress-build-page/` working directory. An explicit public directory must be an existing dedicated directory and must not be a file, symlink, repository root, destination directory, selected theme directory, or internal `.zeropress-build-page/` working directory.
|
|
274
274
|
|
|
275
275
|
If `public-dir` is inside `source`, Build Pages excludes that public subtree from Markdown page discovery.
|
|
276
276
|
|
|
@@ -312,6 +312,7 @@ description: Build a static docs site from Markdown.
|
|
|
312
312
|
path: guides/install
|
|
313
313
|
status: published
|
|
314
314
|
discoverability: default
|
|
315
|
+
last_updated: none
|
|
315
316
|
meta:
|
|
316
317
|
source: docs
|
|
317
318
|
data:
|
|
@@ -337,6 +338,7 @@ Supported front matter fields:
|
|
|
337
338
|
| `path` | Generated route path, such as `guides/install` for `/guides/install`. |
|
|
338
339
|
| `status` | `published` includes the page. `draft` skips the page. Other values warn and skip. |
|
|
339
340
|
| `discoverability` | `default`, `noindex`, or `delist`. Missing is `default`. |
|
|
341
|
+
| `last_updated` | `none` or `git`. Overrides config `markdown.last_updated` for this page. |
|
|
340
342
|
| `meta` | Optional scalar/null metadata copied to the generated page. |
|
|
341
343
|
| `data` | Optional structured JSON-style data for theme-facing lists, facts, galleries, timelines, or swatches. |
|
|
342
344
|
|
|
@@ -352,6 +354,8 @@ Unknown front matter fields are ignored to make migration from existing Markdown
|
|
|
352
354
|
|
|
353
355
|
`delist` is not a security or permission feature. Direct links, explicit menus, explicit collections, and body links can still expose the page.
|
|
354
356
|
|
|
357
|
+
`last_updated` controls optional Git-based page metadata. If config uses `markdown.last_updated: "git"`, set `last_updated: none` on landing, index, or promotional pages that should not show an update date. If config uses `none`, set `last_updated: git` on a specific information page to opt in.
|
|
358
|
+
|
|
355
359
|
Use `meta` for small scalar flags and metadata. Use `data` when a theme should iterate structured content:
|
|
356
360
|
|
|
357
361
|
```html
|
|
@@ -411,7 +415,7 @@ progressive enhancement owned by the theme or site.
|
|
|
411
415
|
|
|
412
416
|
Build Pages reads `<source>/.zeropress/config.json` when present. Missing config falls back to defaults.
|
|
413
417
|
|
|
414
|
-
See the public config reference at [zeropress.dev/build-pages-config](https://zeropress.dev/build-pages
|
|
418
|
+
See the public config reference at [zeropress.dev/build-pages-config](https://zeropress.dev/build-pages/config/).
|
|
415
419
|
|
|
416
420
|
```json
|
|
417
421
|
{
|
|
@@ -438,6 +442,9 @@ See the public config reference at [zeropress.dev/build-pages-config](https://ze
|
|
|
438
442
|
"show_sponsor_banner": false
|
|
439
443
|
}
|
|
440
444
|
},
|
|
445
|
+
"markdown": {
|
|
446
|
+
"last_updated": "git"
|
|
447
|
+
},
|
|
441
448
|
"front_page": {
|
|
442
449
|
"type": "markdown"
|
|
443
450
|
},
|
|
@@ -457,6 +464,15 @@ See the public config reference at [zeropress.dev/build-pages-config](https://ze
|
|
|
457
464
|
]
|
|
458
465
|
}
|
|
459
466
|
},
|
|
467
|
+
"collections": {
|
|
468
|
+
"guides": {
|
|
469
|
+
"title": "Guides",
|
|
470
|
+
"items": [
|
|
471
|
+
"getting-started/index.md",
|
|
472
|
+
"deployment/index.md"
|
|
473
|
+
]
|
|
474
|
+
}
|
|
475
|
+
},
|
|
460
476
|
"custom_html": {
|
|
461
477
|
"head_end": { "file": ".zeropress/head-end.html" },
|
|
462
478
|
"body_end": { "file": ".zeropress/body-end.html" }
|
|
@@ -475,6 +491,22 @@ HTML front page and `custom_html` files must stay inside `.zeropress/`.
|
|
|
475
491
|
|
|
476
492
|
Menu item `meta` is optional scalar display metadata copied into generated preview-data for themes that manually iterate menus. Use it for small values such as `icon`, `badge`, or `accent`; arrays and objects are not accepted.
|
|
477
493
|
|
|
494
|
+
`collections` defines group-level reading order from Markdown source paths. Build Pages converts each source-relative `.md` path into preview-data collection items such as `{ "type": "page", "slug": "deployment" }`. Collection prev/next cursors stop at collection boundaries, so the last item in `collections.guides` does not continue into another collection.
|
|
495
|
+
|
|
496
|
+
`markdown.last_updated` is optional and accepts `none` or `git`. Missing or `none` keeps current behavior and generates no update date. `git` reads each Markdown file's latest Git commit date and adds `page.meta.last_updated_iso` plus `page.meta.last_updated` to generated preview-data when the page does not already define those meta keys. `last_updated_iso` keeps the Git ISO timestamp; `last_updated` is a stable `YYYY-MM-DD` fallback display string. For accurate history in GitHub Actions, configure checkout with `fetch-depth: 0`.
|
|
497
|
+
|
|
498
|
+
Themes can render the generated value with normal escaped interpolation:
|
|
499
|
+
|
|
500
|
+
```html
|
|
501
|
+
{{#if page.meta.last_updated_iso}}
|
|
502
|
+
<time datetime="{{page.meta.last_updated_iso}}" data-zp-local-date>
|
|
503
|
+
{{page.meta.last_updated}}
|
|
504
|
+
</time>
|
|
505
|
+
{{/if}}
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
Client-side progressive enhancement may replace the fallback text with a localized date. The fallback remains useful when JavaScript is unavailable.
|
|
509
|
+
|
|
478
510
|
`site.footer.copyright_text` is rendered by the bundled docs theme when present. If it is omitted, the bundled docs theme falls back to `site.title`. ZeroPress does not add a copyright symbol automatically.
|
|
479
511
|
|
|
480
512
|
The bundled docs theme shows `Published with ZeroPress.` by default. Set `site.footer.attribution` to `false` to hide it.
|
|
@@ -509,12 +541,12 @@ cp ./_site/_zeropress/search_pagefind.js ./_site/_zeropress/search.js
|
|
|
509
541
|
rm ./_site/_zeropress/search.json
|
|
510
542
|
```
|
|
511
543
|
|
|
512
|
-
## Workspace Internal `.zeropress/` Files
|
|
544
|
+
## Workspace Internal `.zeropress-build-page/` Files
|
|
513
545
|
|
|
514
|
-
Build Pages reads optional site config from `<source>/.zeropress/config.json`. Separately, it writes internal working files to `.zeropress/` in the current working directory. These generated working files are not the final deploy output. The final static site is written to the `destination` directory.
|
|
546
|
+
Build Pages reads optional user-authored site config from `<source>/.zeropress/config.json`. Separately, it writes generated internal working files to `.zeropress-build-page/` in the current working directory. These generated working files are not the final deploy output. The final static site is written to the `destination` directory.
|
|
515
547
|
|
|
516
548
|
```txt
|
|
517
|
-
.zeropress/
|
|
549
|
+
.zeropress-build-page/
|
|
518
550
|
build-pages-config.json
|
|
519
551
|
preview-data.json
|
|
520
552
|
build-report.json
|
package/action.yml
CHANGED
package/dist/action.js
CHANGED
|
@@ -53696,7 +53696,7 @@ async function validateThemeFiles(fileMap, options2 = {}) {
|
|
|
53696
53696
|
}
|
|
53697
53697
|
const content = getText(files.get(templatePath));
|
|
53698
53698
|
templateContents.set(templatePath, content);
|
|
53699
|
-
validateTemplateSyntax(templatePath, content, { errors, runtime: manifest?.runtime || DEFAULT_RUNTIME });
|
|
53699
|
+
validateTemplateSyntax(templatePath, content, { errors, warnings, runtime: manifest?.runtime || DEFAULT_RUNTIME });
|
|
53700
53700
|
}
|
|
53701
53701
|
for (const [filePath, value] of files.entries()) {
|
|
53702
53702
|
if (!filePath.startsWith("partials/") || !filePath.endsWith(".html")) {
|
|
@@ -53705,7 +53705,7 @@ async function validateThemeFiles(fileMap, options2 = {}) {
|
|
|
53705
53705
|
const partialName = filePath.slice("partials/".length, -".html".length);
|
|
53706
53706
|
const content = getText(value);
|
|
53707
53707
|
partialContents.set(partialName, content);
|
|
53708
|
-
validateTemplateSyntax(filePath, content, { errors, runtime: manifest?.runtime || DEFAULT_RUNTIME });
|
|
53708
|
+
validateTemplateSyntax(filePath, content, { errors, warnings, runtime: manifest?.runtime || DEFAULT_RUNTIME });
|
|
53709
53709
|
}
|
|
53710
53710
|
validatePartialReferences(templateContents, partialContents, { errors, runtime: manifest?.runtime || DEFAULT_RUNTIME });
|
|
53711
53711
|
return {
|
|
@@ -53922,10 +53922,22 @@ function validateManifest(themeJson) {
|
|
|
53922
53922
|
return { errors, manifest: errors.length > 0 ? void 0 : manifest };
|
|
53923
53923
|
}
|
|
53924
53924
|
function validateTemplateSyntax(templatePath, content, context) {
|
|
53925
|
-
const { errors } = context;
|
|
53925
|
+
const { errors, warnings = [] } = context;
|
|
53926
53926
|
const slotRegex = /\{\{slot:([a-zA-Z0-9_-]+)\}\}/g;
|
|
53927
53927
|
const contentSlotMatches = content.match(/\{\{slot:content\}\}/g) || [];
|
|
53928
53928
|
if (templatePath === "layout.html") {
|
|
53929
|
+
if (!startsWithHtmlDoctype(content)) {
|
|
53930
|
+
warnings.push(issue2(
|
|
53931
|
+
"MISSING_DOCTYPE",
|
|
53932
|
+
"layout.html",
|
|
53933
|
+
"layout.html should start with <!doctype html> to keep browsers in standards mode",
|
|
53934
|
+
"warning",
|
|
53935
|
+
{
|
|
53936
|
+
category: "theme_validation",
|
|
53937
|
+
hint: "Add <!doctype html> before the opening <html> tag."
|
|
53938
|
+
}
|
|
53939
|
+
));
|
|
53940
|
+
}
|
|
53929
53941
|
if (contentSlotMatches.length !== 1) {
|
|
53930
53942
|
errors.push(issue2("INVALID_LAYOUT_SLOT", "layout.html", "layout.html must contain exactly one {{slot:content}}", "error"));
|
|
53931
53943
|
}
|
|
@@ -53957,6 +53969,9 @@ function validateTemplateSyntax(templatePath, content, context) {
|
|
|
53957
53969
|
}
|
|
53958
53970
|
validateRuntimeV05TemplateSyntax(templatePath, content, errors);
|
|
53959
53971
|
}
|
|
53972
|
+
function startsWithHtmlDoctype(content) {
|
|
53973
|
+
return /^\s*(?:<!--[\s\S]*?-->\s*)*<!doctype\s+html\s*>/i.test(content);
|
|
53974
|
+
}
|
|
53960
53975
|
function validateRuntimeV05TemplateSyntax(templatePath, content, errors) {
|
|
53961
53976
|
const stack = [];
|
|
53962
53977
|
let index = 0;
|
|
@@ -61874,8 +61889,12 @@ function attachCollectionCursors(posts, pages, collections) {
|
|
|
61874
61889
|
if (!target) {
|
|
61875
61890
|
return;
|
|
61876
61891
|
}
|
|
61892
|
+
const cursor = buildCollectionCursor(collectionId, collection, items, index);
|
|
61877
61893
|
target.collection_cursors = target.collection_cursors || {};
|
|
61878
|
-
target.collection_cursors[collectionId] =
|
|
61894
|
+
target.collection_cursors[collectionId] = cursor;
|
|
61895
|
+
if (!target.collection_cursor) {
|
|
61896
|
+
target.collection_cursor = cursor;
|
|
61897
|
+
}
|
|
61879
61898
|
});
|
|
61880
61899
|
}
|
|
61881
61900
|
}
|
|
@@ -64222,9 +64241,13 @@ async function linkExists(siteDir, link2) {
|
|
|
64222
64241
|
var __dirname = path3.dirname(fileURLToPath(import.meta.url));
|
|
64223
64242
|
var packageDir = path3.resolve(__dirname, "..");
|
|
64224
64243
|
var prebuildScript = __dirname === path3.join(packageDir, "dist") ? path3.join(__dirname, "prebuild.js") : path3.join(packageDir, "src", "prebuild.js");
|
|
64225
|
-
var
|
|
64226
|
-
var
|
|
64227
|
-
var
|
|
64244
|
+
var INTERNAL_WORK_DIR = ".zeropress-build-page";
|
|
64245
|
+
var PREVIEW_DATA_PATH = `${INTERNAL_WORK_DIR}/preview-data.json`;
|
|
64246
|
+
var STAGING_DIR = `${INTERNAL_WORK_DIR}/public-assets`;
|
|
64247
|
+
var BUNDLED_THEME_ALIASES = /* @__PURE__ */ new Map([
|
|
64248
|
+
["docs", "docs"],
|
|
64249
|
+
["docs1", "docs"]
|
|
64250
|
+
]);
|
|
64228
64251
|
async function runBuildPages(options2) {
|
|
64229
64252
|
const cwd = path3.resolve(options2.cwd || process.cwd());
|
|
64230
64253
|
const copyMarkdownSource = options2.copyMarkdownSource !== false;
|
|
@@ -64232,7 +64255,7 @@ async function runBuildPages(options2) {
|
|
|
64232
64255
|
const publicDirExplicit = hasExplicitPublicDir(options2);
|
|
64233
64256
|
const publicDir = publicDirExplicit ? path3.resolve(cwd, options2.publicDir) : sourceDir;
|
|
64234
64257
|
const destinationDir = path3.resolve(cwd, options2.destination);
|
|
64235
|
-
const generatedDir = path3.join(cwd,
|
|
64258
|
+
const generatedDir = path3.join(cwd, INTERNAL_WORK_DIR);
|
|
64236
64259
|
const stagingDir = path3.join(cwd, STAGING_DIR);
|
|
64237
64260
|
const previewDataPath = path3.join(cwd, PREVIEW_DATA_PATH);
|
|
64238
64261
|
const themeDir = resolveThemeDir(cwd, options2);
|
|
@@ -64246,6 +64269,7 @@ async function runBuildPages(options2) {
|
|
|
64246
64269
|
generatedDir
|
|
64247
64270
|
});
|
|
64248
64271
|
await assertDirectory(sourceDir, "Source directory");
|
|
64272
|
+
await assertDirectory(themeDir, "Theme directory");
|
|
64249
64273
|
await assertPublicDirectory(publicDir, publicDirExplicit);
|
|
64250
64274
|
await assertDestinationPath(destinationDir);
|
|
64251
64275
|
await fs3.rm(generatedDir, { recursive: true, force: true });
|
|
@@ -64289,7 +64313,7 @@ async function runBuildPages(options2) {
|
|
|
64289
64313
|
process.env.ZEROPRESS_PUBLIC_DIR = stagingDir;
|
|
64290
64314
|
try {
|
|
64291
64315
|
const result = await runBuild(themeDir, previewData, destinationDir);
|
|
64292
|
-
console.log(
|
|
64316
|
+
console.log(formatBuildPagesSuccessMessage());
|
|
64293
64317
|
console.log(`Files: ${result.files.length}`);
|
|
64294
64318
|
console.log(`Output: ${formatPath(cwd, destinationDir)}`);
|
|
64295
64319
|
} finally {
|
|
@@ -64310,14 +64334,36 @@ async function runBuildPages(options2) {
|
|
|
64310
64334
|
console.log(`Checked ${result.htmlFiles.length} HTML files for internal links`);
|
|
64311
64335
|
}
|
|
64312
64336
|
}
|
|
64337
|
+
function formatBuildPagesSuccessMessage(stream = process.stdout) {
|
|
64338
|
+
return createColor2(stream).green("Built ZeroPress Pages site successfully");
|
|
64339
|
+
}
|
|
64340
|
+
function createColor2(stream) {
|
|
64341
|
+
const enabled = colorsEnabled(stream);
|
|
64342
|
+
const wrap = (code2, value) => enabled ? `\x1B[${code2}m${value}\x1B[0m` : value;
|
|
64343
|
+
return {
|
|
64344
|
+
red: (value) => wrap("31", value),
|
|
64345
|
+
yellow: (value) => wrap("33", value),
|
|
64346
|
+
green: (value) => wrap("32", value)
|
|
64347
|
+
};
|
|
64348
|
+
}
|
|
64349
|
+
function colorsEnabled(stream) {
|
|
64350
|
+
if (process.env.NO_COLOR) {
|
|
64351
|
+
return false;
|
|
64352
|
+
}
|
|
64353
|
+
if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0") {
|
|
64354
|
+
return true;
|
|
64355
|
+
}
|
|
64356
|
+
return Boolean(stream?.isTTY);
|
|
64357
|
+
}
|
|
64313
64358
|
function resolveThemeDir(cwd, options2) {
|
|
64314
64359
|
if (options2.themePath) {
|
|
64315
64360
|
return path3.resolve(cwd, options2.themePath);
|
|
64316
64361
|
}
|
|
64317
|
-
|
|
64318
|
-
|
|
64362
|
+
const canonicalTheme = BUNDLED_THEME_ALIASES.get(options2.theme);
|
|
64363
|
+
if (canonicalTheme) {
|
|
64364
|
+
return path3.join(packageDir, "themes", canonicalTheme);
|
|
64319
64365
|
}
|
|
64320
|
-
throw new Error(`Unknown bundled theme: ${options2.theme}`);
|
|
64366
|
+
throw new Error(`Unknown bundled theme: ${options2.theme}. Supported bundled themes: ${Array.from(BUNDLED_THEME_ALIASES.keys()).join(", ")}`);
|
|
64321
64367
|
}
|
|
64322
64368
|
function hasExplicitPublicDir(options2) {
|
|
64323
64369
|
return typeof options2.publicDir === "string" && Boolean(options2.publicDir.trim());
|
|
@@ -64389,11 +64435,11 @@ function assertBuildPagesPathLayout({
|
|
|
64389
64435
|
`Public directory must be a dedicated asset directory, not the current working directory. Received: ${formatPath(cwd, publicDir)}`
|
|
64390
64436
|
);
|
|
64391
64437
|
}
|
|
64392
|
-
assertNoPathOverlap(cwd, "Source directory", sourceDir,
|
|
64393
|
-
assertNoPathOverlap(cwd, "Destination directory", destinationDir,
|
|
64394
|
-
assertNoPathOverlap(cwd, "Theme directory", themeDir,
|
|
64438
|
+
assertNoPathOverlap(cwd, "Source directory", sourceDir, `internal ${INTERNAL_WORK_DIR} working directory`, generatedDir);
|
|
64439
|
+
assertNoPathOverlap(cwd, "Destination directory", destinationDir, `internal ${INTERNAL_WORK_DIR} working directory`, generatedDir);
|
|
64440
|
+
assertNoPathOverlap(cwd, "Theme directory", themeDir, `internal ${INTERNAL_WORK_DIR} working directory`, generatedDir);
|
|
64395
64441
|
if (!samePath(publicDir, sourceDir)) {
|
|
64396
|
-
assertNoPathOverlap(cwd, "Public directory", publicDir,
|
|
64442
|
+
assertNoPathOverlap(cwd, "Public directory", publicDir, `internal ${INTERNAL_WORK_DIR} working directory`, generatedDir);
|
|
64397
64443
|
assertNoPathOverlap(cwd, "Public directory", publicDir, "destination directory", destinationDir);
|
|
64398
64444
|
assertNoPathOverlap(cwd, "Public directory", publicDir, "theme directory", themeDir);
|
|
64399
64445
|
}
|
package/dist/prebuild.js
CHANGED
|
@@ -3519,15 +3519,18 @@ var require_gray_matter = __commonJS({
|
|
|
3519
3519
|
// src/prebuild.js
|
|
3520
3520
|
var import_gray_matter = __toESM(require_gray_matter(), 1);
|
|
3521
3521
|
import fs from "node:fs/promises";
|
|
3522
|
+
import { execFile } from "node:child_process";
|
|
3522
3523
|
import path from "node:path";
|
|
3524
|
+
import { promisify } from "node:util";
|
|
3523
3525
|
import { fileURLToPath } from "node:url";
|
|
3524
3526
|
var __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
3527
|
+
var execFileAsync = promisify(execFile);
|
|
3525
3528
|
var rootDir = process.cwd();
|
|
3526
3529
|
var sourceDir = resolveEnvPath(["ZEROPRESS_BUILD_PAGES_SOURCE"], "docs");
|
|
3527
3530
|
var publicDir = resolveEnvPath(["ZEROPRESS_BUILD_PAGES_PUBLIC_DIR"], sourceDir);
|
|
3528
3531
|
var defaultConfigPath = path.join(sourceDir, ".zeropress", "config.json");
|
|
3529
3532
|
var configPath = resolveOptionalEnvPath(["ZEROPRESS_BUILD_PAGES_CONFIG"], defaultConfigPath);
|
|
3530
|
-
var outDir = path.join(rootDir, ".zeropress");
|
|
3533
|
+
var outDir = path.join(rootDir, ".zeropress-build-page");
|
|
3531
3534
|
var buildPagesConfigPath = path.join(outDir, "build-pages-config.json");
|
|
3532
3535
|
var previewDataPath = path.join(outDir, "preview-data.json");
|
|
3533
3536
|
var buildReportPath = path.join(outDir, "build-report.json");
|
|
@@ -3541,6 +3544,7 @@ var FRONT_MATTER_DATA_MAX_DEPTH = 4;
|
|
|
3541
3544
|
var FRONT_MATTER_DATA_MAX_KEYS = 64;
|
|
3542
3545
|
var FRONT_MATTER_DATA_MAX_ARRAY_LENGTH = 256;
|
|
3543
3546
|
var FRONT_MATTER_DISCOVERABILITY_VALUES = /* @__PURE__ */ new Set(["default", "noindex", "delist"]);
|
|
3547
|
+
var MARKDOWN_LAST_UPDATED_VALUES = /* @__PURE__ */ new Set(["none", "git"]);
|
|
3544
3548
|
var markdownDiscoverExcludeRoots = buildMarkdownDiscoverExcludeRoots();
|
|
3545
3549
|
var PrebuildMarkdownError = class extends Error {
|
|
3546
3550
|
constructor(sourcePath, reason, expected = "", code = "invalid_markdown") {
|
|
@@ -3569,10 +3573,12 @@ async function main() {
|
|
|
3569
3573
|
);
|
|
3570
3574
|
const menus = normalizeMenus(config.menus);
|
|
3571
3575
|
const customHtmlConfig = normalizeCustomHtmlConfig(config.custom_html);
|
|
3576
|
+
const markdownConfig = normalizeMarkdownConfig(config.markdown);
|
|
3572
3577
|
const resolvedConfig = buildResolvedConfig(config, {
|
|
3573
3578
|
frontPageConfig,
|
|
3574
3579
|
menus,
|
|
3575
|
-
customHtmlConfig
|
|
3580
|
+
customHtmlConfig,
|
|
3581
|
+
markdownConfig
|
|
3576
3582
|
});
|
|
3577
3583
|
const sourceFiles = await listMarkdownFiles(sourceDir);
|
|
3578
3584
|
const skippedMarkdown = [];
|
|
@@ -3607,21 +3613,29 @@ async function main() {
|
|
|
3607
3613
|
const routeBySourcePath = new Map(
|
|
3608
3614
|
pageInputs.map(({ sourcePath, route }) => [sourcePath, route])
|
|
3609
3615
|
);
|
|
3610
|
-
const
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3616
|
+
const collections = normalizeCollections(config.collections, pageInputs, skippedMarkdown);
|
|
3617
|
+
if (Object.keys(collections).length > 0) {
|
|
3618
|
+
resolvedConfig.collections = collections;
|
|
3619
|
+
}
|
|
3620
|
+
const pages = [];
|
|
3621
|
+
for (const { sourcePath, bodyMarkdown, frontMatter, title, route } of pageInputs) {
|
|
3622
|
+
const meta = await buildPageMeta(sourcePath, frontMatter, markdownConfig);
|
|
3623
|
+
pages.push({
|
|
3624
|
+
title,
|
|
3625
|
+
slug: route.slug,
|
|
3626
|
+
path: route.path,
|
|
3627
|
+
meta: {
|
|
3628
|
+
...meta,
|
|
3629
|
+
...copyMarkdownSource ? { source_markdown_url: buildSourceMarkdownUrl(sourcePath) } : {}
|
|
3630
|
+
},
|
|
3631
|
+
...frontMatter.data !== void 0 ? { data: frontMatter.data } : {},
|
|
3632
|
+
...frontMatter.discoverability !== "default" ? { discoverability: frontMatter.discoverability } : {},
|
|
3633
|
+
content: rewriteMarkdownLinks(bodyMarkdown, sourcePath, routeBySourcePath),
|
|
3634
|
+
document_type: "markdown",
|
|
3635
|
+
excerpt: frontMatter.description || extractExcerpt(bodyMarkdown, title),
|
|
3636
|
+
status: "published"
|
|
3637
|
+
});
|
|
3638
|
+
}
|
|
3625
3639
|
const frontPageResult = await buildFrontPageData(frontPageConfig, pageInputs, resolvedConfig);
|
|
3626
3640
|
if (frontPageResult.page) {
|
|
3627
3641
|
pages.push(frontPageResult.page);
|
|
@@ -3644,6 +3658,9 @@ async function main() {
|
|
|
3644
3658
|
menus,
|
|
3645
3659
|
widgets: {}
|
|
3646
3660
|
};
|
|
3661
|
+
if (Object.keys(collections).length > 0) {
|
|
3662
|
+
previewData.collections = collections;
|
|
3663
|
+
}
|
|
3647
3664
|
if (customHtml) {
|
|
3648
3665
|
previewData.custom_html = customHtml;
|
|
3649
3666
|
}
|
|
@@ -3756,11 +3773,12 @@ function buildSiteData(config, frontPage) {
|
|
|
3756
3773
|
}
|
|
3757
3774
|
return site;
|
|
3758
3775
|
}
|
|
3759
|
-
function buildResolvedConfig(config, { frontPageConfig, menus, customHtmlConfig }) {
|
|
3776
|
+
function buildResolvedConfig(config, { frontPageConfig, menus, customHtmlConfig, markdownConfig }) {
|
|
3760
3777
|
const resolvedConfig = {
|
|
3761
3778
|
$schema: BUILD_PAGES_CONFIG_SCHEMA_URL,
|
|
3762
3779
|
version: "0.1",
|
|
3763
3780
|
site: normalizeSiteConfig(config.site),
|
|
3781
|
+
markdown: markdownConfig,
|
|
3764
3782
|
front_page: frontPageConfig,
|
|
3765
3783
|
menus
|
|
3766
3784
|
};
|
|
@@ -3769,6 +3787,23 @@ function buildResolvedConfig(config, { frontPageConfig, menus, customHtmlConfig
|
|
|
3769
3787
|
}
|
|
3770
3788
|
return resolvedConfig;
|
|
3771
3789
|
}
|
|
3790
|
+
function normalizeMarkdownConfig(value) {
|
|
3791
|
+
if (value === void 0) {
|
|
3792
|
+
return {
|
|
3793
|
+
last_updated: "none"
|
|
3794
|
+
};
|
|
3795
|
+
}
|
|
3796
|
+
if (!isPlainObject(value)) {
|
|
3797
|
+
throw new PrebuildConfigError(
|
|
3798
|
+
"markdown must be an object.",
|
|
3799
|
+
' "markdown": { "last_updated": "git" }'
|
|
3800
|
+
);
|
|
3801
|
+
}
|
|
3802
|
+
assertKnownConfigKeys(value, ["last_updated"], "markdown");
|
|
3803
|
+
return {
|
|
3804
|
+
last_updated: normalizeLastUpdatedPolicy(value.last_updated, "markdown.last_updated", PrebuildConfigError)
|
|
3805
|
+
};
|
|
3806
|
+
}
|
|
3772
3807
|
function normalizeSiteConfig(value) {
|
|
3773
3808
|
if (value !== void 0 && !isPlainObject(value)) {
|
|
3774
3809
|
throw new PrebuildConfigError(
|
|
@@ -4260,6 +4295,69 @@ function defaultMenus() {
|
|
|
4260
4295
|
}
|
|
4261
4296
|
};
|
|
4262
4297
|
}
|
|
4298
|
+
function normalizeCollections(value, pageInputs, skippedMarkdown) {
|
|
4299
|
+
if (value === void 0) {
|
|
4300
|
+
return {};
|
|
4301
|
+
}
|
|
4302
|
+
if (!isPlainObject(value)) {
|
|
4303
|
+
throw new PrebuildConfigError("collections must be an object keyed by collection id.");
|
|
4304
|
+
}
|
|
4305
|
+
const pageBySourcePath = new Map(pageInputs.map((pageInput) => [pageInput.sourcePath, pageInput]));
|
|
4306
|
+
const skippedByFile = new Map(
|
|
4307
|
+
skippedMarkdown.map((entry) => [path.resolve(rootDir, entry.file), entry.reason])
|
|
4308
|
+
);
|
|
4309
|
+
const collections = {};
|
|
4310
|
+
for (const [collectionId, collection] of Object.entries(value)) {
|
|
4311
|
+
validateConfigId(collectionId, `collections.${collectionId}`);
|
|
4312
|
+
if (!isPlainObject(collection)) {
|
|
4313
|
+
throw new PrebuildConfigError(`collections.${collectionId} must be an object.`);
|
|
4314
|
+
}
|
|
4315
|
+
assertKnownConfigKeys(collection, ["title", "description", "items"], `collections.${collectionId}`);
|
|
4316
|
+
if (!Array.isArray(collection.items)) {
|
|
4317
|
+
throw new PrebuildConfigError(`collections.${collectionId}.items must be an array of Markdown source paths.`);
|
|
4318
|
+
}
|
|
4319
|
+
const seenSourcePaths = /* @__PURE__ */ new Set();
|
|
4320
|
+
const items = collection.items.map((item, index) => {
|
|
4321
|
+
const pathLabel = `collections.${collectionId}.items[${index}]`;
|
|
4322
|
+
const normalizedPath = resolveCollectionSourcePath(item, pathLabel);
|
|
4323
|
+
const sourcePath = path.resolve(sourceDir, normalizedPath);
|
|
4324
|
+
if (seenSourcePaths.has(sourcePath)) {
|
|
4325
|
+
throw new PrebuildConfigError(`${pathLabel} duplicates ${normalizedPath} in collections.${collectionId}.`);
|
|
4326
|
+
}
|
|
4327
|
+
seenSourcePaths.add(sourcePath);
|
|
4328
|
+
const pageInput = pageBySourcePath.get(sourcePath);
|
|
4329
|
+
if (!pageInput) {
|
|
4330
|
+
const skippedReason = skippedByFile.get(sourcePath);
|
|
4331
|
+
if (skippedReason) {
|
|
4332
|
+
throw new PrebuildConfigError(`${pathLabel} references skipped Markdown ${normalizedPath}: ${skippedReason}`);
|
|
4333
|
+
}
|
|
4334
|
+
throw new PrebuildConfigError(`${pathLabel} was not discovered as a Markdown page: ${normalizedPath}`);
|
|
4335
|
+
}
|
|
4336
|
+
return {
|
|
4337
|
+
type: "page",
|
|
4338
|
+
slug: pageInput.route.slug
|
|
4339
|
+
};
|
|
4340
|
+
});
|
|
4341
|
+
collections[collectionId] = {
|
|
4342
|
+
title: readConfigString(collection.title, collectionId),
|
|
4343
|
+
...collection.description !== void 0 ? { description: readConfigString(collection.description, "") } : {},
|
|
4344
|
+
items
|
|
4345
|
+
};
|
|
4346
|
+
}
|
|
4347
|
+
return collections;
|
|
4348
|
+
}
|
|
4349
|
+
function resolveCollectionSourcePath(value, pathLabel) {
|
|
4350
|
+
const normalizedPath = normalizeSourceFilePath(value, pathLabel);
|
|
4351
|
+
if (!normalizedPath.toLowerCase().endsWith(".md")) {
|
|
4352
|
+
throw new PrebuildConfigError(`${pathLabel} must be a Markdown source path ending in .md.`);
|
|
4353
|
+
}
|
|
4354
|
+
return normalizedPath;
|
|
4355
|
+
}
|
|
4356
|
+
function validateConfigId(value, pathLabel) {
|
|
4357
|
+
if (!/^[a-z][a-z0-9_-]{0,63}$/.test(value)) {
|
|
4358
|
+
throw new PrebuildConfigError(`${pathLabel} must use a lowercase config id such as "docs" or "reference-guides".`);
|
|
4359
|
+
}
|
|
4360
|
+
}
|
|
4263
4361
|
function buildPrebuildReport({
|
|
4264
4362
|
sourceFiles,
|
|
4265
4363
|
pageInputs,
|
|
@@ -4368,11 +4466,37 @@ function normalizePublishedFrontMatter(frontMatter, sourcePath) {
|
|
|
4368
4466
|
title: normalizeFrontMatterTitle(frontMatter.title, sourcePath),
|
|
4369
4467
|
description: normalizeFrontMatterDescription(frontMatter.description, sourcePath),
|
|
4370
4468
|
path: normalizeFrontMatterRoutePath(frontMatter.path, sourcePath),
|
|
4469
|
+
last_updated: normalizeFrontMatterLastUpdated(frontMatter.last_updated, sourcePath),
|
|
4371
4470
|
discoverability: normalizeFrontMatterDiscoverability(frontMatter.discoverability, sourcePath),
|
|
4372
4471
|
meta: normalizeFrontMatterMeta(frontMatter.meta, sourcePath),
|
|
4373
4472
|
data: normalizeFrontMatterData(frontMatter.data, sourcePath)
|
|
4374
4473
|
};
|
|
4375
4474
|
}
|
|
4475
|
+
function normalizeLastUpdatedPolicy(value, pathLabel, ErrorClass, sourcePath = null) {
|
|
4476
|
+
if (value === void 0) {
|
|
4477
|
+
return "none";
|
|
4478
|
+
}
|
|
4479
|
+
if (typeof value === "string" && MARKDOWN_LAST_UPDATED_VALUES.has(value)) {
|
|
4480
|
+
return value;
|
|
4481
|
+
}
|
|
4482
|
+
if (ErrorClass === PrebuildMarkdownError) {
|
|
4483
|
+
throw new ErrorClass(
|
|
4484
|
+
sourcePath,
|
|
4485
|
+
`${pathLabel} must be one of: ${Array.from(MARKDOWN_LAST_UPDATED_VALUES).join(", ")}.`,
|
|
4486
|
+
" last_updated: none\n last_updated: git"
|
|
4487
|
+
);
|
|
4488
|
+
}
|
|
4489
|
+
throw new ErrorClass(
|
|
4490
|
+
`${pathLabel} must be one of: ${Array.from(MARKDOWN_LAST_UPDATED_VALUES).join(", ")}.`,
|
|
4491
|
+
' "markdown": { "last_updated": "none" }\n "markdown": { "last_updated": "git" }'
|
|
4492
|
+
);
|
|
4493
|
+
}
|
|
4494
|
+
function normalizeFrontMatterLastUpdated(value, sourcePath) {
|
|
4495
|
+
if (value === void 0) {
|
|
4496
|
+
return void 0;
|
|
4497
|
+
}
|
|
4498
|
+
return normalizeLastUpdatedPolicy(value, "front matter last_updated", PrebuildMarkdownError, sourcePath);
|
|
4499
|
+
}
|
|
4376
4500
|
function normalizeFrontMatterTitle(value, sourcePath) {
|
|
4377
4501
|
if (value === void 0) {
|
|
4378
4502
|
return "";
|
|
@@ -4456,6 +4580,74 @@ function normalizeFrontMatterMeta(value, sourcePath) {
|
|
|
4456
4580
|
}
|
|
4457
4581
|
return meta;
|
|
4458
4582
|
}
|
|
4583
|
+
async function buildPageMeta(sourcePath, frontMatter, markdownConfig) {
|
|
4584
|
+
const meta = {
|
|
4585
|
+
...frontMatter.meta
|
|
4586
|
+
};
|
|
4587
|
+
if (hasManualLastUpdatedMeta(meta)) {
|
|
4588
|
+
return meta;
|
|
4589
|
+
}
|
|
4590
|
+
const lastUpdatedPolicy = frontMatter.last_updated || markdownConfig.last_updated;
|
|
4591
|
+
if (lastUpdatedPolicy !== "git") {
|
|
4592
|
+
return meta;
|
|
4593
|
+
}
|
|
4594
|
+
const lastUpdatedIso = await readGitLastUpdatedIso(sourcePath);
|
|
4595
|
+
if (!lastUpdatedIso) {
|
|
4596
|
+
return meta;
|
|
4597
|
+
}
|
|
4598
|
+
return {
|
|
4599
|
+
...meta,
|
|
4600
|
+
last_updated_iso: lastUpdatedIso,
|
|
4601
|
+
last_updated: lastUpdatedIso.slice(0, 10)
|
|
4602
|
+
};
|
|
4603
|
+
}
|
|
4604
|
+
function hasManualLastUpdatedMeta(meta) {
|
|
4605
|
+
return Object.hasOwn(meta, "last_updated") || Object.hasOwn(meta, "last_updated_iso");
|
|
4606
|
+
}
|
|
4607
|
+
async function readGitLastUpdatedIso(sourcePath) {
|
|
4608
|
+
const realSourcePath = await resolveRealPath(sourcePath);
|
|
4609
|
+
const realRootDir = await resolveRealPath(rootDir);
|
|
4610
|
+
const gitPath = path.relative(realRootDir, realSourcePath);
|
|
4611
|
+
try {
|
|
4612
|
+
const { stdout } = await execFileAsync("git", [
|
|
4613
|
+
"-C",
|
|
4614
|
+
realRootDir,
|
|
4615
|
+
"log",
|
|
4616
|
+
"-1",
|
|
4617
|
+
"--format=%cI",
|
|
4618
|
+
"--",
|
|
4619
|
+
gitPath
|
|
4620
|
+
], {
|
|
4621
|
+
encoding: "utf8"
|
|
4622
|
+
});
|
|
4623
|
+
const value = stdout.trim();
|
|
4624
|
+
if (!value) {
|
|
4625
|
+
warnGitLastUpdated(sourcePath, "no commit date was found for this file.");
|
|
4626
|
+
return "";
|
|
4627
|
+
}
|
|
4628
|
+
if (!/^\d{4}-\d{2}-\d{2}T/.test(value)) {
|
|
4629
|
+
warnGitLastUpdated(sourcePath, `unexpected git date output: ${value}`);
|
|
4630
|
+
return "";
|
|
4631
|
+
}
|
|
4632
|
+
return value;
|
|
4633
|
+
} catch (error) {
|
|
4634
|
+
warnGitLastUpdated(sourcePath, error instanceof Error ? error.message : String(error));
|
|
4635
|
+
return "";
|
|
4636
|
+
}
|
|
4637
|
+
}
|
|
4638
|
+
async function resolveRealPath(value) {
|
|
4639
|
+
try {
|
|
4640
|
+
return await fs.realpath(value);
|
|
4641
|
+
} catch {
|
|
4642
|
+
return value;
|
|
4643
|
+
}
|
|
4644
|
+
}
|
|
4645
|
+
function warnGitLastUpdated(sourcePath, reason) {
|
|
4646
|
+
console.warn([
|
|
4647
|
+
`[zeropress-build-pages] Warning: could not read git last_updated for ${formatSourcePath(sourcePath)}.`,
|
|
4648
|
+
`Reason: ${reason}`
|
|
4649
|
+
].join("\n"));
|
|
4650
|
+
}
|
|
4459
4651
|
function isPreviewMetaValue(value) {
|
|
4460
4652
|
return value === null || typeof value === "string" || typeof value === "number" && Number.isFinite(value) || typeof value === "boolean";
|
|
4461
4653
|
}
|
|
@@ -4673,9 +4865,11 @@ function buildRoutePath(relativeSourcePath, sourcePath, options2 = {}) {
|
|
|
4673
4865
|
return routePath;
|
|
4674
4866
|
}
|
|
4675
4867
|
function buildSlug(routePath) {
|
|
4676
|
-
const segments = routePath.split("/");
|
|
4677
|
-
|
|
4678
|
-
|
|
4868
|
+
const segments = routePath.split("/").filter(Boolean);
|
|
4869
|
+
if (segments.length > 1 && segments.at(-1) === "index") {
|
|
4870
|
+
segments.pop();
|
|
4871
|
+
}
|
|
4872
|
+
return sanitizePathSegment(segments.join("-") || "index");
|
|
4679
4873
|
}
|
|
4680
4874
|
function sanitizePathSegment(segment) {
|
|
4681
4875
|
return segment.replace(/[^a-z0-9.-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeropress/build-pages",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"description": "ZeroPress Markdown build action and CLI for static hosting",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"node": ">=18.18.0"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@zeropress/build": "0.6.
|
|
43
|
+
"@zeropress/build": "0.6.4",
|
|
44
44
|
"gray-matter": "4.0.3"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|