@zeropress/build-pages 0.6.2 → 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 +158 -33
- package/action.yml +1 -1
- package/dist/action.js +303 -142
- package/dist/prebuild.js +303 -25
- package/package.json +2 -2
- package/schemas/zeropress-build-pages.config.v0.1.schema.json +110 -3
- package/src/index.js +71 -13
- package/src/prebuild.js +349 -26
- package/themes/docs/404.html +13 -2
- package/themes/docs/assets/style.css +861 -253
- package/themes/docs/assets/theme.js +236 -4
- package/themes/docs/layout.html +58 -15
- package/themes/docs/page.html +12 -13
- package/themes/docs/partials/theme-bootstrap.html +10 -0
- package/themes/docs/post.html +37 -7
- package/themes/docs/theme.json +9 -4
package/dist/prebuild.js
CHANGED
|
@@ -3519,28 +3519,32 @@ 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");
|
|
3534
3537
|
var skipUntitledMarkdown = readBooleanEnv("ZEROPRESS_SKIP_UNTITLED_MARKDOWN");
|
|
3535
3538
|
var copyMarkdownSource = readBooleanEnv("ZEROPRESS_COPY_MARKDOWN_SOURCE", true);
|
|
3536
3539
|
var FRONT_PAGE_TYPES = /* @__PURE__ */ new Set(["theme_index", "markdown", "html"]);
|
|
3537
|
-
var BUILD_PAGES_CONFIG_SCHEMA_URL = "https://zeropress.dev/
|
|
3538
|
-
var PREVIEW_DATA_SCHEMA_URL = "https://zeropress.dev/
|
|
3540
|
+
var BUILD_PAGES_CONFIG_SCHEMA_URL = "https://schemas.zeropress.dev/build-pages-config/v0.1/schema.json";
|
|
3541
|
+
var PREVIEW_DATA_SCHEMA_URL = "https://schemas.zeropress.dev/preview-data/v0.6/schema.json";
|
|
3539
3542
|
var FRONT_MATTER_DATA_KEY_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*(?:-[a-zA-Z0-9_]+)*$/;
|
|
3540
3543
|
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
|
}
|
|
@@ -3729,7 +3746,7 @@ function buildSiteData(config, frontPage) {
|
|
|
3729
3746
|
description: configuredSite.description,
|
|
3730
3747
|
url: configuredSite.url,
|
|
3731
3748
|
media_base_url: "",
|
|
3732
|
-
locale:
|
|
3749
|
+
locale: configuredSite.locale,
|
|
3733
3750
|
posts_per_page: 10,
|
|
3734
3751
|
datetime_display: "static",
|
|
3735
3752
|
date_style: "medium",
|
|
@@ -3745,16 +3762,23 @@ function buildSiteData(config, frontPage) {
|
|
|
3745
3762
|
search: configuredSite.search !== false,
|
|
3746
3763
|
indexing: configuredSite.indexing !== false
|
|
3747
3764
|
};
|
|
3765
|
+
if (configuredSite.logo) {
|
|
3766
|
+
site.logo = configuredSite.logo;
|
|
3767
|
+
}
|
|
3768
|
+
if (configuredSite.meta !== void 0) {
|
|
3769
|
+
site.meta = configuredSite.meta;
|
|
3770
|
+
}
|
|
3748
3771
|
if (configuredSite.footer) {
|
|
3749
3772
|
site.footer = configuredSite.footer;
|
|
3750
3773
|
}
|
|
3751
3774
|
return site;
|
|
3752
3775
|
}
|
|
3753
|
-
function buildResolvedConfig(config, { frontPageConfig, menus, customHtmlConfig }) {
|
|
3776
|
+
function buildResolvedConfig(config, { frontPageConfig, menus, customHtmlConfig, markdownConfig }) {
|
|
3754
3777
|
const resolvedConfig = {
|
|
3755
3778
|
$schema: BUILD_PAGES_CONFIG_SCHEMA_URL,
|
|
3756
3779
|
version: "0.1",
|
|
3757
3780
|
site: normalizeSiteConfig(config.site),
|
|
3781
|
+
markdown: markdownConfig,
|
|
3758
3782
|
front_page: frontPageConfig,
|
|
3759
3783
|
menus
|
|
3760
3784
|
};
|
|
@@ -3763,6 +3787,23 @@ function buildResolvedConfig(config, { frontPageConfig, menus, customHtmlConfig
|
|
|
3763
3787
|
}
|
|
3764
3788
|
return resolvedConfig;
|
|
3765
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
|
+
}
|
|
3766
3807
|
function normalizeSiteConfig(value) {
|
|
3767
3808
|
if (value !== void 0 && !isPlainObject(value)) {
|
|
3768
3809
|
throw new PrebuildConfigError(
|
|
@@ -3771,21 +3812,83 @@ function normalizeSiteConfig(value) {
|
|
|
3771
3812
|
);
|
|
3772
3813
|
}
|
|
3773
3814
|
const configuredSite = isPlainObject(value) ? value : {};
|
|
3774
|
-
assertKnownConfigKeys(configuredSite, ["title", "description", "url", "expose_generator", "search", "indexing", "footer"], "site");
|
|
3815
|
+
assertKnownConfigKeys(configuredSite, ["title", "description", "url", "logo", "locale", "expose_generator", "search", "indexing", "footer", "meta"], "site");
|
|
3775
3816
|
const site = {
|
|
3776
3817
|
title: readConfigString(configuredSite.title, "Documentation"),
|
|
3777
3818
|
description: readConfigString(configuredSite.description, "A documentation site."),
|
|
3778
3819
|
url: readEnv("ZEROPRESS_SITE_URL", readConfigString(configuredSite.url, "")),
|
|
3820
|
+
locale: normalizeSiteLocale(configuredSite.locale),
|
|
3779
3821
|
expose_generator: readConfigBoolean(configuredSite.expose_generator, true, "site.expose_generator"),
|
|
3780
3822
|
search: readConfigBoolean(configuredSite.search, true, "site.search"),
|
|
3781
3823
|
indexing: readConfigBoolean(configuredSite.indexing, true, "site.indexing")
|
|
3782
3824
|
};
|
|
3825
|
+
const logo = normalizeSiteLogo(configuredSite.logo);
|
|
3826
|
+
if (logo) {
|
|
3827
|
+
site.logo = logo;
|
|
3828
|
+
}
|
|
3783
3829
|
const footer = normalizeFooter(configuredSite.footer);
|
|
3784
3830
|
if (footer) {
|
|
3785
3831
|
site.footer = footer;
|
|
3786
3832
|
}
|
|
3833
|
+
if (configuredSite.meta !== void 0) {
|
|
3834
|
+
site.meta = normalizeSiteMeta(configuredSite.meta, "site.meta");
|
|
3835
|
+
}
|
|
3787
3836
|
return site;
|
|
3788
3837
|
}
|
|
3838
|
+
function normalizeSiteLocale(value) {
|
|
3839
|
+
if (value === void 0) {
|
|
3840
|
+
return "en-US";
|
|
3841
|
+
}
|
|
3842
|
+
if (typeof value !== "string") {
|
|
3843
|
+
throw new PrebuildConfigError("site.locale must be a string when provided.");
|
|
3844
|
+
}
|
|
3845
|
+
const locale = value.trim();
|
|
3846
|
+
if (locale.length < 2) {
|
|
3847
|
+
throw new PrebuildConfigError('site.locale must be a non-empty locale string such as "en-US" or "ko-KR".');
|
|
3848
|
+
}
|
|
3849
|
+
return locale;
|
|
3850
|
+
}
|
|
3851
|
+
function normalizeSiteLogo(value) {
|
|
3852
|
+
if (value === void 0) {
|
|
3853
|
+
return void 0;
|
|
3854
|
+
}
|
|
3855
|
+
if (!isPlainObject(value)) {
|
|
3856
|
+
throw new PrebuildConfigError("site.logo must be an object when provided.");
|
|
3857
|
+
}
|
|
3858
|
+
assertKnownConfigKeys(value, ["src", "alt"], "site.logo");
|
|
3859
|
+
const src = readConfigString(value.src, "");
|
|
3860
|
+
if (!src) {
|
|
3861
|
+
throw new PrebuildConfigError(
|
|
3862
|
+
"site.logo.src must be a non-empty URL-like string.",
|
|
3863
|
+
' "logo": { "src": "/logo.svg", "alt": "My Site" }'
|
|
3864
|
+
);
|
|
3865
|
+
}
|
|
3866
|
+
validateUrlLikeString(src, "site.logo.src");
|
|
3867
|
+
const logo = { src };
|
|
3868
|
+
if (value.alt !== void 0) {
|
|
3869
|
+
if (typeof value.alt !== "string") {
|
|
3870
|
+
throw new PrebuildConfigError("site.logo.alt must be a string when provided.");
|
|
3871
|
+
}
|
|
3872
|
+
const alt = value.alt.trim();
|
|
3873
|
+
if (alt) {
|
|
3874
|
+
logo.alt = alt;
|
|
3875
|
+
}
|
|
3876
|
+
}
|
|
3877
|
+
return logo;
|
|
3878
|
+
}
|
|
3879
|
+
function normalizeSiteMeta(value, pathLabel) {
|
|
3880
|
+
if (!isPlainObject(value)) {
|
|
3881
|
+
throw new PrebuildConfigError(`${pathLabel} must be an object when provided.`);
|
|
3882
|
+
}
|
|
3883
|
+
const meta = {};
|
|
3884
|
+
for (const [key, metaValue] of Object.entries(value)) {
|
|
3885
|
+
if (!isPreviewMetaValue(metaValue)) {
|
|
3886
|
+
throw new PrebuildConfigError(`${pathLabel}.${key} must be a string, number, boolean, or null.`);
|
|
3887
|
+
}
|
|
3888
|
+
meta[key] = metaValue;
|
|
3889
|
+
}
|
|
3890
|
+
return meta;
|
|
3891
|
+
}
|
|
3789
3892
|
function normalizeFooter(value) {
|
|
3790
3893
|
if (value === void 0) {
|
|
3791
3894
|
return void 0;
|
|
@@ -3807,6 +3910,22 @@ function normalizeFooter(value) {
|
|
|
3807
3910
|
}
|
|
3808
3911
|
return Object.keys(footer).length ? footer : void 0;
|
|
3809
3912
|
}
|
|
3913
|
+
function validateUrlLikeString(value, pathLabel) {
|
|
3914
|
+
if (value.startsWith("//")) {
|
|
3915
|
+
throw new PrebuildConfigError(`${pathLabel} must be an absolute URL or a safe relative path.`);
|
|
3916
|
+
}
|
|
3917
|
+
if (value.startsWith("/") || value.startsWith("./") || value.startsWith("../")) {
|
|
3918
|
+
return;
|
|
3919
|
+
}
|
|
3920
|
+
try {
|
|
3921
|
+
const url = new URL(value);
|
|
3922
|
+
if (!url.protocol || !url.hostname) {
|
|
3923
|
+
throw new Error("missing host");
|
|
3924
|
+
}
|
|
3925
|
+
} catch {
|
|
3926
|
+
throw new PrebuildConfigError(`${pathLabel} must be an absolute URL or a safe relative path.`);
|
|
3927
|
+
}
|
|
3928
|
+
}
|
|
3810
3929
|
function readConfigBoolean(value, fallback, pathName) {
|
|
3811
3930
|
if (value === void 0) {
|
|
3812
3931
|
return fallback;
|
|
@@ -4176,6 +4295,69 @@ function defaultMenus() {
|
|
|
4176
4295
|
}
|
|
4177
4296
|
};
|
|
4178
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
|
+
}
|
|
4179
4361
|
function buildPrebuildReport({
|
|
4180
4362
|
sourceFiles,
|
|
4181
4363
|
pageInputs,
|
|
@@ -4284,11 +4466,37 @@ function normalizePublishedFrontMatter(frontMatter, sourcePath) {
|
|
|
4284
4466
|
title: normalizeFrontMatterTitle(frontMatter.title, sourcePath),
|
|
4285
4467
|
description: normalizeFrontMatterDescription(frontMatter.description, sourcePath),
|
|
4286
4468
|
path: normalizeFrontMatterRoutePath(frontMatter.path, sourcePath),
|
|
4469
|
+
last_updated: normalizeFrontMatterLastUpdated(frontMatter.last_updated, sourcePath),
|
|
4287
4470
|
discoverability: normalizeFrontMatterDiscoverability(frontMatter.discoverability, sourcePath),
|
|
4288
4471
|
meta: normalizeFrontMatterMeta(frontMatter.meta, sourcePath),
|
|
4289
4472
|
data: normalizeFrontMatterData(frontMatter.data, sourcePath)
|
|
4290
4473
|
};
|
|
4291
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
|
+
}
|
|
4292
4500
|
function normalizeFrontMatterTitle(value, sourcePath) {
|
|
4293
4501
|
if (value === void 0) {
|
|
4294
4502
|
return "";
|
|
@@ -4372,6 +4580,74 @@ function normalizeFrontMatterMeta(value, sourcePath) {
|
|
|
4372
4580
|
}
|
|
4373
4581
|
return meta;
|
|
4374
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
|
+
}
|
|
4375
4651
|
function isPreviewMetaValue(value) {
|
|
4376
4652
|
return value === null || typeof value === "string" || typeof value === "number" && Number.isFinite(value) || typeof value === "boolean";
|
|
4377
4653
|
}
|
|
@@ -4589,9 +4865,11 @@ function buildRoutePath(relativeSourcePath, sourcePath, options2 = {}) {
|
|
|
4589
4865
|
return routePath;
|
|
4590
4866
|
}
|
|
4591
4867
|
function buildSlug(routePath) {
|
|
4592
|
-
const segments = routePath.split("/");
|
|
4593
|
-
|
|
4594
|
-
|
|
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");
|
|
4595
4873
|
}
|
|
4596
4874
|
function sanitizePathSegment(segment) {
|
|
4597
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": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "https://zeropress.dev/
|
|
3
|
+
"$id": "https://schemas.zeropress.dev/build-pages-config/v0.1/schema.json",
|
|
4
4
|
"title": "ZeroPress Build Pages Config v0.1",
|
|
5
5
|
"description": "Optional source-directory configuration for @zeropress/build-pages workflows.",
|
|
6
6
|
"markdownDescription": "Optional source-directory configuration for `@zeropress/build-pages` workflows.",
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
"site": {
|
|
22
22
|
"$ref": "#/$defs/site"
|
|
23
23
|
},
|
|
24
|
+
"markdown": {
|
|
25
|
+
"$ref": "#/$defs/markdown"
|
|
26
|
+
},
|
|
24
27
|
"front_page": {
|
|
25
28
|
"$ref": "#/$defs/frontPage"
|
|
26
29
|
},
|
|
@@ -29,6 +32,9 @@
|
|
|
29
32
|
},
|
|
30
33
|
"menus": {
|
|
31
34
|
"$ref": "#/$defs/menus"
|
|
35
|
+
},
|
|
36
|
+
"collections": {
|
|
37
|
+
"$ref": "#/$defs/collections"
|
|
32
38
|
}
|
|
33
39
|
},
|
|
34
40
|
"$defs": {
|
|
@@ -50,6 +56,15 @@
|
|
|
50
56
|
"description": "Canonical site URL. Use an empty string or omit this field for local builds without canonical output.",
|
|
51
57
|
"markdownDescription": "Canonical site URL. Use an empty string or omit this field for local builds without canonical output."
|
|
52
58
|
},
|
|
59
|
+
"logo": {
|
|
60
|
+
"$ref": "#/$defs/siteLogo"
|
|
61
|
+
},
|
|
62
|
+
"locale": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"minLength": 2,
|
|
65
|
+
"description": "Site locale used for generated preview-data language metadata, such as html lang and feed language.",
|
|
66
|
+
"markdownDescription": "Site locale used for generated preview-data language metadata, such as HTML `lang` and feed language."
|
|
67
|
+
},
|
|
53
68
|
"expose_generator": {
|
|
54
69
|
"type": "boolean",
|
|
55
70
|
"default": true,
|
|
@@ -70,6 +85,29 @@
|
|
|
70
85
|
},
|
|
71
86
|
"footer": {
|
|
72
87
|
"$ref": "#/$defs/siteFooter"
|
|
88
|
+
},
|
|
89
|
+
"meta": {
|
|
90
|
+
"$ref": "#/$defs/previewMeta"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"siteLogo": {
|
|
95
|
+
"type": "object",
|
|
96
|
+
"additionalProperties": false,
|
|
97
|
+
"required": ["src"],
|
|
98
|
+
"description": "Optional site logo data copied into generated preview-data.",
|
|
99
|
+
"markdownDescription": "Optional site logo data copied into generated preview-data.",
|
|
100
|
+
"properties": {
|
|
101
|
+
"src": {
|
|
102
|
+
"type": "string",
|
|
103
|
+
"minLength": 1,
|
|
104
|
+
"description": "Logo URL or safe relative path.",
|
|
105
|
+
"markdownDescription": "Logo URL or safe relative path."
|
|
106
|
+
},
|
|
107
|
+
"alt": {
|
|
108
|
+
"type": "string",
|
|
109
|
+
"description": "Optional logo alternative text. Themes may fall back to site.title when omitted.",
|
|
110
|
+
"markdownDescription": "Optional logo alternative text. Themes may fall back to `site.title` when omitted."
|
|
73
111
|
}
|
|
74
112
|
}
|
|
75
113
|
},
|
|
@@ -92,6 +130,21 @@
|
|
|
92
130
|
}
|
|
93
131
|
}
|
|
94
132
|
},
|
|
133
|
+
"markdown": {
|
|
134
|
+
"type": "object",
|
|
135
|
+
"additionalProperties": false,
|
|
136
|
+
"description": "Markdown source processing options.",
|
|
137
|
+
"markdownDescription": "Markdown source processing options.",
|
|
138
|
+
"properties": {
|
|
139
|
+
"last_updated": {
|
|
140
|
+
"type": "string",
|
|
141
|
+
"enum": ["none", "git"],
|
|
142
|
+
"default": "none",
|
|
143
|
+
"description": "Optional page meta enrichment. none does not generate last_updated values; git reads the latest commit date for each Markdown file.",
|
|
144
|
+
"markdownDescription": "Optional page meta enrichment. `none` does not generate `last_updated` values; `git` reads the latest commit date for each Markdown file."
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
},
|
|
95
148
|
"frontPage": {
|
|
96
149
|
"type": "object",
|
|
97
150
|
"additionalProperties": false,
|
|
@@ -287,23 +340,68 @@
|
|
|
287
340
|
"default": []
|
|
288
341
|
}
|
|
289
342
|
}
|
|
343
|
+
},
|
|
344
|
+
"collections": {
|
|
345
|
+
"type": "object",
|
|
346
|
+
"description": "Reading-order collections generated from Markdown source paths.",
|
|
347
|
+
"markdownDescription": "Reading-order collections generated from Markdown source paths.",
|
|
348
|
+
"propertyNames": {
|
|
349
|
+
"type": "string",
|
|
350
|
+
"pattern": "^[a-z][a-z0-9_-]{0,63}$"
|
|
351
|
+
},
|
|
352
|
+
"additionalProperties": {
|
|
353
|
+
"$ref": "#/$defs/collection"
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
"collection": {
|
|
357
|
+
"type": "object",
|
|
358
|
+
"additionalProperties": false,
|
|
359
|
+
"required": ["items"],
|
|
360
|
+
"properties": {
|
|
361
|
+
"title": {
|
|
362
|
+
"type": "string",
|
|
363
|
+
"minLength": 1
|
|
364
|
+
},
|
|
365
|
+
"description": {
|
|
366
|
+
"type": "string"
|
|
367
|
+
},
|
|
368
|
+
"items": {
|
|
369
|
+
"type": "array",
|
|
370
|
+
"items": {
|
|
371
|
+
"type": "string",
|
|
372
|
+
"minLength": 1,
|
|
373
|
+
"pattern": "^[^\\\\?#]+\\.md$"
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
290
377
|
}
|
|
291
378
|
},
|
|
292
379
|
"examples": [
|
|
293
380
|
{
|
|
294
|
-
"$schema": "https://zeropress.dev/
|
|
381
|
+
"$schema": "https://schemas.zeropress.dev/build-pages-config/v0.1/schema.json",
|
|
295
382
|
"version": "0.1",
|
|
296
383
|
"site": {
|
|
297
384
|
"title": "ZeroPress Public Docs",
|
|
298
385
|
"description": "Public documentation.",
|
|
299
386
|
"url": "https://zeropress.dev",
|
|
387
|
+
"logo": {
|
|
388
|
+
"src": "/logo.svg",
|
|
389
|
+
"alt": "ZeroPress Public Docs"
|
|
390
|
+
},
|
|
391
|
+
"locale": "en-US",
|
|
300
392
|
"expose_generator": true,
|
|
301
393
|
"search": true,
|
|
302
|
-
"indexing": true
|
|
394
|
+
"indexing": true,
|
|
395
|
+
"meta": {
|
|
396
|
+
"issue": "Spring 2026"
|
|
397
|
+
}
|
|
303
398
|
},
|
|
304
399
|
"front_page": {
|
|
305
400
|
"type": "markdown"
|
|
306
401
|
},
|
|
402
|
+
"markdown": {
|
|
403
|
+
"last_updated": "git"
|
|
404
|
+
},
|
|
307
405
|
"custom_html": {
|
|
308
406
|
"head_end": {
|
|
309
407
|
"file": ".zeropress/head-end.html"
|
|
@@ -327,6 +425,15 @@
|
|
|
327
425
|
}
|
|
328
426
|
]
|
|
329
427
|
}
|
|
428
|
+
},
|
|
429
|
+
"collections": {
|
|
430
|
+
"guides": {
|
|
431
|
+
"title": "Guides",
|
|
432
|
+
"items": [
|
|
433
|
+
"getting-started/index.md",
|
|
434
|
+
"deployment/index.md"
|
|
435
|
+
]
|
|
436
|
+
}
|
|
330
437
|
}
|
|
331
438
|
}
|
|
332
439
|
]
|