@zeropress/build-pages 0.5.6 → 0.6.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 +105 -24
- package/action.yml +3 -0
- package/dist/action.js +1814 -334
- package/dist/prebuild.js +157 -24
- package/package.json +2 -2
- package/schemas/zeropress-build-pages.config.v0.1.schema.json +33 -12
- package/src/action.js +2 -1
- package/src/index.js +154 -3
- package/src/prebuild.js +178 -23
- package/themes/docs/assets/style.css +212 -0
- package/themes/docs/assets/theme.js +121 -0
- package/themes/docs/layout.html +20 -4
- package/themes/docs/page.html +2 -2
- package/themes/docs/partials/theme-scripts.html +1 -0
- package/themes/docs/post.html +1 -1
- package/themes/docs/theme.json +5 -6
package/dist/prebuild.js
CHANGED
|
@@ -3524,6 +3524,7 @@ import { fileURLToPath } from "node:url";
|
|
|
3524
3524
|
var __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
3525
3525
|
var rootDir = process.cwd();
|
|
3526
3526
|
var sourceDir = resolveEnvPath(["ZEROPRESS_BUILD_PAGES_SOURCE"], "docs");
|
|
3527
|
+
var publicDir = resolveEnvPath(["ZEROPRESS_BUILD_PAGES_PUBLIC_DIR"], sourceDir);
|
|
3527
3528
|
var defaultConfigPath = path.join(sourceDir, ".zeropress", "config.json");
|
|
3528
3529
|
var configPath = resolveOptionalEnvPath(["ZEROPRESS_BUILD_PAGES_CONFIG"], defaultConfigPath);
|
|
3529
3530
|
var outDir = path.join(rootDir, ".zeropress");
|
|
@@ -3534,7 +3535,13 @@ var skipUntitledMarkdown = readBooleanEnv("ZEROPRESS_SKIP_UNTITLED_MARKDOWN");
|
|
|
3534
3535
|
var copyMarkdownSource = readBooleanEnv("ZEROPRESS_COPY_MARKDOWN_SOURCE", true);
|
|
3535
3536
|
var FRONT_PAGE_TYPES = /* @__PURE__ */ new Set(["theme_index", "markdown", "html"]);
|
|
3536
3537
|
var BUILD_PAGES_CONFIG_SCHEMA_URL = "https://zeropress.dev/schemas/zeropress-build-pages.config.v0.1.schema.json";
|
|
3537
|
-
var PREVIEW_DATA_SCHEMA_URL = "https://zeropress.dev/schemas/preview-data.v0.
|
|
3538
|
+
var PREVIEW_DATA_SCHEMA_URL = "https://zeropress.dev/schemas/preview-data.v0.6.schema.json";
|
|
3539
|
+
var FRONT_MATTER_DATA_KEY_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*(?:-[a-zA-Z0-9_]+)*$/;
|
|
3540
|
+
var FRONT_MATTER_DATA_MAX_DEPTH = 4;
|
|
3541
|
+
var FRONT_MATTER_DATA_MAX_KEYS = 64;
|
|
3542
|
+
var FRONT_MATTER_DATA_MAX_ARRAY_LENGTH = 256;
|
|
3543
|
+
var FRONT_MATTER_DISCOVERABILITY_VALUES = /* @__PURE__ */ new Set(["default", "noindex", "delist"]);
|
|
3544
|
+
var markdownDiscoverExcludeRoots = buildMarkdownDiscoverExcludeRoots();
|
|
3538
3545
|
var PrebuildMarkdownError = class extends Error {
|
|
3539
3546
|
constructor(sourcePath, reason, expected = "", code = "invalid_markdown") {
|
|
3540
3547
|
super(reason);
|
|
@@ -3608,6 +3615,8 @@ async function main() {
|
|
|
3608
3615
|
...frontMatter.meta,
|
|
3609
3616
|
...copyMarkdownSource ? { source_markdown_url: buildSourceMarkdownUrl(sourcePath) } : {}
|
|
3610
3617
|
},
|
|
3618
|
+
...frontMatter.data !== void 0 ? { data: frontMatter.data } : {},
|
|
3619
|
+
...frontMatter.discoverability !== "default" ? { discoverability: frontMatter.discoverability } : {},
|
|
3611
3620
|
content: rewriteMarkdownLinks(bodyMarkdown, sourcePath, routeBySourcePath),
|
|
3612
3621
|
document_type: "markdown",
|
|
3613
3622
|
excerpt: frontMatter.description || extractExcerpt(bodyMarkdown, title),
|
|
@@ -3621,7 +3630,7 @@ async function main() {
|
|
|
3621
3630
|
const customHtml = await buildCustomHtmlData(customHtmlConfig);
|
|
3622
3631
|
const previewData = {
|
|
3623
3632
|
$schema: PREVIEW_DATA_SCHEMA_URL,
|
|
3624
|
-
version: "0.
|
|
3633
|
+
version: "0.6",
|
|
3625
3634
|
generator: "zeropress-build-pages",
|
|
3626
3635
|
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3627
3636
|
site,
|
|
@@ -3719,18 +3728,21 @@ function buildSiteData(config, frontPage) {
|
|
|
3719
3728
|
title: configuredSite.title,
|
|
3720
3729
|
description: configuredSite.description,
|
|
3721
3730
|
url: configuredSite.url,
|
|
3722
|
-
|
|
3731
|
+
media_base_url: "",
|
|
3723
3732
|
locale: "en-US",
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3733
|
+
posts_per_page: 10,
|
|
3734
|
+
datetime_display: "static",
|
|
3735
|
+
date_style: "medium",
|
|
3736
|
+
time_style: "none",
|
|
3727
3737
|
timezone: "UTC",
|
|
3728
3738
|
permalinks: defaultPermalinks(),
|
|
3729
3739
|
front_page: frontPage,
|
|
3730
3740
|
post_index: {
|
|
3731
3741
|
enabled: false
|
|
3732
3742
|
},
|
|
3733
|
-
|
|
3743
|
+
disallow_comments: true,
|
|
3744
|
+
expose_generator: configuredSite.expose_generator !== false,
|
|
3745
|
+
search: configuredSite.search !== false,
|
|
3734
3746
|
indexing: configuredSite.indexing !== false
|
|
3735
3747
|
};
|
|
3736
3748
|
if (configuredSite.footer) {
|
|
@@ -3759,11 +3771,13 @@ function normalizeSiteConfig(value) {
|
|
|
3759
3771
|
);
|
|
3760
3772
|
}
|
|
3761
3773
|
const configuredSite = isPlainObject(value) ? value : {};
|
|
3762
|
-
assertKnownConfigKeys(configuredSite, ["title", "description", "url", "indexing", "footer"], "site");
|
|
3774
|
+
assertKnownConfigKeys(configuredSite, ["title", "description", "url", "expose_generator", "search", "indexing", "footer"], "site");
|
|
3763
3775
|
const site = {
|
|
3764
3776
|
title: readConfigString(configuredSite.title, "Documentation"),
|
|
3765
3777
|
description: readConfigString(configuredSite.description, "A documentation site."),
|
|
3766
3778
|
url: readEnv("ZEROPRESS_SITE_URL", readConfigString(configuredSite.url, "")),
|
|
3779
|
+
expose_generator: readConfigBoolean(configuredSite.expose_generator, true, "site.expose_generator"),
|
|
3780
|
+
search: readConfigBoolean(configuredSite.search, true, "site.search"),
|
|
3767
3781
|
indexing: readConfigBoolean(configuredSite.indexing, true, "site.indexing")
|
|
3768
3782
|
};
|
|
3769
3783
|
const footer = normalizeFooter(configuredSite.footer);
|
|
@@ -3785,19 +3799,11 @@ function normalizeFooter(value) {
|
|
|
3785
3799
|
if (copyrightText) {
|
|
3786
3800
|
footer.copyright_text = copyrightText;
|
|
3787
3801
|
}
|
|
3788
|
-
if (value.attribution !== void 0
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
if (isPlainObject(value.attribution)) {
|
|
3792
|
-
assertKnownConfigKeys(value.attribution, ["enabled"], "site.footer.attribution");
|
|
3793
|
-
if (value.attribution.enabled !== void 0 && typeof value.attribution.enabled !== "boolean") {
|
|
3794
|
-
throw new PrebuildConfigError("site.footer.attribution.enabled must be a boolean when provided.");
|
|
3802
|
+
if (value.attribution !== void 0) {
|
|
3803
|
+
if (typeof value.attribution !== "boolean") {
|
|
3804
|
+
throw new PrebuildConfigError("site.footer.attribution must be a boolean when provided.");
|
|
3795
3805
|
}
|
|
3796
|
-
|
|
3797
|
-
if (isPlainObject(value.attribution) && typeof value.attribution.enabled === "boolean") {
|
|
3798
|
-
footer.attribution = {
|
|
3799
|
-
enabled: value.attribution.enabled
|
|
3800
|
-
};
|
|
3806
|
+
footer.attribution = value.attribution;
|
|
3801
3807
|
}
|
|
3802
3808
|
return Object.keys(footer).length ? footer : void 0;
|
|
3803
3809
|
}
|
|
@@ -4143,9 +4149,23 @@ function normalizeMenuItem(item, pathLabel) {
|
|
|
4143
4149
|
url,
|
|
4144
4150
|
type: readConfigString(item.type, "custom"),
|
|
4145
4151
|
target: readConfigString(item.target, "_self"),
|
|
4152
|
+
...item.meta !== void 0 ? { meta: normalizeMenuItemMeta(item.meta, `${pathLabel}.meta`) } : {},
|
|
4146
4153
|
children: Array.isArray(item.children) ? item.children.map((child, index) => normalizeMenuItem(child, `${pathLabel}.children[${index}]`)) : []
|
|
4147
4154
|
};
|
|
4148
4155
|
}
|
|
4156
|
+
function normalizeMenuItemMeta(value, pathLabel) {
|
|
4157
|
+
if (!isPlainObject(value)) {
|
|
4158
|
+
throw new PrebuildConfigError(`${pathLabel} must be an object when provided.`);
|
|
4159
|
+
}
|
|
4160
|
+
const meta = {};
|
|
4161
|
+
for (const [key, metaValue] of Object.entries(value)) {
|
|
4162
|
+
if (!isPreviewMetaValue(metaValue)) {
|
|
4163
|
+
throw new PrebuildConfigError(`${pathLabel}.${key} must be a string, number, boolean, or null.`);
|
|
4164
|
+
}
|
|
4165
|
+
meta[key] = metaValue;
|
|
4166
|
+
}
|
|
4167
|
+
return meta;
|
|
4168
|
+
}
|
|
4149
4169
|
function defaultMenus() {
|
|
4150
4170
|
return {
|
|
4151
4171
|
primary: {
|
|
@@ -4168,6 +4188,7 @@ function buildPrebuildReport({
|
|
|
4168
4188
|
return {
|
|
4169
4189
|
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4170
4190
|
source_dir: formatSourcePath(sourceDir),
|
|
4191
|
+
public_dir: formatSourcePath(publicDir),
|
|
4171
4192
|
config_path: formatSourcePath(configPath),
|
|
4172
4193
|
build_pages_config_path: formatSourcePath(buildPagesConfigPath),
|
|
4173
4194
|
preview_data_path: formatSourcePath(previewDataPath),
|
|
@@ -4193,7 +4214,8 @@ function buildPrebuildReport({
|
|
|
4193
4214
|
function printPrebuildSummary(report) {
|
|
4194
4215
|
const lines = [
|
|
4195
4216
|
"ZeroPress build report",
|
|
4196
|
-
`-
|
|
4217
|
+
`- Source root: ${report.source_dir}`,
|
|
4218
|
+
`- Public root: ${report.public_dir}`,
|
|
4197
4219
|
`- Markdown discovered: ${report.markdown.discovered}`,
|
|
4198
4220
|
`- Markdown pages generated: ${report.markdown.generated_pages}`,
|
|
4199
4221
|
`- Markdown skipped: ${report.markdown.skipped}`,
|
|
@@ -4262,7 +4284,9 @@ function normalizePublishedFrontMatter(frontMatter, sourcePath) {
|
|
|
4262
4284
|
title: normalizeFrontMatterTitle(frontMatter.title, sourcePath),
|
|
4263
4285
|
description: normalizeFrontMatterDescription(frontMatter.description, sourcePath),
|
|
4264
4286
|
path: normalizeFrontMatterRoutePath(frontMatter.path, sourcePath),
|
|
4265
|
-
|
|
4287
|
+
discoverability: normalizeFrontMatterDiscoverability(frontMatter.discoverability, sourcePath),
|
|
4288
|
+
meta: normalizeFrontMatterMeta(frontMatter.meta, sourcePath),
|
|
4289
|
+
data: normalizeFrontMatterData(frontMatter.data, sourcePath)
|
|
4266
4290
|
};
|
|
4267
4291
|
}
|
|
4268
4292
|
function normalizeFrontMatterTitle(value, sourcePath) {
|
|
@@ -4305,11 +4329,24 @@ function normalizeFrontMatterRoutePath(value, sourcePath) {
|
|
|
4305
4329
|
throw new PrebuildMarkdownError(
|
|
4306
4330
|
sourcePath,
|
|
4307
4331
|
"front matter path must be a safe generated route path.",
|
|
4308
|
-
" path: guides/install\n path: spec/preview-data-v0.
|
|
4332
|
+
" path: guides/install\n path: spec/preview-data-v0.6"
|
|
4309
4333
|
);
|
|
4310
4334
|
}
|
|
4311
4335
|
return routePath;
|
|
4312
4336
|
}
|
|
4337
|
+
function normalizeFrontMatterDiscoverability(value, sourcePath) {
|
|
4338
|
+
if (value === void 0) {
|
|
4339
|
+
return "default";
|
|
4340
|
+
}
|
|
4341
|
+
if (typeof value === "string" && FRONT_MATTER_DISCOVERABILITY_VALUES.has(value)) {
|
|
4342
|
+
return value;
|
|
4343
|
+
}
|
|
4344
|
+
throw new PrebuildMarkdownError(
|
|
4345
|
+
sourcePath,
|
|
4346
|
+
`front matter discoverability must be one of: ${Array.from(FRONT_MATTER_DISCOVERABILITY_VALUES).join(", ")}.`,
|
|
4347
|
+
" discoverability: default\n discoverability: noindex\n discoverability: delist"
|
|
4348
|
+
);
|
|
4349
|
+
}
|
|
4313
4350
|
function isSafeRoutePathSegment(segment) {
|
|
4314
4351
|
return /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/.test(segment) && !segment.includes("..");
|
|
4315
4352
|
}
|
|
@@ -4336,7 +4373,88 @@ function normalizeFrontMatterMeta(value, sourcePath) {
|
|
|
4336
4373
|
return meta;
|
|
4337
4374
|
}
|
|
4338
4375
|
function isPreviewMetaValue(value) {
|
|
4339
|
-
return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
4376
|
+
return value === null || typeof value === "string" || typeof value === "number" && Number.isFinite(value) || typeof value === "boolean";
|
|
4377
|
+
}
|
|
4378
|
+
function normalizeFrontMatterData(value, sourcePath) {
|
|
4379
|
+
if (value === void 0) {
|
|
4380
|
+
return void 0;
|
|
4381
|
+
}
|
|
4382
|
+
if (!isPlainObject(value)) {
|
|
4383
|
+
throw new PrebuildMarkdownError(
|
|
4384
|
+
sourcePath,
|
|
4385
|
+
"front matter data must be an object when provided."
|
|
4386
|
+
);
|
|
4387
|
+
}
|
|
4388
|
+
validateFrontMatterDataObject(value, sourcePath, "data", 0);
|
|
4389
|
+
return value;
|
|
4390
|
+
}
|
|
4391
|
+
function validateFrontMatterDataValue(value, sourcePath, pathLabel, depth) {
|
|
4392
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") {
|
|
4393
|
+
return;
|
|
4394
|
+
}
|
|
4395
|
+
if (typeof value === "number") {
|
|
4396
|
+
if (!Number.isFinite(value)) {
|
|
4397
|
+
throw new PrebuildMarkdownError(
|
|
4398
|
+
sourcePath,
|
|
4399
|
+
`front matter ${pathLabel} must be a finite number.`
|
|
4400
|
+
);
|
|
4401
|
+
}
|
|
4402
|
+
return;
|
|
4403
|
+
}
|
|
4404
|
+
if (Array.isArray(value)) {
|
|
4405
|
+
validateFrontMatterDataArray(value, sourcePath, pathLabel, depth);
|
|
4406
|
+
return;
|
|
4407
|
+
}
|
|
4408
|
+
if (isPlainObject(value)) {
|
|
4409
|
+
validateFrontMatterDataObject(value, sourcePath, pathLabel, depth);
|
|
4410
|
+
return;
|
|
4411
|
+
}
|
|
4412
|
+
throw new PrebuildMarkdownError(
|
|
4413
|
+
sourcePath,
|
|
4414
|
+
`front matter ${pathLabel} must be JSON-safe structured data.`
|
|
4415
|
+
);
|
|
4416
|
+
}
|
|
4417
|
+
function validateFrontMatterDataObject(object, sourcePath, pathLabel, depth) {
|
|
4418
|
+
if (depth > FRONT_MATTER_DATA_MAX_DEPTH) {
|
|
4419
|
+
throw new PrebuildMarkdownError(
|
|
4420
|
+
sourcePath,
|
|
4421
|
+
`front matter ${pathLabel} nesting must not exceed ${FRONT_MATTER_DATA_MAX_DEPTH} container levels.`
|
|
4422
|
+
);
|
|
4423
|
+
}
|
|
4424
|
+
const entries = Object.entries(object);
|
|
4425
|
+
if (entries.length > FRONT_MATTER_DATA_MAX_KEYS) {
|
|
4426
|
+
throw new PrebuildMarkdownError(
|
|
4427
|
+
sourcePath,
|
|
4428
|
+
`front matter ${pathLabel} must not contain more than ${FRONT_MATTER_DATA_MAX_KEYS} keys.`
|
|
4429
|
+
);
|
|
4430
|
+
}
|
|
4431
|
+
for (const [key, dataValue] of entries) {
|
|
4432
|
+
const childLabel = `${pathLabel}.${key}`;
|
|
4433
|
+
if (!FRONT_MATTER_DATA_KEY_PATTERN.test(key)) {
|
|
4434
|
+
throw new PrebuildMarkdownError(
|
|
4435
|
+
sourcePath,
|
|
4436
|
+
`front matter ${childLabel} uses an invalid key.`
|
|
4437
|
+
);
|
|
4438
|
+
}
|
|
4439
|
+
validateFrontMatterDataValue(dataValue, sourcePath, childLabel, depth + 1);
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
4442
|
+
function validateFrontMatterDataArray(array, sourcePath, pathLabel, depth) {
|
|
4443
|
+
if (depth > FRONT_MATTER_DATA_MAX_DEPTH) {
|
|
4444
|
+
throw new PrebuildMarkdownError(
|
|
4445
|
+
sourcePath,
|
|
4446
|
+
`front matter ${pathLabel} nesting must not exceed ${FRONT_MATTER_DATA_MAX_DEPTH} container levels.`
|
|
4447
|
+
);
|
|
4448
|
+
}
|
|
4449
|
+
if (array.length > FRONT_MATTER_DATA_MAX_ARRAY_LENGTH) {
|
|
4450
|
+
throw new PrebuildMarkdownError(
|
|
4451
|
+
sourcePath,
|
|
4452
|
+
`front matter ${pathLabel} must not contain more than ${FRONT_MATTER_DATA_MAX_ARRAY_LENGTH} items.`
|
|
4453
|
+
);
|
|
4454
|
+
}
|
|
4455
|
+
array.forEach((dataValue, index) => {
|
|
4456
|
+
validateFrontMatterDataValue(dataValue, sourcePath, `${pathLabel}[${index}]`, depth + 1);
|
|
4457
|
+
});
|
|
4340
4458
|
}
|
|
4341
4459
|
function formatFrontMatterValue(value) {
|
|
4342
4460
|
if (typeof value === "string") {
|
|
@@ -4385,6 +4503,9 @@ async function listMarkdownFiles(dir) {
|
|
|
4385
4503
|
continue;
|
|
4386
4504
|
}
|
|
4387
4505
|
const entryPath = path.join(dir, entry.name);
|
|
4506
|
+
if (isMarkdownDiscoverExcluded(entryPath)) {
|
|
4507
|
+
continue;
|
|
4508
|
+
}
|
|
4388
4509
|
if (entry.isDirectory()) {
|
|
4389
4510
|
files.push(...await listMarkdownFiles(entryPath));
|
|
4390
4511
|
continue;
|
|
@@ -4395,6 +4516,18 @@ async function listMarkdownFiles(dir) {
|
|
|
4395
4516
|
}
|
|
4396
4517
|
return files.sort((left, right) => left.localeCompare(right));
|
|
4397
4518
|
}
|
|
4519
|
+
function buildMarkdownDiscoverExcludeRoots() {
|
|
4520
|
+
if (samePath(sourceDir, publicDir) || !isPathInside(sourceDir, publicDir)) {
|
|
4521
|
+
return [];
|
|
4522
|
+
}
|
|
4523
|
+
return [publicDir];
|
|
4524
|
+
}
|
|
4525
|
+
function isMarkdownDiscoverExcluded(entryPath) {
|
|
4526
|
+
return markdownDiscoverExcludeRoots.some((excludeRoot) => samePath(entryPath, excludeRoot) || isPathInside(excludeRoot, entryPath));
|
|
4527
|
+
}
|
|
4528
|
+
function samePath(firstPath, secondPath) {
|
|
4529
|
+
return path.resolve(firstPath) === path.resolve(secondPath);
|
|
4530
|
+
}
|
|
4398
4531
|
function shouldIgnoreMarkdownDiscoverEntry(name) {
|
|
4399
4532
|
const basename = String(name || "");
|
|
4400
4533
|
const lowerName = basename.toLowerCase();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeropress/build-pages",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.2",
|
|
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.
|
|
43
|
+
"@zeropress/build": "0.6.2",
|
|
44
44
|
"gray-matter": "4.0.3"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
@@ -50,6 +50,18 @@
|
|
|
50
50
|
"description": "Canonical site URL. Use an empty string or omit this field for local builds without canonical output.",
|
|
51
51
|
"markdownDescription": "Canonical site URL. Use an empty string or omit this field for local builds without canonical output."
|
|
52
52
|
},
|
|
53
|
+
"expose_generator": {
|
|
54
|
+
"type": "boolean",
|
|
55
|
+
"default": true,
|
|
56
|
+
"description": "Whether generated HTML should expose the ZeroPress generator meta tag. Set false for white-label sites.",
|
|
57
|
+
"markdownDescription": "Whether generated HTML should expose `<meta name=\"generator\" content=\"ZeroPress\">`. Set `false` for white-label sites."
|
|
58
|
+
},
|
|
59
|
+
"search": {
|
|
60
|
+
"type": "boolean",
|
|
61
|
+
"default": true,
|
|
62
|
+
"description": "Whether native ZeroPress search should be enabled when the selected theme supports search UI. Set false to omit native search artifacts and hide theme search UI.",
|
|
63
|
+
"markdownDescription": "Whether native ZeroPress search should be enabled when the selected theme supports search UI. Set `false` to omit native search artifacts and hide theme search UI."
|
|
64
|
+
},
|
|
53
65
|
"indexing": {
|
|
54
66
|
"type": "boolean",
|
|
55
67
|
"default": true,
|
|
@@ -74,17 +86,6 @@
|
|
|
74
86
|
"markdownDescription": "Footer copyright or legal text. ZeroPress does not add a copyright symbol automatically."
|
|
75
87
|
},
|
|
76
88
|
"attribution": {
|
|
77
|
-
"$ref": "#/$defs/siteFooterAttribution"
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
},
|
|
81
|
-
"siteFooterAttribution": {
|
|
82
|
-
"type": "object",
|
|
83
|
-
"additionalProperties": false,
|
|
84
|
-
"description": "Optional ZeroPress attribution display policy for themes.",
|
|
85
|
-
"markdownDescription": "Optional ZeroPress attribution display policy for themes.",
|
|
86
|
-
"properties": {
|
|
87
|
-
"enabled": {
|
|
88
89
|
"type": "boolean",
|
|
89
90
|
"description": "When false, bundled themes hide the Published with ZeroPress attribution. Missing or true means attribution may be shown.",
|
|
90
91
|
"markdownDescription": "When `false`, bundled themes hide the Published with ZeroPress attribution. Missing or `true` means attribution may be shown."
|
|
@@ -227,6 +228,12 @@
|
|
|
227
228
|
"$ref": "#/$defs/menu"
|
|
228
229
|
}
|
|
229
230
|
},
|
|
231
|
+
"previewMeta": {
|
|
232
|
+
"type": "object",
|
|
233
|
+
"additionalProperties": {
|
|
234
|
+
"type": ["string", "number", "boolean", "null"]
|
|
235
|
+
}
|
|
236
|
+
},
|
|
230
237
|
"menu": {
|
|
231
238
|
"type": "object",
|
|
232
239
|
"additionalProperties": false,
|
|
@@ -248,6 +255,8 @@
|
|
|
248
255
|
"type": "object",
|
|
249
256
|
"additionalProperties": false,
|
|
250
257
|
"required": ["title", "url"],
|
|
258
|
+
"description": "Menu item copied into generated preview-data.",
|
|
259
|
+
"markdownDescription": "Menu item copied into generated preview-data.",
|
|
251
260
|
"properties": {
|
|
252
261
|
"title": {
|
|
253
262
|
"type": "string",
|
|
@@ -267,6 +276,9 @@
|
|
|
267
276
|
"enum": ["_self", "_blank"],
|
|
268
277
|
"default": "_self"
|
|
269
278
|
},
|
|
279
|
+
"meta": {
|
|
280
|
+
"$ref": "#/$defs/previewMeta"
|
|
281
|
+
},
|
|
270
282
|
"children": {
|
|
271
283
|
"type": "array",
|
|
272
284
|
"items": {
|
|
@@ -285,6 +297,8 @@
|
|
|
285
297
|
"title": "ZeroPress Public Docs",
|
|
286
298
|
"description": "Public documentation.",
|
|
287
299
|
"url": "https://zeropress.dev",
|
|
300
|
+
"expose_generator": true,
|
|
301
|
+
"search": true,
|
|
288
302
|
"indexing": true
|
|
289
303
|
},
|
|
290
304
|
"front_page": {
|
|
@@ -303,7 +317,14 @@
|
|
|
303
317
|
"name": "Primary Menu",
|
|
304
318
|
"items": [
|
|
305
319
|
{ "title": "Home", "url": "/" },
|
|
306
|
-
{
|
|
320
|
+
{
|
|
321
|
+
"title": "Docs",
|
|
322
|
+
"url": "/docs/",
|
|
323
|
+
"meta": {
|
|
324
|
+
"icon": "book-open",
|
|
325
|
+
"badge": "New"
|
|
326
|
+
}
|
|
327
|
+
}
|
|
307
328
|
]
|
|
308
329
|
}
|
|
309
330
|
}
|
package/src/action.js
CHANGED
|
@@ -2,6 +2,7 @@ import { runBuildPages } from './index.js';
|
|
|
2
2
|
|
|
3
3
|
const options = {
|
|
4
4
|
source: input('source') || './docs',
|
|
5
|
+
publicDir: input('public-dir'),
|
|
5
6
|
destination: input('destination') || './_site',
|
|
6
7
|
theme: input('theme') || 'docs',
|
|
7
8
|
themePath: input('theme-path'),
|
|
@@ -21,7 +22,7 @@ try {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
function input(name) {
|
|
24
|
-
return process.env[`INPUT_${name.toUpperCase()
|
|
25
|
+
return process.env[`INPUT_${name.toUpperCase()}`]?.trim() || '';
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
function booleanInput(name, fallback) {
|
package/src/index.js
CHANGED
|
@@ -39,6 +39,8 @@ export async function runBuildPages(options) {
|
|
|
39
39
|
const cwd = path.resolve(options.cwd || process.cwd());
|
|
40
40
|
const copyMarkdownSource = options.copyMarkdownSource !== false;
|
|
41
41
|
const sourceDir = path.resolve(cwd, options.source);
|
|
42
|
+
const publicDirExplicit = hasExplicitPublicDir(options);
|
|
43
|
+
const publicDir = publicDirExplicit ? path.resolve(cwd, options.publicDir) : sourceDir;
|
|
42
44
|
const destinationDir = path.resolve(cwd, options.destination);
|
|
43
45
|
const generatedDir = path.join(cwd, '.zeropress');
|
|
44
46
|
const stagingDir = path.join(cwd, STAGING_DIR);
|
|
@@ -48,18 +50,23 @@ export async function runBuildPages(options) {
|
|
|
48
50
|
assertBuildPagesPathLayout({
|
|
49
51
|
cwd,
|
|
50
52
|
sourceDir,
|
|
53
|
+
publicDir,
|
|
54
|
+
publicDirExplicit,
|
|
51
55
|
destinationDir,
|
|
52
56
|
themeDir,
|
|
53
57
|
generatedDir,
|
|
54
58
|
});
|
|
55
59
|
await assertDirectory(sourceDir, 'Source directory');
|
|
60
|
+
await assertPublicDirectory(publicDir, publicDirExplicit);
|
|
61
|
+
await assertDestinationPath(destinationDir);
|
|
56
62
|
await fs.rm(generatedDir, { recursive: true, force: true });
|
|
57
63
|
await fs.mkdir(generatedDir, { recursive: true });
|
|
58
64
|
|
|
59
65
|
const env = {
|
|
60
66
|
...process.env,
|
|
61
67
|
ZEROPRESS_BUILD_PAGES_SOURCE: sourceDir,
|
|
62
|
-
|
|
68
|
+
ZEROPRESS_BUILD_PAGES_PUBLIC_DIR: publicDir,
|
|
69
|
+
ZEROPRESS_PUBLIC_DIR: publicDir,
|
|
63
70
|
ZEROPRESS_SKIP_UNTITLED_MARKDOWN: String(Boolean(options.skipUntitledMarkdown)),
|
|
64
71
|
ZEROPRESS_COPY_MARKDOWN_SOURCE: String(copyMarkdownSource),
|
|
65
72
|
};
|
|
@@ -85,10 +92,13 @@ export async function runBuildPages(options) {
|
|
|
85
92
|
await fs.rm(destinationDir, { recursive: true, force: true });
|
|
86
93
|
await fs.rm(stagingDir, { recursive: true, force: true });
|
|
87
94
|
await fs.mkdir(stagingDir, { recursive: true });
|
|
88
|
-
await copyPublicStaging(
|
|
95
|
+
await copyPublicStaging(publicDir, stagingDir, {
|
|
89
96
|
excludePaths: [destinationDir, themeDir, generatedDir],
|
|
90
97
|
copyMarkdownSource,
|
|
91
98
|
});
|
|
99
|
+
if (copyMarkdownSource) {
|
|
100
|
+
await copySourceMarkdownFiles(sourceDir, stagingDir, previewData);
|
|
101
|
+
}
|
|
92
102
|
|
|
93
103
|
const previousPublicDir = process.env.ZEROPRESS_PUBLIC_DIR;
|
|
94
104
|
process.env.ZEROPRESS_PUBLIC_DIR = stagingDir;
|
|
@@ -137,6 +147,7 @@ export function parseArgs(argv) {
|
|
|
137
147
|
|
|
138
148
|
const valueOptions = new Set([
|
|
139
149
|
'--source',
|
|
150
|
+
'--public-dir',
|
|
140
151
|
'--destination',
|
|
141
152
|
'--theme',
|
|
142
153
|
'--theme-path',
|
|
@@ -167,6 +178,7 @@ export function parseArgs(argv) {
|
|
|
167
178
|
|
|
168
179
|
return {
|
|
169
180
|
source,
|
|
181
|
+
publicDir: flags['public-dir'] || '',
|
|
170
182
|
destination,
|
|
171
183
|
theme: flags.theme || DEFAULT_THEME,
|
|
172
184
|
themePath: flags['theme-path'] || '',
|
|
@@ -186,6 +198,7 @@ Usage:
|
|
|
186
198
|
|
|
187
199
|
Options:
|
|
188
200
|
--source <dir> Dedicated source directory (required)
|
|
201
|
+
--public-dir <dir> Public passthrough directory (default: source)
|
|
189
202
|
--destination <dir> Output directory (required)
|
|
190
203
|
--theme docs Bundled theme name (default: docs)
|
|
191
204
|
--theme-path <dir> Custom ZeroPress theme directory
|
|
@@ -208,6 +221,10 @@ function resolveThemeDir(cwd, options) {
|
|
|
208
221
|
throw new Error(`Unknown bundled theme: ${options.theme}`);
|
|
209
222
|
}
|
|
210
223
|
|
|
224
|
+
function hasExplicitPublicDir(options) {
|
|
225
|
+
return typeof options.publicDir === 'string' && Boolean(options.publicDir.trim());
|
|
226
|
+
}
|
|
227
|
+
|
|
211
228
|
async function assertDirectory(dir, label) {
|
|
212
229
|
let stat;
|
|
213
230
|
try {
|
|
@@ -223,7 +240,55 @@ async function assertDirectory(dir, label) {
|
|
|
223
240
|
}
|
|
224
241
|
}
|
|
225
242
|
|
|
226
|
-
function
|
|
243
|
+
async function assertPublicDirectory(publicDir, explicit) {
|
|
244
|
+
if (!explicit) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let stat;
|
|
249
|
+
try {
|
|
250
|
+
stat = await fs.lstat(publicDir);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
if (error?.code === 'ENOENT') {
|
|
253
|
+
throw new Error(`Public directory not found: ${publicDir}`);
|
|
254
|
+
}
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (stat.isSymbolicLink()) {
|
|
259
|
+
throw new Error(`Public directory must not be a symbolic link: ${publicDir}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!stat.isDirectory()) {
|
|
263
|
+
throw new Error(`Public path is not a directory: ${publicDir}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function assertDestinationPath(destinationDir) {
|
|
268
|
+
let stat;
|
|
269
|
+
try {
|
|
270
|
+
stat = await fs.lstat(destinationDir);
|
|
271
|
+
} catch (error) {
|
|
272
|
+
if (error?.code === 'ENOENT') {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!stat.isDirectory()) {
|
|
279
|
+
throw new Error(`Destination path is not a directory: ${destinationDir}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function assertBuildPagesPathLayout({
|
|
284
|
+
cwd,
|
|
285
|
+
sourceDir,
|
|
286
|
+
publicDir,
|
|
287
|
+
publicDirExplicit,
|
|
288
|
+
destinationDir,
|
|
289
|
+
themeDir,
|
|
290
|
+
generatedDir,
|
|
291
|
+
}) {
|
|
227
292
|
if (samePath(sourceDir, cwd)) {
|
|
228
293
|
throw new Error(
|
|
229
294
|
'Source directory must be a dedicated content directory, not the current working directory. '
|
|
@@ -231,11 +296,36 @@ function assertBuildPagesPathLayout({ cwd, sourceDir, destinationDir, themeDir,
|
|
|
231
296
|
);
|
|
232
297
|
}
|
|
233
298
|
|
|
299
|
+
if (publicDirExplicit && samePath(publicDir, cwd)) {
|
|
300
|
+
throw new Error(
|
|
301
|
+
'Public directory must be a dedicated asset directory, not the current working directory. '
|
|
302
|
+
+ `Received: ${formatPath(cwd, publicDir)}`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
234
306
|
assertNoPathOverlap(cwd, 'Source directory', sourceDir, 'internal .zeropress working directory', generatedDir);
|
|
235
307
|
assertNoPathOverlap(cwd, 'Destination directory', destinationDir, 'internal .zeropress working directory', generatedDir);
|
|
236
308
|
assertNoPathOverlap(cwd, 'Theme directory', themeDir, 'internal .zeropress working directory', generatedDir);
|
|
309
|
+
if (!samePath(publicDir, sourceDir)) {
|
|
310
|
+
assertNoPathOverlap(cwd, 'Public directory', publicDir, 'internal .zeropress working directory', generatedDir);
|
|
311
|
+
assertNoPathOverlap(cwd, 'Public directory', publicDir, 'destination directory', destinationDir);
|
|
312
|
+
assertNoPathOverlap(cwd, 'Public directory', publicDir, 'theme directory', themeDir);
|
|
313
|
+
}
|
|
237
314
|
assertNoPathOverlap(cwd, 'Source directory', sourceDir, 'destination directory', destinationDir);
|
|
238
315
|
assertNoPathOverlap(cwd, 'Source directory', sourceDir, 'theme directory', themeDir);
|
|
316
|
+
assertSourceIsNotInsidePublicDirectory(cwd, sourceDir, publicDir);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function assertSourceIsNotInsidePublicDirectory(cwd, sourceDir, publicDir) {
|
|
320
|
+
if (samePath(sourceDir, publicDir) || !isPathInside(publicDir, sourceDir)) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
throw new Error(
|
|
325
|
+
'Source directory must not be inside the public directory. '
|
|
326
|
+
+ `Source directory: ${formatPath(cwd, sourceDir)}; `
|
|
327
|
+
+ `Public directory: ${formatPath(cwd, publicDir)}`,
|
|
328
|
+
);
|
|
239
329
|
}
|
|
240
330
|
|
|
241
331
|
function assertNoPathOverlap(cwd, firstLabel, firstPath, secondLabel, secondPath) {
|
|
@@ -282,6 +372,67 @@ async function copyPublicStaging(sourceDir, targetDir, options) {
|
|
|
282
372
|
}
|
|
283
373
|
}
|
|
284
374
|
|
|
375
|
+
async function copySourceMarkdownFiles(sourceDir, targetDir, previewData) {
|
|
376
|
+
const markdownUrls = new Set();
|
|
377
|
+
|
|
378
|
+
for (const page of previewData?.content?.pages || []) {
|
|
379
|
+
const sourceMarkdownUrl = page?.meta?.source_markdown_url;
|
|
380
|
+
if (typeof sourceMarkdownUrl === 'string' && sourceMarkdownUrl) {
|
|
381
|
+
markdownUrls.add(sourceMarkdownUrl);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
for (const sourceMarkdownUrl of markdownUrls) {
|
|
386
|
+
const relativePath = sourceMarkdownUrlToRelativePath(sourceMarkdownUrl);
|
|
387
|
+
if (!relativePath) {
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const sourcePath = path.join(sourceDir, relativePath);
|
|
392
|
+
if (!isPathInside(sourceDir, sourcePath)) {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const targetPath = path.join(targetDir, relativePath);
|
|
397
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
398
|
+
await fs.copyFile(sourcePath, targetPath);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function sourceMarkdownUrlToRelativePath(sourceMarkdownUrl) {
|
|
403
|
+
if (
|
|
404
|
+
!sourceMarkdownUrl.startsWith('/')
|
|
405
|
+
|| sourceMarkdownUrl.includes('?')
|
|
406
|
+
|| sourceMarkdownUrl.includes('#')
|
|
407
|
+
) {
|
|
408
|
+
return '';
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const rawSegments = sourceMarkdownUrl.slice(1).split('/');
|
|
412
|
+
const segments = [];
|
|
413
|
+
for (const rawSegment of rawSegments) {
|
|
414
|
+
if (!rawSegment) {
|
|
415
|
+
return '';
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
let segment;
|
|
419
|
+
try {
|
|
420
|
+
segment = decodeURIComponent(rawSegment);
|
|
421
|
+
} catch {
|
|
422
|
+
return '';
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (!segment || segment === '.' || segment === '..' || segment.includes('/') || segment.includes('\\')) {
|
|
426
|
+
return '';
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
segments.push(segment);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const relativePath = segments.join('/');
|
|
433
|
+
return relativePath.toLowerCase().endsWith('.md') ? relativePath : '';
|
|
434
|
+
}
|
|
435
|
+
|
|
285
436
|
function shouldIgnorePublicEntry(name) {
|
|
286
437
|
const basename = String(name || '');
|
|
287
438
|
const lowerName = basename.toLowerCase();
|