@zeropress/build-pages 0.6.1 → 0.6.3

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/src/prebuild.js CHANGED
@@ -6,6 +6,7 @@ import matter from 'gray-matter';
6
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
7
  const rootDir = process.cwd();
8
8
  const sourceDir = resolveEnvPath(['ZEROPRESS_BUILD_PAGES_SOURCE'], 'docs');
9
+ const publicDir = resolveEnvPath(['ZEROPRESS_BUILD_PAGES_PUBLIC_DIR'], sourceDir);
9
10
  const defaultConfigPath = path.join(sourceDir, '.zeropress', 'config.json');
10
11
  const configPath = resolveOptionalEnvPath(['ZEROPRESS_BUILD_PAGES_CONFIG'], defaultConfigPath);
11
12
  const outDir = path.join(rootDir, '.zeropress');
@@ -15,13 +16,14 @@ const buildReportPath = path.join(outDir, 'build-report.json');
15
16
  const skipUntitledMarkdown = readBooleanEnv('ZEROPRESS_SKIP_UNTITLED_MARKDOWN');
16
17
  const copyMarkdownSource = readBooleanEnv('ZEROPRESS_COPY_MARKDOWN_SOURCE', true);
17
18
  const FRONT_PAGE_TYPES = new Set(['theme_index', 'markdown', 'html']);
18
- const BUILD_PAGES_CONFIG_SCHEMA_URL = 'https://zeropress.dev/schemas/zeropress-build-pages.config.v0.1.schema.json';
19
- const PREVIEW_DATA_SCHEMA_URL = 'https://zeropress.dev/schemas/preview-data.v0.6.schema.json';
19
+ const BUILD_PAGES_CONFIG_SCHEMA_URL = 'https://schemas.zeropress.dev/build-pages-config/v0.1/schema.json';
20
+ const PREVIEW_DATA_SCHEMA_URL = 'https://schemas.zeropress.dev/preview-data/v0.6/schema.json';
20
21
  const FRONT_MATTER_DATA_KEY_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*(?:-[a-zA-Z0-9_]+)*$/;
21
22
  const FRONT_MATTER_DATA_MAX_DEPTH = 4;
22
23
  const FRONT_MATTER_DATA_MAX_KEYS = 64;
23
24
  const FRONT_MATTER_DATA_MAX_ARRAY_LENGTH = 256;
24
25
  const FRONT_MATTER_DISCOVERABILITY_VALUES = new Set(['default', 'noindex', 'delist']);
26
+ const markdownDiscoverExcludeRoots = buildMarkdownDiscoverExcludeRoots();
25
27
 
