@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
|
@@ -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": {
|
|
@@ -124,6 +130,21 @@
|
|
|
124
130
|
}
|
|
125
131
|
}
|
|
126
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
|
+
},
|
|
127
148
|
"frontPage": {
|
|
128
149
|
"type": "object",
|
|
129
150
|
"additionalProperties": false,
|
|
@@ -319,6 +340,40 @@
|
|
|
319
340
|
"default": []
|
|
320
341
|
}
|
|
321
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
|
+
}
|
|
322
377
|
}
|
|
323
378
|
},
|
|
324
379
|
"examples": [
|
|
@@ -344,6 +399,9 @@
|
|
|
344
399
|
"front_page": {
|
|
345
400
|
"type": "markdown"
|
|
346
401
|
},
|
|
402
|
+
"markdown": {
|
|
403
|
+
"last_updated": "git"
|
|
404
|
+
},
|
|
347
405
|
"custom_html": {
|
|
348
406
|
"head_end": {
|
|
349
407
|
"file": ".zeropress/head-end.html"
|
|
@@ -367,6 +425,15 @@
|
|
|
367
425
|
}
|
|
368
426
|
]
|
|
369
427
|
}
|
|
428
|
+
},
|
|
429
|
+
"collections": {
|
|
430
|
+
"guides": {
|
|
431
|
+
"title": "Guides",
|
|
432
|
+
"items": [
|
|
433
|
+
"getting-started/index.md",
|
|
434
|
+
"deployment/index.md"
|
|
435
|
+
]
|
|
436
|
+
}
|
|
370
437
|
}
|
|
371
438
|
}
|
|
372
439
|
]
|
package/src/index.js
CHANGED
|
@@ -10,12 +10,21 @@ const packageDir = path.resolve(__dirname, '..');
|
|
|
10
10
|
const prebuildScript = __dirname === path.join(packageDir, 'dist')
|
|
11
11
|
? path.join(__dirname, 'prebuild.js')
|
|
12
12
|
: path.join(packageDir, 'src', 'prebuild.js');
|
|
13
|
-
const
|
|
14
|
-
const
|
|
13
|
+
const INTERNAL_WORK_DIR = '.zeropress-build-page';
|
|
14
|
+
const PREVIEW_DATA_PATH = `${INTERNAL_WORK_DIR}/preview-data.json`;
|
|
15
|
+
const STAGING_DIR = `${INTERNAL_WORK_DIR}/public-assets`;
|
|
15
16
|
const DEFAULT_THEME = 'docs';
|
|
17
|
+
const BUNDLED_THEME_ALIASES = new Map([
|
|
18
|
+
['docs', 'docs'],
|
|
19
|
+
['docs1', 'docs'],
|
|
20
|
+
]);
|
|
16
21
|
|
|
17
22
|
export async function runCli(argv = process.argv.slice(2)) {
|
|
18
23
|
try {
|
|
24
|
+
if (argv.length === 0) {
|
|
25
|
+
printHelp();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
19
28
|
if (argv.includes('--help') || argv.includes('-h')) {
|
|
20
29
|
printHelp();
|
|
21
30
|
return;
|
|
@@ -30,7 +39,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
30
39
|
await runBuildPages(options);
|
|
31
40
|
} catch (error) {
|
|
32
41
|
const message = error instanceof Error ? error.message : String(error);
|
|
33
|
-
console.error(message);
|
|
42
|
+
console.error(colorizeError(prefixError(message)));
|
|
34
43
|
process.exitCode = 1;
|
|
35
44
|
}
|
|
36
45
|
}
|
|
@@ -42,7 +51,7 @@ export async function runBuildPages(options) {
|
|
|
42
51
|
const publicDirExplicit = hasExplicitPublicDir(options);
|
|
43
52
|
const publicDir = publicDirExplicit ? path.resolve(cwd, options.publicDir) : sourceDir;
|
|
44
53
|
const destinationDir = path.resolve(cwd, options.destination);
|
|
45
|
-
const generatedDir = path.join(cwd,
|
|
54
|
+
const generatedDir = path.join(cwd, INTERNAL_WORK_DIR);
|
|
46
55
|
const stagingDir = path.join(cwd, STAGING_DIR);
|
|
47
56
|
const previewDataPath = path.join(cwd, PREVIEW_DATA_PATH);
|
|
48
57
|
const themeDir = resolveThemeDir(cwd, options);
|
|
@@ -57,6 +66,7 @@ export async function runBuildPages(options) {
|
|
|
57
66
|
generatedDir,
|
|
58
67
|
});
|
|
59
68
|
await assertDirectory(sourceDir, 'Source directory');
|
|
69
|
+
await assertDirectory(themeDir, 'Theme directory');
|
|
60
70
|
await assertPublicDirectory(publicDir, publicDirExplicit);
|
|
61
71
|
await assertDestinationPath(destinationDir);
|
|
62
72
|
await fs.rm(generatedDir, { recursive: true, force: true });
|
|
@@ -104,7 +114,7 @@ export async function runBuildPages(options) {
|
|
|
104
114
|
process.env.ZEROPRESS_PUBLIC_DIR = stagingDir;
|
|
105
115
|
try {
|
|
106
116
|
const result = await runBuild(themeDir, previewData, destinationDir);
|
|
107
|
-
console.log(
|
|
117
|
+
console.log(formatBuildPagesSuccessMessage());
|
|
108
118
|
console.log(`Files: ${result.files.length}`);
|
|
109
119
|
console.log(`Output: ${formatPath(cwd, destinationDir)}`);
|
|
110
120
|
} finally {
|
|
@@ -164,6 +174,10 @@ export function parseArgs(argv) {
|
|
|
164
174
|
continue;
|
|
165
175
|
}
|
|
166
176
|
|
|
177
|
+
if (!arg.startsWith('--')) {
|
|
178
|
+
throw new Error(`Invalid arguments: unexpected positional argument: ${arg}. Use --source <dir> and --destination <dir>.`);
|
|
179
|
+
}
|
|
180
|
+
|
|
167
181
|
throw new Error(`Invalid arguments: unknown option ${arg}`);
|
|
168
182
|
}
|
|
169
183
|
|
|
@@ -200,7 +214,7 @@ Options:
|
|
|
200
214
|
--source <dir> Dedicated source directory (required)
|
|
201
215
|
--public-dir <dir> Public passthrough directory (default: source)
|
|
202
216
|
--destination <dir> Output directory (required)
|
|
203
|
-
--theme docs Bundled theme name (default: docs)
|
|
217
|
+
--theme docs Bundled theme name (default: docs; docs1 aliases docs)
|
|
204
218
|
--theme-path <dir> Custom ZeroPress theme directory
|
|
205
219
|
--config <path> Config file (default: <source>/.zeropress/config.json)
|
|
206
220
|
--site-url <url> Canonical site URL override
|
|
@@ -211,14 +225,58 @@ Options:
|
|
|
211
225
|
--version, -v Show version`);
|
|
212
226
|
}
|
|
213
227
|
|
|
228
|
+
function prefixError(message) {
|
|
229
|
+
if (message.startsWith('[zeropress-build-pages]')) {
|
|
230
|
+
return message;
|
|
231
|
+
}
|
|
232
|
+
return `[zeropress-build-pages] ${message}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function formatBuildPagesSuccessMessage(stream = process.stdout) {
|
|
236
|
+
return createColor(stream).green('Built ZeroPress Pages site successfully');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function colorizeError(message) {
|
|
240
|
+
if (!colorsEnabled(process.stderr)) {
|
|
241
|
+
return message;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return message
|
|
245
|
+
.replace(/^(\[zeropress-build-pages\].*)/m, '\x1b[31m$1\x1b[0m')
|
|
246
|
+
.replace(/\bERROR\b/g, '\x1b[31mERROR\x1b[0m')
|
|
247
|
+
.replace(/\bWARN\b/g, '\x1b[33mWARN\x1b[0m')
|
|
248
|
+
.replace(/\bHint:/g, '\x1b[1mHint:\x1b[0m');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function createColor(stream) {
|
|
252
|
+
const enabled = colorsEnabled(stream);
|
|
253
|
+
const wrap = (code, value) => (enabled ? `\x1b[${code}m${value}\x1b[0m` : value);
|
|
254
|
+
return {
|
|
255
|
+
red: (value) => wrap('31', value),
|
|
256
|
+
yellow: (value) => wrap('33', value),
|
|
257
|
+
green: (value) => wrap('32', value),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function colorsEnabled(stream) {
|
|
262
|
+
if (process.env.NO_COLOR) {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== '0') {
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
return Boolean(stream?.isTTY);
|
|
269
|
+
}
|
|
270
|
+
|
|
214
271
|
function resolveThemeDir(cwd, options) {
|
|
215
272
|
if (options.themePath) {
|
|
216
273
|
return path.resolve(cwd, options.themePath);
|
|
217
274
|
}
|
|
218
|
-
|
|
219
|
-
|
|
275
|
+
const canonicalTheme = BUNDLED_THEME_ALIASES.get(options.theme);
|
|
276
|
+
if (canonicalTheme) {
|
|
277
|
+
return path.join(packageDir, 'themes', canonicalTheme);
|
|
220
278
|
}
|
|
221
|
-
throw new Error(`Unknown bundled theme: ${options.theme}`);
|
|
279
|
+
throw new Error(`Unknown bundled theme: ${options.theme}. Supported bundled themes: ${Array.from(BUNDLED_THEME_ALIASES.keys()).join(', ')}`);
|
|
222
280
|
}
|
|
223
281
|
|
|
224
282
|
function hasExplicitPublicDir(options) {
|
|
@@ -303,11 +361,11 @@ function assertBuildPagesPathLayout({
|
|
|
303
361
|
);
|
|
304
362
|
}
|
|
305
363
|
|
|
306
|
-
assertNoPathOverlap(cwd, 'Source directory', sourceDir,
|
|
307
|
-
assertNoPathOverlap(cwd, 'Destination directory', destinationDir,
|
|
308
|
-
assertNoPathOverlap(cwd, 'Theme directory', themeDir,
|
|
364
|
+
assertNoPathOverlap(cwd, 'Source directory', sourceDir, `internal ${INTERNAL_WORK_DIR} working directory`, generatedDir);
|
|
365
|
+
assertNoPathOverlap(cwd, 'Destination directory', destinationDir, `internal ${INTERNAL_WORK_DIR} working directory`, generatedDir);
|
|
366
|
+
assertNoPathOverlap(cwd, 'Theme directory', themeDir, `internal ${INTERNAL_WORK_DIR} working directory`, generatedDir);
|
|
309
367
|
if (!samePath(publicDir, sourceDir)) {
|
|
310
|
-
assertNoPathOverlap(cwd, 'Public directory', publicDir,
|
|
368
|
+
assertNoPathOverlap(cwd, 'Public directory', publicDir, `internal ${INTERNAL_WORK_DIR} working directory`, generatedDir);
|
|
311
369
|
assertNoPathOverlap(cwd, 'Public directory', publicDir, 'destination directory', destinationDir);
|
|
312
370
|
assertNoPathOverlap(cwd, 'Public directory', publicDir, 'theme directory', themeDir);
|
|
313
371
|
}
|
package/src/prebuild.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
2
3
|
import path from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
3
5
|
import { fileURLToPath } from 'node:url';
|
|
4
6
|
import matter from 'gray-matter';
|
|
5
7
|
|
|
6
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
7
10
|
const rootDir = process.cwd();
|
|
8
11
|
const sourceDir = resolveEnvPath(['ZEROPRESS_BUILD_PAGES_SOURCE'], 'docs');
|
|
9
12
|
const publicDir = resolveEnvPath(['ZEROPRESS_BUILD_PAGES_PUBLIC_DIR'], sourceDir);
|
|
10
13
|
const defaultConfigPath = path.join(sourceDir, '.zeropress', 'config.json');
|
|
11
14
|
const configPath = resolveOptionalEnvPath(['ZEROPRESS_BUILD_PAGES_CONFIG'], defaultConfigPath);
|
|
12
|
-
const outDir = path.join(rootDir, '.zeropress');
|
|
15
|
+
const outDir = path.join(rootDir, '.zeropress-build-page');
|
|
13
16
|
const buildPagesConfigPath = path.join(outDir, 'build-pages-config.json');
|
|
14
17
|
const previewDataPath = path.join(outDir, 'preview-data.json');
|
|
15
18
|
const buildReportPath = path.join(outDir, 'build-report.json');
|
|
@@ -23,6 +26,7 @@ const FRONT_MATTER_DATA_MAX_DEPTH = 4;
|
|
|
23
26
|
const FRONT_MATTER_DATA_MAX_KEYS = 64;
|
|
24
27
|
const FRONT_MATTER_DATA_MAX_ARRAY_LENGTH = 256;
|
|
25
28
|
const FRONT_MATTER_DISCOVERABILITY_VALUES = new Set(['default', 'noindex', 'delist']);
|
|
29
|
+
const MARKDOWN_LAST_UPDATED_VALUES = new Set(['none', 'git']);
|
|
26
30
|
const markdownDiscoverExcludeRoots = buildMarkdownDiscoverExcludeRoots();
|
|
27
31
|
|
|
28
32
|
class PrebuildMarkdownError extends Error {
|
|
@@ -55,10 +59,12 @@ async function main() {
|
|
|
55
59
|
);
|
|
56
60
|
const menus = normalizeMenus(config.menus);
|
|
57
61
|
const customHtmlConfig = normalizeCustomHtmlConfig(config.custom_html);
|
|
62
|
+
const markdownConfig = normalizeMarkdownConfig(config.markdown);
|
|
58
63
|
const resolvedConfig = buildResolvedConfig(config, {
|
|
59
64
|
frontPageConfig,
|
|
60
65
|
menus,
|
|
61
66
|
customHtmlConfig,
|
|
67
|
+
markdownConfig,
|
|
62
68
|
});
|
|
63
69
|
const sourceFiles = await listMarkdownFiles(sourceDir);
|
|
64
70
|
const skippedMarkdown = [];
|
|
@@ -97,22 +103,30 @@ async function main() {
|
|
|
97
103
|
const routeBySourcePath = new Map(
|
|
98
104
|
pageInputs.map(({ sourcePath, route }) => [sourcePath, route]),
|
|
99
105
|
);
|
|
106
|
+
const collections = normalizeCollections(config.collections, pageInputs, skippedMarkdown);
|
|
107
|
+
if (Object.keys(collections).length > 0) {
|
|
108
|
+
resolvedConfig.collections = collections;
|
|
109
|
+
}
|
|
100
110
|
|
|
101
|
-
const pages =
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
111
|
+
const pages = [];
|
|
112
|
+
for (const { sourcePath, bodyMarkdown, frontMatter, title, route } of pageInputs) {
|
|
113
|
+
const meta = await buildPageMeta(sourcePath, frontMatter, markdownConfig);
|
|
114
|
+
pages.push({
|
|
115
|
+
title,
|
|
116
|
+
slug: route.slug,
|
|
117
|
+
path: route.path,
|
|
118
|
+
meta: {
|
|
119
|
+
...meta,
|
|
120
|
+
...(copyMarkdownSource ? { source_markdown_url: buildSourceMarkdownUrl(sourcePath) } : {}),
|
|
121
|
+
},
|
|
122
|
+
...(frontMatter.data !== undefined ? { data: frontMatter.data } : {}),
|
|
123
|
+
...(frontMatter.discoverability !== 'default' ? { discoverability: frontMatter.discoverability } : {}),
|
|
124
|
+
content: rewriteMarkdownLinks(bodyMarkdown, sourcePath, routeBySourcePath),
|
|
125
|
+
document_type: 'markdown',
|
|
126
|
+
excerpt: frontMatter.description || extractExcerpt(bodyMarkdown, title),
|
|
127
|
+
status: 'published',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
116
130
|
|
|
117
131
|
const frontPageResult = await buildFrontPageData(frontPageConfig, pageInputs, resolvedConfig);
|
|
118
132
|
if (frontPageResult.page) {
|
|
@@ -138,6 +152,10 @@ async function main() {
|
|
|
138
152
|
menus,
|
|
139
153
|
widgets: {},
|
|
140
154
|
};
|
|
155
|
+
|
|
156
|
+
if (Object.keys(collections).length > 0) {
|
|
157
|
+
previewData.collections = collections;
|
|
158
|
+
}
|
|
141
159
|
if (customHtml) {
|
|
142
160
|
previewData.custom_html = customHtml;
|
|
143
161
|
}
|
|
@@ -264,11 +282,12 @@ function buildSiteData(config, frontPage) {
|
|
|
264
282
|
return site;
|
|
265
283
|
}
|
|
266
284
|
|
|
267
|
-
function buildResolvedConfig(config, { frontPageConfig, menus, customHtmlConfig }) {
|
|
285
|
+
function buildResolvedConfig(config, { frontPageConfig, menus, customHtmlConfig, markdownConfig }) {
|
|
268
286
|
const resolvedConfig = {
|
|
269
287
|
$schema: BUILD_PAGES_CONFIG_SCHEMA_URL,
|
|
270
288
|
version: '0.1',
|
|
271
289
|
site: normalizeSiteConfig(config.site),
|
|
290
|
+
markdown: markdownConfig,
|
|
272
291
|
front_page: frontPageConfig,
|
|
273
292
|
menus,
|
|
274
293
|
};
|
|
@@ -280,6 +299,25 @@ function buildResolvedConfig(config, { frontPageConfig, menus, customHtmlConfig
|
|
|
280
299
|
return resolvedConfig;
|
|
281
300
|
}
|
|
282
301
|
|
|
302
|
+
function normalizeMarkdownConfig(value) {
|
|
303
|
+
if (value === undefined) {
|
|
304
|
+
return {
|
|
305
|
+
last_updated: 'none',
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
if (!isPlainObject(value)) {
|
|
309
|
+
throw new PrebuildConfigError(
|
|
310
|
+
'markdown must be an object.',
|
|
311
|
+
' "markdown": { "last_updated": "git" }',
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
assertKnownConfigKeys(value, ['last_updated'], 'markdown');
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
last_updated: normalizeLastUpdatedPolicy(value.last_updated, 'markdown.last_updated', PrebuildConfigError),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
283
321
|
function normalizeSiteConfig(value) {
|
|
284
322
|
if (value !== undefined && !isPlainObject(value)) {
|
|
285
323
|
throw new PrebuildConfigError(
|
|
@@ -853,6 +891,79 @@ function defaultMenus() {
|
|
|
853
891
|
};
|
|
854
892
|
}
|
|
855
893
|
|
|
894
|
+
function normalizeCollections(value, pageInputs, skippedMarkdown) {
|
|
895
|
+
if (value === undefined) {
|
|
896
|
+
return {};
|
|
897
|
+
}
|
|
898
|
+
if (!isPlainObject(value)) {
|
|
899
|
+
throw new PrebuildConfigError('collections must be an object keyed by collection id.');
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const pageBySourcePath = new Map(pageInputs.map((pageInput) => [pageInput.sourcePath, pageInput]));
|
|
903
|
+
const skippedByFile = new Map(
|
|
904
|
+
skippedMarkdown.map((entry) => [path.resolve(rootDir, entry.file), entry.reason]),
|
|
905
|
+
);
|
|
906
|
+
const collections = {};
|
|
907
|
+
|
|
908
|
+
for (const [collectionId, collection] of Object.entries(value)) {
|
|
909
|
+
validateConfigId(collectionId, `collections.${collectionId}`);
|
|
910
|
+
if (!isPlainObject(collection)) {
|
|
911
|
+
throw new PrebuildConfigError(`collections.${collectionId} must be an object.`);
|
|
912
|
+
}
|
|
913
|
+
assertKnownConfigKeys(collection, ['title', 'description', 'items'], `collections.${collectionId}`);
|
|
914
|
+
if (!Array.isArray(collection.items)) {
|
|
915
|
+
throw new PrebuildConfigError(`collections.${collectionId}.items must be an array of Markdown source paths.`);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const seenSourcePaths = new Set();
|
|
919
|
+
const items = collection.items.map((item, index) => {
|
|
920
|
+
const pathLabel = `collections.${collectionId}.items[${index}]`;
|
|
921
|
+
const normalizedPath = resolveCollectionSourcePath(item, pathLabel);
|
|
922
|
+
const sourcePath = path.resolve(sourceDir, normalizedPath);
|
|
923
|
+
if (seenSourcePaths.has(sourcePath)) {
|
|
924
|
+
throw new PrebuildConfigError(`${pathLabel} duplicates ${normalizedPath} in collections.${collectionId}.`);
|
|
925
|
+
}
|
|
926
|
+
seenSourcePaths.add(sourcePath);
|
|
927
|
+
|
|
928
|
+
const pageInput = pageBySourcePath.get(sourcePath);
|
|
929
|
+
if (!pageInput) {
|
|
930
|
+
const skippedReason = skippedByFile.get(sourcePath);
|
|
931
|
+
if (skippedReason) {
|
|
932
|
+
throw new PrebuildConfigError(`${pathLabel} references skipped Markdown ${normalizedPath}: ${skippedReason}`);
|
|
933
|
+
}
|
|
934
|
+
throw new PrebuildConfigError(`${pathLabel} was not discovered as a Markdown page: ${normalizedPath}`);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
return {
|
|
938
|
+
type: 'page',
|
|
939
|
+
slug: pageInput.route.slug,
|
|
940
|
+
};
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
collections[collectionId] = {
|
|
944
|
+
title: readConfigString(collection.title, collectionId),
|
|
945
|
+
...(collection.description !== undefined ? { description: readConfigString(collection.description, '') } : {}),
|
|
946
|
+
items,
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
return collections;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function resolveCollectionSourcePath(value, pathLabel) {
|
|
954
|
+
const normalizedPath = normalizeSourceFilePath(value, pathLabel);
|
|
955
|
+
if (!normalizedPath.toLowerCase().endsWith('.md')) {
|
|
956
|
+
throw new PrebuildConfigError(`${pathLabel} must be a Markdown source path ending in .md.`);
|
|
957
|
+
}
|
|
958
|
+
return normalizedPath;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function validateConfigId(value, pathLabel) {
|
|
962
|
+
if (!/^[a-z][a-z0-9_-]{0,63}$/.test(value)) {
|
|
963
|
+
throw new PrebuildConfigError(`${pathLabel} must use a lowercase config id such as "docs" or "reference-guides".`);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
856
967
|
function buildPrebuildReport({
|
|
857
968
|
sourceFiles,
|
|
858
969
|
pageInputs,
|
|
@@ -970,12 +1081,42 @@ function normalizePublishedFrontMatter(frontMatter, sourcePath) {
|
|
|
970
1081
|
title: normalizeFrontMatterTitle(frontMatter.title, sourcePath),
|
|
971
1082
|
description: normalizeFrontMatterDescription(frontMatter.description, sourcePath),
|
|
972
1083
|
path: normalizeFrontMatterRoutePath(frontMatter.path, sourcePath),
|
|
1084
|
+
last_updated: normalizeFrontMatterLastUpdated(frontMatter.last_updated, sourcePath),
|
|
973
1085
|
discoverability: normalizeFrontMatterDiscoverability(frontMatter.discoverability, sourcePath),
|
|
974
1086
|
meta: normalizeFrontMatterMeta(frontMatter.meta, sourcePath),
|
|
975
1087
|
data: normalizeFrontMatterData(frontMatter.data, sourcePath),
|
|
976
1088
|
};
|
|
977
1089
|
}
|
|
978
1090
|
|
|
1091
|
+
function normalizeLastUpdatedPolicy(value, pathLabel, ErrorClass, sourcePath = null) {
|
|
1092
|
+
if (value === undefined) {
|
|
1093
|
+
return 'none';
|
|
1094
|
+
}
|
|
1095
|
+
if (typeof value === 'string' && MARKDOWN_LAST_UPDATED_VALUES.has(value)) {
|
|
1096
|
+
return value;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
if (ErrorClass === PrebuildMarkdownError) {
|
|
1100
|
+
throw new ErrorClass(
|
|
1101
|
+
sourcePath,
|
|
1102
|
+
`${pathLabel} must be one of: ${Array.from(MARKDOWN_LAST_UPDATED_VALUES).join(', ')}.`,
|
|
1103
|
+
' last_updated: none\n last_updated: git',
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
throw new ErrorClass(
|
|
1108
|
+
`${pathLabel} must be one of: ${Array.from(MARKDOWN_LAST_UPDATED_VALUES).join(', ')}.`,
|
|
1109
|
+
' "markdown": { "last_updated": "none" }\n "markdown": { "last_updated": "git" }',
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function normalizeFrontMatterLastUpdated(value, sourcePath) {
|
|
1114
|
+
if (value === undefined) {
|
|
1115
|
+
return undefined;
|
|
1116
|
+
}
|
|
1117
|
+
return normalizeLastUpdatedPolicy(value, 'front matter last_updated', PrebuildMarkdownError, sourcePath);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
979
1120
|
function normalizeFrontMatterTitle(value, sourcePath) {
|
|
980
1121
|
if (value === undefined) {
|
|
981
1122
|
return '';
|
|
@@ -1082,6 +1223,87 @@ function normalizeFrontMatterMeta(value, sourcePath) {
|
|
|
1082
1223
|
return meta;
|
|
1083
1224
|
}
|
|
1084
1225
|
|
|
1226
|
+
async function buildPageMeta(sourcePath, frontMatter, markdownConfig) {
|
|
1227
|
+
const meta = {
|
|
1228
|
+
...frontMatter.meta,
|
|
1229
|
+
};
|
|
1230
|
+
|
|
1231
|
+
if (hasManualLastUpdatedMeta(meta)) {
|
|
1232
|
+
return meta;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
const lastUpdatedPolicy = frontMatter.last_updated || markdownConfig.last_updated;
|
|
1236
|
+
if (lastUpdatedPolicy !== 'git') {
|
|
1237
|
+
return meta;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
const lastUpdatedIso = await readGitLastUpdatedIso(sourcePath);
|
|
1241
|
+
if (!lastUpdatedIso) {
|
|
1242
|
+
return meta;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
return {
|
|
1246
|
+
...meta,
|
|
1247
|
+
last_updated_iso: lastUpdatedIso,
|
|
1248
|
+
last_updated: lastUpdatedIso.slice(0, 10),
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function hasManualLastUpdatedMeta(meta) {
|
|
1253
|
+
return (
|
|
1254
|
+
Object.hasOwn(meta, 'last_updated')
|
|
1255
|
+
|| Object.hasOwn(meta, 'last_updated_iso')
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
async function readGitLastUpdatedIso(sourcePath) {
|
|
1260
|
+
const realSourcePath = await resolveRealPath(sourcePath);
|
|
1261
|
+
const realRootDir = await resolveRealPath(rootDir);
|
|
1262
|
+
const gitPath = path.relative(realRootDir, realSourcePath);
|
|
1263
|
+
try {
|
|
1264
|
+
const { stdout } = await execFileAsync('git', [
|
|
1265
|
+
'-C',
|
|
1266
|
+
realRootDir,
|
|
1267
|
+
'log',
|
|
1268
|
+
'-1',
|
|
1269
|
+
'--format=%cI',
|
|
1270
|
+
'--',
|
|
1271
|
+
gitPath,
|
|
1272
|
+
], {
|
|
1273
|
+
encoding: 'utf8',
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
const value = stdout.trim();
|
|
1277
|
+
if (!value) {
|
|
1278
|
+
warnGitLastUpdated(sourcePath, 'no commit date was found for this file.');
|
|
1279
|
+
return '';
|
|
1280
|
+
}
|
|
1281
|
+
if (!/^\d{4}-\d{2}-\d{2}T/.test(value)) {
|
|
1282
|
+
warnGitLastUpdated(sourcePath, `unexpected git date output: ${value}`);
|
|
1283
|
+
return '';
|
|
1284
|
+
}
|
|
1285
|
+
return value;
|
|
1286
|
+
} catch (error) {
|
|
1287
|
+
warnGitLastUpdated(sourcePath, error instanceof Error ? error.message : String(error));
|
|
1288
|
+
return '';
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
async function resolveRealPath(value) {
|
|
1293
|
+
try {
|
|
1294
|
+
return await fs.realpath(value);
|
|
1295
|
+
} catch {
|
|
1296
|
+
return value;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
function warnGitLastUpdated(sourcePath, reason) {
|
|
1301
|
+
console.warn([
|
|
1302
|
+
`[zeropress-build-pages] Warning: could not read git last_updated for ${formatSourcePath(sourcePath)}.`,
|
|
1303
|
+
`Reason: ${reason}`,
|
|
1304
|
+
].join('\n'));
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1085
1307
|
function isPreviewMetaValue(value) {
|
|
1086
1308
|
return (
|
|
1087
1309
|
value === null
|
|
@@ -1358,11 +1580,11 @@ function buildRoutePath(relativeSourcePath, sourcePath, options = {}) {
|
|
|
1358
1580
|
}
|
|
1359
1581
|
|
|
1360
1582
|
function buildSlug(routePath) {
|
|
1361
|
-
const segments = routePath.split('/');
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
return sanitizePathSegment(
|
|
1583
|
+
const segments = routePath.split('/').filter(Boolean);
|
|
1584
|
+
if (segments.length > 1 && segments.at(-1) === 'index') {
|
|
1585
|
+
segments.pop();
|
|
1586
|
+
}
|
|
1587
|
+
return sanitizePathSegment(segments.join('-') || 'index');
|
|
1366
1588
|
}
|
|
1367
1589
|
|
|
1368
1590
|
function sanitizePathSegment(segment) {
|
|
@@ -200,14 +200,9 @@ a:hover {
|
|
|
200
200
|
top: 0;
|
|
201
201
|
z-index: 20;
|
|
202
202
|
background: var(--header-bg);
|
|
203
|
-
border-bottom: 1px solid
|
|
203
|
+
border-bottom: 1px solid var(--line);
|
|
204
204
|
backdrop-filter: saturate(140%) blur(14px);
|
|
205
205
|
-webkit-backdrop-filter: saturate(140%) blur(14px);
|
|
206
|
-
transition: border-color 200ms var(--ease), background 200ms var(--ease);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
.site-header.is-scrolled {
|
|
210
|
-
border-bottom-color: var(--line);
|
|
211
206
|
}
|
|
212
207
|
|
|
213
208
|
.site-header__inner {
|
|
@@ -451,7 +446,11 @@ a:hover {
|
|
|
451
446
|
transition: border-color 120ms var(--ease), background 120ms var(--ease), color 120ms var(--ease);
|
|
452
447
|
}
|
|
453
448
|
|
|
454
|
-
.theme-toggle:
|
|
449
|
+
.theme-toggle:disabled {
|
|
450
|
+
cursor: not-allowed;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.theme-toggle:not(:disabled):hover {
|
|
455
454
|
border-color: var(--line-strong);
|
|
456
455
|
color: var(--accent);
|
|
457
456
|
}
|