@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/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,28 +1,32 @@
|
|
|
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');
|
|
16
19
|
const skipUntitledMarkdown = readBooleanEnv('ZEROPRESS_SKIP_UNTITLED_MARKDOWN');
|
|
17
20
|
const copyMarkdownSource = readBooleanEnv('ZEROPRESS_COPY_MARKDOWN_SOURCE', true);
|
|
18
21
|
const FRONT_PAGE_TYPES = new Set(['theme_index', 'markdown', 'html']);
|
|
19
|
-
const BUILD_PAGES_CONFIG_SCHEMA_URL = 'https://zeropress.dev/
|
|
20
|
-
const PREVIEW_DATA_SCHEMA_URL = 'https://zeropress.dev/
|
|
22
|
+
const BUILD_PAGES_CONFIG_SCHEMA_URL = 'https://schemas.zeropress.dev/build-pages-config/v0.1/schema.json';
|
|
23
|
+
const PREVIEW_DATA_SCHEMA_URL = 'https://schemas.zeropress.dev/preview-data/v0.6/schema.json';
|
|
21
24
|
const FRONT_MATTER_DATA_KEY_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*(?:-[a-zA-Z0-9_]+)*$/;
|
|
22
25
|
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
|
}
|
|
@@ -232,7 +250,7 @@ function buildSiteData(config, frontPage) {
|
|
|
232
250
|
description: configuredSite.description,
|
|
233
251
|
url: configuredSite.url,
|
|
234
252
|
media_base_url: '',
|
|
235
|
-
locale:
|
|
253
|
+
locale: configuredSite.locale,
|
|
236
254
|
posts_per_page: 10,
|
|
237
255
|
datetime_display: 'static',
|
|
238
256
|
date_style: 'medium',
|
|
@@ -249,6 +267,14 @@ function buildSiteData(config, frontPage) {
|
|
|
249
267
|
indexing: configuredSite.indexing !== false,
|
|
250
268
|
};
|
|
251
269
|
|
|
270
|
+
if (configuredSite.logo) {
|
|
271
|
+
site.logo = configuredSite.logo;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (configuredSite.meta !== undefined) {
|
|
275
|
+
site.meta = configuredSite.meta;
|
|
276
|
+
}
|
|
277
|
+
|
|
252
278
|
if (configuredSite.footer) {
|
|
253
279
|
site.footer = configuredSite.footer;
|
|
254
280
|
}
|
|
@@ -256,11 +282,12 @@ function buildSiteData(config, frontPage) {
|
|
|
256
282
|
return site;
|
|
257
283
|
}
|
|
258
284
|
|
|
259
|
-
function buildResolvedConfig(config, { frontPageConfig, menus, customHtmlConfig }) {
|
|
285
|
+
function buildResolvedConfig(config, { frontPageConfig, menus, customHtmlConfig, markdownConfig }) {
|
|
260
286
|
const resolvedConfig = {
|
|
261
287
|
$schema: BUILD_PAGES_CONFIG_SCHEMA_URL,
|
|
262
288
|
version: '0.1',
|
|
263
289
|
site: normalizeSiteConfig(config.site),
|
|
290
|
+
markdown: markdownConfig,
|
|
264
291
|
front_page: frontPageConfig,
|
|
265
292
|
menus,
|
|
266
293
|
};
|
|
@@ -272,6 +299,25 @@ function buildResolvedConfig(config, { frontPageConfig, menus, customHtmlConfig
|
|
|
272
299
|
return resolvedConfig;
|
|
273
300
|
}
|
|
274
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
|
+
|
|
275
321
|
function normalizeSiteConfig(value) {
|
|
276
322
|
if (value !== undefined && !isPlainObject(value)) {
|
|
277
323
|
throw new PrebuildConfigError(
|
|
@@ -281,24 +327,98 @@ function normalizeSiteConfig(value) {
|
|
|
281
327
|
}
|
|
282
328
|
|
|
283
329
|
const configuredSite = isPlainObject(value) ? value : {};
|
|
284
|
-
assertKnownConfigKeys(configuredSite, ['title', 'description', 'url', 'expose_generator', 'search', 'indexing', 'footer'], 'site');
|
|
330
|
+
assertKnownConfigKeys(configuredSite, ['title', 'description', 'url', 'logo', 'locale', 'expose_generator', 'search', 'indexing', 'footer', 'meta'], 'site');
|
|
285
331
|
const site = {
|
|
286
332
|
title: readConfigString(configuredSite.title, 'Documentation'),
|
|
287
333
|
description: readConfigString(configuredSite.description, 'A documentation site.'),
|
|
288
334
|
url: readEnv('ZEROPRESS_SITE_URL', readConfigString(configuredSite.url, '')),
|
|
335
|
+
locale: normalizeSiteLocale(configuredSite.locale),
|
|
289
336
|
expose_generator: readConfigBoolean(configuredSite.expose_generator, true, 'site.expose_generator'),
|
|
290
337
|
search: readConfigBoolean(configuredSite.search, true, 'site.search'),
|
|
291
338
|
indexing: readConfigBoolean(configuredSite.indexing, true, 'site.indexing'),
|
|
292
339
|
};
|
|
293
340
|
|
|
341
|
+
const logo = normalizeSiteLogo(configuredSite.logo);
|
|
342
|
+
if (logo) {
|
|
343
|
+
site.logo = logo;
|
|
344
|
+
}
|
|
345
|
+
|
|
294
346
|
const footer = normalizeFooter(configuredSite.footer);
|
|
295
347
|
if (footer) {
|
|
296
348
|
site.footer = footer;
|
|
297
349
|
}
|
|
298
350
|
|
|
351
|
+
if (configuredSite.meta !== undefined) {
|
|
352
|
+
site.meta = normalizeSiteMeta(configuredSite.meta, 'site.meta');
|
|
353
|
+
}
|
|
354
|
+
|
|
299
355
|
return site;
|
|
300
356
|
}
|
|
301
357
|
|
|
358
|
+
function normalizeSiteLocale(value) {
|
|
359
|
+
if (value === undefined) {
|
|
360
|
+
return 'en-US';
|
|
361
|
+
}
|
|
362
|
+
if (typeof value !== 'string') {
|
|
363
|
+
throw new PrebuildConfigError('site.locale must be a string when provided.');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const locale = value.trim();
|
|
367
|
+
if (locale.length < 2) {
|
|
368
|
+
throw new PrebuildConfigError('site.locale must be a non-empty locale string such as "en-US" or "ko-KR".');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return locale;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function normalizeSiteLogo(value) {
|
|
375
|
+
if (value === undefined) {
|
|
376
|
+
return undefined;
|
|
377
|
+
}
|
|
378
|
+
if (!isPlainObject(value)) {
|
|
379
|
+
throw new PrebuildConfigError('site.logo must be an object when provided.');
|
|
380
|
+
}
|
|
381
|
+
assertKnownConfigKeys(value, ['src', 'alt'], 'site.logo');
|
|
382
|
+
|
|
383
|
+
const src = readConfigString(value.src, '');
|
|
384
|
+
if (!src) {
|
|
385
|
+
throw new PrebuildConfigError(
|
|
386
|
+
'site.logo.src must be a non-empty URL-like string.',
|
|
387
|
+
' "logo": { "src": "/logo.svg", "alt": "My Site" }',
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
validateUrlLikeString(src, 'site.logo.src');
|
|
391
|
+
|
|
392
|
+
const logo = { src };
|
|
393
|
+
if (value.alt !== undefined) {
|
|
394
|
+
if (typeof value.alt !== 'string') {
|
|
395
|
+
throw new PrebuildConfigError('site.logo.alt must be a string when provided.');
|
|
396
|
+
}
|
|
397
|
+
const alt = value.alt.trim();
|
|
398
|
+
if (alt) {
|
|
399
|
+
logo.alt = alt;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return logo;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function normalizeSiteMeta(value, pathLabel) {
|
|
407
|
+
if (!isPlainObject(value)) {
|
|
408
|
+
throw new PrebuildConfigError(`${pathLabel} must be an object when provided.`);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const meta = {};
|
|
412
|
+
for (const [key, metaValue] of Object.entries(value)) {
|
|
413
|
+
if (!isPreviewMetaValue(metaValue)) {
|
|
414
|
+
throw new PrebuildConfigError(`${pathLabel}.${key} must be a string, number, boolean, or null.`);
|
|
415
|
+
}
|
|
416
|
+
meta[key] = metaValue;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return meta;
|
|
420
|
+
}
|
|
421
|
+
|
|
302
422
|
function normalizeFooter(value) {
|
|
303
423
|
if (value === undefined) {
|
|
304
424
|
return undefined;
|
|
@@ -324,6 +444,25 @@ function normalizeFooter(value) {
|
|
|
324
444
|
return Object.keys(footer).length ? footer : undefined;
|
|
325
445
|
}
|
|
326
446
|
|
|
447
|
+
function validateUrlLikeString(value, pathLabel) {
|
|
448
|
+
if (value.startsWith('//')) {
|
|
449
|
+
throw new PrebuildConfigError(`${pathLabel} must be an absolute URL or a safe relative path.`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (value.startsWith('/') || value.startsWith('./') || value.startsWith('../')) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
const url = new URL(value);
|
|
458
|
+
if (!url.protocol || !url.hostname) {
|
|
459
|
+
throw new Error('missing host');
|
|
460
|
+
}
|
|
461
|
+
} catch {
|
|
462
|
+
throw new PrebuildConfigError(`${pathLabel} must be an absolute URL or a safe relative path.`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
327
466
|
function readConfigBoolean(value, fallback, pathName) {
|
|
328
467
|
if (value === undefined) {
|
|
329
468
|
return fallback;
|
|
@@ -752,6 +891,79 @@ function defaultMenus() {
|
|
|
752
891
|
};
|
|
753
892
|
}
|
|
754
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
|
+
|
|
755
967
|
function buildPrebuildReport({
|
|
756
968
|
sourceFiles,
|
|
757
969
|
pageInputs,
|
|
@@ -869,12 +1081,42 @@ function normalizePublishedFrontMatter(frontMatter, sourcePath) {
|
|
|
869
1081
|
title: normalizeFrontMatterTitle(frontMatter.title, sourcePath),
|
|
870
1082
|
description: normalizeFrontMatterDescription(frontMatter.description, sourcePath),
|
|
871
1083
|
path: normalizeFrontMatterRoutePath(frontMatter.path, sourcePath),
|
|
1084
|
+
last_updated: normalizeFrontMatterLastUpdated(frontMatter.last_updated, sourcePath),
|
|
872
1085
|
discoverability: normalizeFrontMatterDiscoverability(frontMatter.discoverability, sourcePath),
|
|
873
1086
|
meta: normalizeFrontMatterMeta(frontMatter.meta, sourcePath),
|
|
874
1087
|
data: normalizeFrontMatterData(frontMatter.data, sourcePath),
|
|
875
1088
|
};
|
|
876
1089
|
}
|
|
877
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
|
+
|
|
878
1120
|
function normalizeFrontMatterTitle(value, sourcePath) {
|
|
879
1121
|
if (value === undefined) {
|
|
880
1122
|
return '';
|
|
@@ -981,6 +1223,87 @@ function normalizeFrontMatterMeta(value, sourcePath) {
|
|
|
981
1223
|
return meta;
|
|
982
1224
|
}
|
|
983
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
|
+
|
|
984
1307
|
function isPreviewMetaValue(value) {
|
|
985
1308
|
return (
|
|
986
1309
|
value === null
|
|
@@ -1257,11 +1580,11 @@ function buildRoutePath(relativeSourcePath, sourcePath, options = {}) {
|
|
|
1257
1580
|
}
|
|
1258
1581
|
|
|
1259
1582
|
function buildSlug(routePath) {
|
|
1260
|
-
const segments = routePath.split('/');
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
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');
|
|
1265
1588
|
}
|
|
1266
1589
|
|
|
1267
1590
|
function sanitizePathSegment(segment) {
|
package/themes/docs/404.html
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
<section class="shell not-found">
|
|
2
|
-
<p class="eyebrow">
|
|
2
|
+
<p class="eyebrow eyebrow--error">
|
|
3
|
+
<span class="eyebrow__dot" aria-hidden="true"></span>
|
|
4
|
+
404
|
|
5
|
+
</p>
|
|
3
6
|
<h1>Page not found</h1>
|
|
4
7
|
<p class="lede">The page you are looking for is not part of the current documentation build.</p>
|
|
5
|
-
<
|
|
8
|
+
<div class="hero__actions">
|
|
9
|
+
<a class="button-link" href="/">
|
|
10
|
+
Return home
|
|
11
|
+
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
12
|
+
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
13
|
+
<polyline points="12 5 19 12 12 19"></polyline>
|
|
14
|
+
</svg>
|
|
15
|
+
</a>
|
|
16
|
+
</div>
|
|
6
17
|
</section>
|