26
28
  class PrebuildMarkdownError extends Error {
27
29
  constructor(sourcePath, reason, expected = '', code = 'invalid_markdown') {
@@ -230,7 +232,7 @@ function buildSiteData(config, frontPage) {
230
232
  description: configuredSite.description,
231
233
  url: configuredSite.url,
232
234
  media_base_url: '',
233
- locale: 'en-US',
235
+ locale: configuredSite.locale,
234
236
  posts_per_page: 10,
235
237
  datetime_display: 'static',
236
238
  date_style: 'medium',
@@ -243,9 +245,18 @@ function buildSiteData(config, frontPage) {
243
245
  },
244
246
  disallow_comments: true,
245
247
  expose_generator: configuredSite.expose_generator !== false,
248
+ search: configuredSite.search !== false,
246
249
  indexing: configuredSite.indexing !== false,
247
250
  };
248
251
 
252
+ if (configuredSite.logo) {
253
+ site.logo = configuredSite.logo;
254
+ }
255
+
256
+ if (configuredSite.meta !== undefined) {
257
+ site.meta = configuredSite.meta;
258
+ }
259
+
249
260
  if (configuredSite.footer) {
250
261
  site.footer = configuredSite.footer;
251
262
  }
@@ -278,23 +289,98 @@ function normalizeSiteConfig(value) {
278
289
  }
279
290
 
280
291
  const configuredSite = isPlainObject(value) ? value : {};
281
- assertKnownConfigKeys(configuredSite, ['title', 'description', 'url', 'expose_generator', 'indexing', 'footer'], 'site');
292
+ assertKnownConfigKeys(configuredSite, ['title', 'description', 'url', 'logo', 'locale', 'expose_generator', 'search', 'indexing', 'footer', 'meta'], 'site');
282
293
  const site = {
283
294
  title: readConfigString(configuredSite.title, 'Documentation'),
284
295
  description: readConfigString(configuredSite.description, 'A documentation site.'),
285
296
  url: readEnv('ZEROPRESS_SITE_URL', readConfigString(configuredSite.url, '')),
297
+ locale: normalizeSiteLocale(configuredSite.locale),
286
298
  expose_generator: readConfigBoolean(configuredSite.expose_generator, true, 'site.expose_generator'),
299
+ search: readConfigBoolean(configuredSite.search, true, 'site.search'),
287
300
  indexing: readConfigBoolean(configuredSite.indexing, true, 'site.indexing'),
288
301
  };
289
302
 
303
+ const logo = normalizeSiteLogo(configuredSite.logo);
304
+ if (logo) {
305
+ site.logo = logo;
306
+ }
307
+
290
308
  const footer = normalizeFooter(configuredSite.footer);
291
309
  if (footer) {
292
310
  site.footer = footer;
293
311
  }
294
312
 
313
+ if (configuredSite.meta !== undefined) {
314
+ site.meta = normalizeSiteMeta(configuredSite.meta, 'site.meta');
315
+ }
316
+
295
317
  return site;
296
318
  }
297
319
 
320
+ function normalizeSiteLocale(value) {
321
+ if (value === undefined) {
322
+ return 'en-US';
323
+ }
324
+ if (typeof value !== 'string') {
325
+ throw new PrebuildConfigError('site.locale must be a string when provided.');
326
+ }
327
+
328
+ const locale = value.trim();
329
+ if (locale.length < 2) {
330
+ throw new PrebuildConfigError('site.locale must be a non-empty locale string such as "en-US" or "ko-KR".');
331
+ }
332
+
333
+ return locale;
334
+ }
335
+
336
+ function normalizeSiteLogo(value) {
337
+ if (value === undefined) {
338
+ return undefined;
339
+ }
340
+ if (!isPlainObject(value)) {
341
+ throw new PrebuildConfigError('site.logo must be an object when provided.');
342
+ }
343
+ assertKnownConfigKeys(value, ['src', 'alt'], 'site.logo');
344
+
345
+ const src = readConfigString(value.src, '');
346
+ if (!src) {
347
+ throw new PrebuildConfigError(
348
+ 'site.logo.src must be a non-empty URL-like string.',
349
+ ' "logo": { "src": "/logo.svg", "alt": "My Site" }',
350
+ );
351
+ }
352
+ validateUrlLikeString(src, 'site.logo.src');
353
+
354
+ const logo = { src };
355
+ if (value.alt !== undefined) {
356
+ if (typeof value.alt !== 'string') {
357
+ throw new PrebuildConfigError('site.logo.alt must be a string when provided.');
358
+ }
359
+ const alt = value.alt.trim();
360
+ if (alt) {
361
+ logo.alt = alt;
362
+ }
363
+ }
364
+
365
+ return logo;
366
+ }
367
+
368
+ function normalizeSiteMeta(value, pathLabel) {
369
+ if (!isPlainObject(value)) {
370
+ throw new PrebuildConfigError(`${pathLabel} must be an object when provided.`);
371
+ }
372
+
373
+ const meta = {};
374
+ for (const [key, metaValue] of Object.entries(value)) {
375
+ if (!isPreviewMetaValue(metaValue)) {
376
+ throw new PrebuildConfigError(`${pathLabel}.${key} must be a string, number, boolean, or null.`);
377
+ }
378
+ meta[key] = metaValue;
379
+ }
380
+
381
+ return meta;
382
+ }
383
+
298
384
  function normalizeFooter(value) {
299
385
  if (value === undefined) {
300
386
  return undefined;
@@ -320,6 +406,25 @@ function normalizeFooter(value) {
320
406
  return Object.keys(footer).length ? footer : undefined;
321
407
  }
322
408
 
409
+ function validateUrlLikeString(value, pathLabel) {
410
+ if (value.startsWith('//')) {
411
+ throw new PrebuildConfigError(`${pathLabel} must be an absolute URL or a safe relative path.`);
412
+ }
413
+
414
+ if (value.startsWith('/') || value.startsWith('./') || value.startsWith('../')) {
415
+ return;
416
+ }
417
+
418
+ try {
419
+ const url = new URL(value);
420
+ if (!url.protocol || !url.hostname) {
421
+ throw new Error('missing host');
422
+ }
423
+ } catch {
424
+ throw new PrebuildConfigError(`${pathLabel} must be an absolute URL or a safe relative path.`);
425
+ }
426
+ }
427
+
323
428
  function readConfigBoolean(value, fallback, pathName) {
324
429
  if (value === undefined) {
325
430
  return fallback;
@@ -760,6 +865,7 @@ function buildPrebuildReport({
760
865
  return {
761
866
  generated_at: new Date().toISOString(),
762
867
  source_dir: formatSourcePath(sourceDir),
868
+ public_dir: formatSourcePath(publicDir),
763
869
  config_path: formatSourcePath(configPath),
764
870
  build_pages_config_path: formatSourcePath(buildPagesConfigPath),
765
871
  preview_data_path: formatSourcePath(previewDataPath),
@@ -786,7 +892,8 @@ function buildPrebuildReport({
786
892
  function printPrebuildSummary(report) {
787
893
  const lines = [
788
894
  'ZeroPress build report',
789
- `- Public root: ${report.source_dir}`,
895
+ `- Source root: ${report.source_dir}`,
896
+ `- Public root: ${report.public_dir}`,
790
897
  `- Markdown discovered: ${report.markdown.discovered}`,
791
898
  `- Markdown pages generated: ${report.markdown.generated_pages}`,
792
899
  `- Markdown skipped: ${report.markdown.skipped}`,
@@ -1134,6 +1241,10 @@ async function listMarkdownFiles(dir) {
1134
1241
  }
1135
1242
 
1136
1243
  const entryPath = path.join(dir, entry.name);
1244
+ if (isMarkdownDiscoverExcluded(entryPath)) {
1245
+ continue;
1246
+ }
1247
+
1137
1248
  if (entry.isDirectory()) {
1138
1249
  files.push(...await listMarkdownFiles(entryPath));
1139
1250
  continue;
@@ -1147,6 +1258,24 @@ async function listMarkdownFiles(dir) {
1147
1258
  return files.sort((left, right) => left.localeCompare(right));
1148
1259
  }
1149
1260
 
1261
+ function buildMarkdownDiscoverExcludeRoots() {
1262
+ if (samePath(sourceDir, publicDir) || !isPathInside(sourceDir, publicDir)) {
1263
+ return [];
1264
+ }
1265
+
1266
+ return [publicDir];
1267
+ }
1268
+
1269
+ function isMarkdownDiscoverExcluded(entryPath) {
1270
+ return markdownDiscoverExcludeRoots.some((excludeRoot) => (
1271
+ samePath(entryPath, excludeRoot) || isPathInside(excludeRoot, entryPath)
1272
+ ));
1273
+ }
1274
+
1275
+ function samePath(firstPath, secondPath) {
1276
+ return path.resolve(firstPath) === path.resolve(secondPath);
1277
+ }
1278
+
1150
1279
  function shouldIgnoreMarkdownDiscoverEntry(name) {
1151
1280
  const basename = String(name || '');
1152
1281
  const lowerName = basename.toLowerCase();
@@ -1,6 +1,17 @@
1
1
  <section class="shell not-found">
2
- <p class="eyebrow">404</p>
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
- <a class="button-link" href="/">Return home</a>
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>