@zeropress/build-pages 0.5.1 → 0.5.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/src/prebuild.js CHANGED
@@ -1,17 +1,21 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
+ import matter from 'gray-matter';
4
5
 
5
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
7
  const rootDir = process.cwd();
7
- const sourceDir = resolveEnvPath(['ZEROPRESS_BUILD_PAGES_SOURCE', 'ZEROPRESS_PUBLIC_DIR'], '.');
8
+ const sourceDir = resolveEnvPath(['ZEROPRESS_BUILD_PAGES_SOURCE'], 'docs');
8
9
  const defaultConfigPath = path.join(sourceDir, '.zeropress', 'config.json');
9
10
  const configPath = resolveOptionalEnvPath(['ZEROPRESS_BUILD_PAGES_CONFIG'], defaultConfigPath);
10
11
  const outDir = path.join(rootDir, '.zeropress');
12
+ const buildPagesConfigPath = path.join(outDir, 'build-pages-config.json');
11
13
  const previewDataPath = path.join(outDir, 'preview-data.json');
12
- const prebuildReportPath = path.join(outDir, 'prebuild-report.json');
14
+ const buildReportPath = path.join(outDir, 'build-report.json');
13
15
  const skipUntitledMarkdown = readBooleanEnv('ZEROPRESS_SKIP_UNTITLED_MARKDOWN');
14
16
  const FRONT_PAGE_TYPES = new Set(['theme_index', 'markdown', 'html']);
17
+ const BUILD_PAGES_CONFIG_SCHEMA_URL = 'https://zeropress.dev/schemas/zeropress-build-pages.config.v0.1.schema.json';
18
+ const PREVIEW_DATA_SCHEMA_URL = 'https://zeropress.dev/schemas/preview-data.v0.5.schema.json';
15
19
 
16
20
  class PrebuildMarkdownError extends Error {
17
21
  constructor(sourcePath, reason, expected = '', code = 'invalid_markdown') {
@@ -41,23 +45,43 @@ async function main() {
41
45
  normalizeFrontPageConfig(config.front_page),
42
46
  config.front_page,
43
47
  );
48
+ const menus = normalizeMenus(config.menus);
49
+ const customHtmlConfig = normalizeCustomHtmlConfig(config.custom_html);
50
+ const resolvedConfig = buildResolvedConfig(config, {
51
+ frontPageConfig,
52
+ menus,
53
+ customHtmlConfig,
54
+ });
44
55
  const sourceFiles = await listMarkdownFiles(sourceDir);
45
56
  const skippedMarkdown = [];
46
57
  const pageInputs = [];
47
58
 
48
59
  for (const sourcePath of sourceFiles) {
49
60
  const rawMarkdown = await fs.readFile(sourcePath, 'utf8');
50
- const title = extractTitleOrSkip(rawMarkdown, sourcePath, skippedMarkdown);
61
+ const parsedMarkdown = parseMarkdownSource(rawMarkdown, sourcePath);
62
+ const frontMatterStatus = readFrontMatterStatus(parsedMarkdown.frontMatter.status, sourcePath);
63
+ if (frontMatterStatus !== 'published') {
64
+ recordSkippedMarkdown(skippedMarkdown, sourcePath, frontMatterStatus.reason);
65
+ if (frontMatterStatus.warning) {
66
+ console.warn(formatSkippedMarkdownWarning(sourcePath, frontMatterStatus.reason, frontMatterStatus.expected));
67
+ }
68
+ continue;
69
+ }
70
+
71
+ const frontMatter = normalizePublishedFrontMatter(parsedMarkdown.frontMatter, sourcePath);
72
+ const title = extractTitleOrSkip(parsedMarkdown.bodyMarkdown, sourcePath, skippedMarkdown, frontMatter.title);
51
73
  if (!title) {
52
74
  continue;
53
75
  }
54
76
 
55
77
  pageInputs.push({
56
78
  sourcePath,
57
- rawMarkdown,
79
+ bodyMarkdown: parsedMarkdown.bodyMarkdown,
80
+ frontMatter,
58
81
  title,
59
82
  route: buildPageRoute(sourcePath, {
60
83
  allowRootIndex: shouldAllowRootMarkdownIndex(frontPageConfig),
84
+ routePath: frontMatter.path,
61
85
  }),
62
86
  });
63
87
  }
@@ -66,29 +90,30 @@ async function main() {
66
90
  pageInputs.map(({ sourcePath, route }) => [sourcePath, route]),
67
91
  );
68
92
 
69
- const pages = pageInputs.map(({ sourcePath, rawMarkdown, title, route }) => ({
93
+ const pages = pageInputs.map(({ sourcePath, bodyMarkdown, frontMatter, title, route }) => ({
70
94
  title,
71
95
  slug: route.slug,
72
96
  path: route.path,
73
97
  meta: {
98
+ ...frontMatter.meta,
74
99
  source_markdown_url: buildSourceMarkdownUrl(sourcePath),
75
100
  },
76
- content: rewriteMarkdownLinks(rawMarkdown, sourcePath, routeBySourcePath),
101
+ content: rewriteMarkdownLinks(bodyMarkdown, sourcePath, routeBySourcePath),
77
102
  document_type: 'markdown',
78
- excerpt: extractExcerpt(rawMarkdown, title),
103
+ excerpt: frontMatter.description || extractExcerpt(bodyMarkdown, title),
79
104
  status: 'published',
80
105
  }));
81
106
 
82
- const frontPageResult = await buildFrontPageData(frontPageConfig, pageInputs, config);
107
+ const frontPageResult = await buildFrontPageData(frontPageConfig, pageInputs, resolvedConfig);
83
108
  if (frontPageResult.page) {
84
109
  pages.push(frontPageResult.page);
85
110
  }
86
111
 
87
- const site = buildSiteData(config, frontPageResult.frontPage);
88
- const menus = normalizeMenus(config.menus);
89
- const customHtml = await buildCustomHtmlData(config.custom_html);
112
+ const site = buildSiteData(resolvedConfig, frontPageResult.frontPage);
113
+ const customHtml = await buildCustomHtmlData(customHtmlConfig);
90
114
 
91
115
  const previewData = {
116
+ $schema: PREVIEW_DATA_SCHEMA_URL,
92
117
  version: '0.5',
93
118
  generator: 'zeropress-build-pages',
94
119
  generated_at: new Date().toISOString(),
@@ -108,6 +133,7 @@ async function main() {
108
133
  }
109
134
 
110
135
  await fs.mkdir(outDir, { recursive: true });
136
+ await fs.writeFile(buildPagesConfigPath, `${JSON.stringify(resolvedConfig, null, 2)}\n`, 'utf8');
111
137
  await fs.writeFile(previewDataPath, `${JSON.stringify(previewData, null, 2)}\n`, 'utf8');
112
138
 
113
139
  const report = buildPrebuildReport({
@@ -119,7 +145,7 @@ async function main() {
119
145
  frontPage: frontPageResult.frontPage,
120
146
  customHtml,
121
147
  });
122
- await fs.writeFile(prebuildReportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
148
+ await fs.writeFile(buildReportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
123
149
 
124
150
  console.log(`Wrote ${path.relative(rootDir, previewDataPath)} with ${pages.length} pages`);
125
151
  printPrebuildSummary(report);
@@ -189,27 +215,66 @@ async function loadPrebuildConfig() {
189
215
  }
190
216
 
191
217
  function buildSiteData(config, frontPage) {
192
- const configuredSite = isPlainObject(config.site) ? config.site : {};
193
- const footer = normalizeFooter(configuredSite.footer);
218
+ const configuredSite = isPlainObject(config.site) ? config.site : normalizeSiteConfig(undefined);
194
219
 
195
220
  const site = {
196
- title: readConfigString(configuredSite.title, 'ZeroPress Site'),
197
- description: readConfigString(configuredSite.description, 'Documentation built with ZeroPress.'),
198
- url: readEnv('ZEROPRESS_SITE_URL', readConfigString(configuredSite.url, '')),
199
- mediaBaseUrl: readConfigString(configuredSite.mediaBaseUrl, ''),
200
- locale: readConfigString(configuredSite.locale, 'en-US'),
201
- postsPerPage: readConfigInteger(configuredSite.postsPerPage, 10),
202
- dateFormat: readConfigString(configuredSite.dateFormat, 'YYYY-MM-DD'),
203
- timeFormat: readConfigString(configuredSite.timeFormat, 'HH:mm'),
204
- timezone: readConfigString(configuredSite.timezone, 'UTC'),
205
- permalinks: normalizePermalinks(configuredSite.permalinks),
221
+ title: configuredSite.title,
222
+ description: configuredSite.description,
223
+ url: configuredSite.url,
224
+ mediaBaseUrl: '',
225
+ locale: 'en-US',
226
+ postsPerPage: 10,
227
+ dateFormat: 'YYYY-MM-DD',
228
+ timeFormat: 'HH:mm',
229
+ timezone: 'UTC',
230
+ permalinks: defaultPermalinks(),
206
231
  front_page: frontPage,
207
232
  post_index: {
208
233
  enabled: false,
209
234
  },
210
- disallowComments: configuredSite.disallowComments !== false,
235
+ disallowComments: true,
211
236
  };
212
237
 
238
+ if (configuredSite.footer) {
239
+ site.footer = configuredSite.footer;
240
+ }
241
+
242
+ return site;
243
+ }
244
+
245
+ function buildResolvedConfig(config, { frontPageConfig, menus, customHtmlConfig }) {
246
+ const resolvedConfig = {
247
+ $schema: BUILD_PAGES_CONFIG_SCHEMA_URL,
248
+ version: '0.1',
249
+ site: normalizeSiteConfig(config.site),
250
+ front_page: frontPageConfig,
251
+ menus,
252
+ };
253
+
254
+ if (customHtmlConfig) {
255
+ resolvedConfig.custom_html = customHtmlConfig;
256
+ }
257
+
258
+ return resolvedConfig;
259
+ }
260
+
261
+ function normalizeSiteConfig(value) {
262
+ if (value !== undefined && !isPlainObject(value)) {
263
+ throw new PrebuildConfigError(
264
+ 'site must be an object.',
265
+ ' "site": { "title": "My Docs", "description": "Project documentation" }',
266
+ );
267
+ }
268
+
269
+ const configuredSite = isPlainObject(value) ? value : {};
270
+ assertKnownConfigKeys(configuredSite, ['title', 'description', 'url', 'footer'], 'site');
271
+ const site = {
272
+ title: readConfigString(configuredSite.title, 'Documentation'),
273
+ description: readConfigString(configuredSite.description, 'A documentation site.'),
274
+ url: readEnv('ZEROPRESS_SITE_URL', readConfigString(configuredSite.url, '')),
275
+ };
276
+
277
+ const footer = normalizeFooter(configuredSite.footer);
213
278
  if (footer) {
214
279
  site.footer = footer;
215
280
  }
@@ -218,9 +283,13 @@ function buildSiteData(config, frontPage) {
218
283
  }
219
284
 
220
285
  function normalizeFooter(value) {
221
- if (!isPlainObject(value)) {
286
+ if (value === undefined) {
222
287
  return undefined;
223
288
  }
289
+ if (!isPlainObject(value)) {
290
+ throw new PrebuildConfigError('site.footer must be an object.');
291
+ }
292
+ assertKnownConfigKeys(value, ['copyright_text', 'attribution'], 'site.footer');
224
293
 
225
294
  const footer = {};
226
295
  const copyrightText = readConfigString(value.copyright_text, '');
@@ -228,6 +297,15 @@ function normalizeFooter(value) {
228
297
  footer.copyright_text = copyrightText;
229
298
  }
230
299
 
300
+ if (value.attribution !== undefined && !isPlainObject(value.attribution)) {
301
+ throw new PrebuildConfigError('site.footer.attribution must be an object.');
302
+ }
303
+ if (isPlainObject(value.attribution)) {
304
+ assertKnownConfigKeys(value.attribution, ['enabled'], 'site.footer.attribution');
305
+ if (value.attribution.enabled !== undefined && typeof value.attribution.enabled !== 'boolean') {
306
+ throw new PrebuildConfigError('site.footer.attribution.enabled must be a boolean when provided.');
307
+ }
308
+ }
231
309
  if (isPlainObject(value.attribution) && typeof value.attribution.enabled === 'boolean') {
232
310
  footer.attribution = {
233
311
  enabled: value.attribution.enabled,
@@ -390,7 +468,7 @@ function isZeropressHtmlFile(filePath) {
390
468
  return filePath.startsWith('.zeropress/') && filePath.toLowerCase().endsWith('.html');
391
469
  }
392
470
 
393
- async function buildCustomHtmlData(value) {
471
+ function normalizeCustomHtmlConfig(value) {
394
472
  if (value === undefined) {
395
473
  return undefined;
396
474
  }
@@ -408,18 +486,18 @@ async function buildCustomHtmlData(value) {
408
486
  );
409
487
  }
410
488
 
411
- const customHtml = {};
489
+ const customHtmlConfig = {};
412
490
  if (value.head_end !== undefined) {
413
- customHtml.head_end = await buildCustomHtmlSlotData(value.head_end, 'custom_html.head_end');
491
+ customHtmlConfig.head_end = normalizeCustomHtmlSlotConfig(value.head_end, 'custom_html.head_end');
414
492
  }
415
493
  if (value.body_end !== undefined) {
416
- customHtml.body_end = await buildCustomHtmlSlotData(value.body_end, 'custom_html.body_end');
494
+ customHtmlConfig.body_end = normalizeCustomHtmlSlotConfig(value.body_end, 'custom_html.body_end');
417
495
  }
418
496
 
419
- return customHtml;
497
+ return customHtmlConfig;
420
498
  }
421
499
 
422
- async function buildCustomHtmlSlotData(value, pathLabel) {
500
+ function normalizeCustomHtmlSlotConfig(value, pathLabel) {
423
501
  if (!isPlainObject(value)) {
424
502
  throw new PrebuildConfigError(`${pathLabel} must be an object.`);
425
503
  }
@@ -439,7 +517,29 @@ async function buildCustomHtmlSlotData(value, pathLabel) {
439
517
  );
440
518
  }
441
519
 
442
- const sourcePath = resolveConfiguredSourceFile(file, '.html', `${pathLabel}.file`);
520
+ return {
521
+ file,
522
+ };
523
+ }
524
+
525
+ async function buildCustomHtmlData(config) {
526
+ if (!config) {
527
+ return undefined;
528
+ }
529
+
530
+ const customHtml = {};
531
+ if (config.head_end) {
532
+ customHtml.head_end = await buildCustomHtmlSlotData(config.head_end, 'custom_html.head_end');
533
+ }
534
+ if (config.body_end) {
535
+ customHtml.body_end = await buildCustomHtmlSlotData(config.body_end, 'custom_html.body_end');
536
+ }
537
+
538
+ return customHtml;
539
+ }
540
+
541
+ async function buildCustomHtmlSlotData(slotConfig, pathLabel) {
542
+ const sourcePath = resolveConfiguredSourceFile(slotConfig.file, '.html', `${pathLabel}.file`);
443
543
  return {
444
544
  content: await readRequiredSourceFile(sourcePath, `${pathLabel}.file`),
445
545
  };
@@ -549,13 +649,13 @@ function isPathInside(parentPath, childPath) {
549
649
  return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
550
650
  }
551
651
 
552
- function normalizePermalinks(value) {
652
+ function defaultPermalinks() {
553
653
  return {
554
- output_style: readConfigString(value?.output_style, 'html-extension'),
555
- posts: readConfigString(value?.posts, '/posts/:slug/'),
556
- pages: readConfigString(value?.pages, '/:slug/'),
557
- categories: readConfigString(value?.categories, '/categories/:slug/'),
558
- tags: readConfigString(value?.tags, '/tags/:slug/'),
654
+ output_style: 'html-extension',
655
+ posts: '/posts/:slug/',
656
+ pages: '/:slug/',
657
+ categories: '/categories/:slug/',
658
+ tags: '/tags/:slug/',
559
659
  };
560
660
  }
561
661
 
@@ -629,8 +729,9 @@ function buildPrebuildReport({
629
729
  generated_at: new Date().toISOString(),
630
730
  source_dir: formatSourcePath(sourceDir),
631
731
  config_path: formatSourcePath(configPath),
732
+ build_pages_config_path: formatSourcePath(buildPagesConfigPath),
632
733
  preview_data_path: formatSourcePath(previewDataPath),
633
- report_path: formatSourcePath(prebuildReportPath),
734
+ report_path: formatSourcePath(buildReportPath),
634
735
  skip_untitled_markdown: skipUntitledMarkdown,
635
736
  markdown: {
636
737
  discovered: sourceFiles.length,
@@ -651,7 +752,7 @@ function buildPrebuildReport({
651
752
 
652
753
  function printPrebuildSummary(report) {
653
754
  const lines = [
654
- 'ZeroPress prebuild report',
755
+ 'ZeroPress build report',
655
756
  `- Public root: ${report.source_dir}`,
656
757
  `- Markdown discovered: ${report.markdown.discovered}`,
657
758
  `- Markdown pages generated: ${report.markdown.generated_pages}`,
@@ -659,6 +760,7 @@ function printPrebuildSummary(report) {
659
760
  `- Total preview pages: ${report.pages.total}`,
660
761
  `- Front page: ${formatFrontPageSummary(report.front_page)}`,
661
762
  `- Custom HTML slots: ${report.custom_html.length ? report.custom_html.join(', ') : 'none'}`,
763
+ `- Resolved config: ${report.build_pages_config_path}`,
662
764
  `- Report: ${report.report_path}`,
663
765
  ];
664
766
 
@@ -680,7 +782,171 @@ function formatFrontPageSummary(frontPageReport) {
680
782
  return `html ${config.file} -> / (${previewData.page_slug})`;
681
783
  }
682
784
 
683
- function extractTitleOrSkip(markdown, sourcePath, skippedMarkdown) {
785
+ function parseMarkdownSource(rawMarkdown, sourcePath) {
786
+ try {
787
+ const parsed = matter(rawMarkdown);
788
+ if (!isPlainObject(parsed.data)) {
789
+ throw new PrebuildMarkdownError(
790
+ sourcePath,
791
+ 'front matter must be a YAML object.',
792
+ );
793
+ }
794
+
795
+ return {
796
+ bodyMarkdown: parsed.content,
797
+ frontMatter: parsed.data,
798
+ };
799
+ } catch (error) {
800
+ if (error instanceof PrebuildMarkdownError) {
801
+ throw error;
802
+ }
803
+
804
+ throw new PrebuildMarkdownError(
805
+ sourcePath,
806
+ `invalid YAML front matter: ${error instanceof Error ? error.message : String(error)}`,
807
+ );
808
+ }
809
+ }
810
+
811
+ function readFrontMatterStatus(value, sourcePath) {
812
+ if (value === undefined || value === 'published') {
813
+ return 'published';
814
+ }
815
+ if (value === 'draft') {
816
+ return {
817
+ reason: 'front matter status is "draft".',
818
+ };
819
+ }
820
+
821
+ return {
822
+ reason: `unsupported front matter status ${formatFrontMatterValue(value)}.`,
823
+ expected: 'Expected status: published or draft.',
824
+ warning: true,
825
+ };
826
+ }
827
+
828
+ function normalizePublishedFrontMatter(frontMatter, sourcePath) {
829
+ return {
830
+ title: normalizeFrontMatterTitle(frontMatter.title, sourcePath),
831
+ description: normalizeFrontMatterDescription(frontMatter.description, sourcePath),
832
+ path: normalizeFrontMatterRoutePath(frontMatter.path, sourcePath),
833
+ meta: normalizeFrontMatterMeta(frontMatter.meta, sourcePath),
834
+ };
835
+ }
836
+
837
+ function normalizeFrontMatterTitle(value, sourcePath) {
838
+ if (value === undefined) {
839
+ return '';
840
+ }
841
+ if (typeof value !== 'string' || !value.trim()) {
842
+ throw new PrebuildMarkdownError(
843
+ sourcePath,
844
+ 'front matter title must be a non-empty string when provided.',
845
+ );
846
+ }
847
+
848
+ return value.trim();
849
+ }
850
+
851
+ function normalizeFrontMatterDescription(value, sourcePath) {
852
+ if (value === undefined) {
853
+ return '';
854
+ }
855
+ if (typeof value !== 'string') {
856
+ throw new PrebuildMarkdownError(
857
+ sourcePath,
858
+ 'front matter description must be a string when provided.',
859
+ );
860
+ }
861
+
862
+ return value.trim();
863
+ }
864
+
865
+ function normalizeFrontMatterRoutePath(value, sourcePath) {
866
+ if (value === undefined) {
867
+ return '';
868
+ }
869
+ if (typeof value !== 'string' || !value.trim()) {
870
+ throw new PrebuildMarkdownError(
871
+ sourcePath,
872
+ 'front matter path must be a non-empty string when provided.',
873
+ );
874
+ }
875
+
876
+ const routePath = value.trim();
877
+ const segments = routePath.split('/');
878
+ if (
879
+ routePath.startsWith('/')
880
+ || routePath.endsWith('/')
881
+ || routePath.includes('\\')
882
+ || routePath.includes('?')
883
+ || routePath.includes('#')
884
+ || segments.some((segment) => !isSafeRoutePathSegment(segment))
885
+ ) {
886
+ throw new PrebuildMarkdownError(
887
+ sourcePath,
888
+ 'front matter path must be a safe generated route path.',
889
+ ' path: guides/install\n path: spec/preview-data-v0.5',
890
+ );
891
+ }
892
+
893
+ return routePath;
894
+ }
895
+
896
+ function isSafeRoutePathSegment(segment) {
897
+ return (
898
+ /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/.test(segment)
899
+ && !segment.includes('..')
900
+ );
901
+ }
902
+
903
+ function normalizeFrontMatterMeta(value, sourcePath) {
904
+ if (value === undefined) {
905
+ return {};
906
+ }
907
+ if (!isPlainObject(value)) {
908
+ throw new PrebuildMarkdownError(
909
+ sourcePath,
910
+ 'front matter meta must be an object when provided.',
911
+ );
912
+ }
913
+
914
+ const meta = {};
915
+ for (const [key, metaValue] of Object.entries(value)) {
916
+ if (!isPreviewMetaValue(metaValue)) {
917
+ throw new PrebuildMarkdownError(
918
+ sourcePath,
919
+ `front matter meta.${key} must be a string, number, boolean, or null.`,
920
+ );
921
+ }
922
+ meta[key] = metaValue;
923
+ }
924
+
925
+ return meta;
926
+ }
927
+
928
+ function isPreviewMetaValue(value) {
929
+ return (
930
+ value === null
931
+ || typeof value === 'string'
932
+ || typeof value === 'number'
933
+ || typeof value === 'boolean'
934
+ );
935
+ }
936
+
937
+ function formatFrontMatterValue(value) {
938
+ if (typeof value === 'string') {
939
+ return `"${value}"`;
940
+ }
941
+ const serialized = JSON.stringify(value);
942
+ return serialized === undefined ? String(value) : serialized;
943
+ }
944
+
945
+ function extractTitleOrSkip(markdown, sourcePath, skippedMarkdown, frontMatterTitle = '') {
946
+ if (frontMatterTitle) {
947
+ return frontMatterTitle;
948
+ }
949
+
684
950
  try {
685
951
  return extractTitle(markdown, sourcePath);
686
952
  } catch (error) {
@@ -689,11 +955,8 @@ function extractTitleOrSkip(markdown, sourcePath, skippedMarkdown) {
689
955
  && error instanceof PrebuildMarkdownError
690
956
  && error.code === 'untitled_markdown'
691
957
  ) {
692
- console.warn(formatSkippedUntitledMarkdownWarning(error));
693
- skippedMarkdown.push({
694
- file: formatSourcePath(error.sourcePath),
695
- reason: error.reason,
696
- });
958
+ console.warn(formatSkippedMarkdownWarning(error.sourcePath, error.reason, '', 'Skipped untitled Markdown'));
959
+ recordSkippedMarkdown(skippedMarkdown, error.sourcePath, error.reason);
697
960
  return '';
698
961
  }
699
962
 
@@ -701,12 +964,23 @@ function extractTitleOrSkip(markdown, sourcePath, skippedMarkdown) {
701
964
  }
702
965
  }
703
966
 
704
- function formatSkippedUntitledMarkdownWarning(error) {
705
- return [
706
- `[zeropress-build-pages] Skipped untitled Markdown: ${formatSourcePath(error.sourcePath)}`,
707
- `Reason: ${error.reason}`,
708
- 'This file was not added to preview-data pages.',
709
- ].join('\n');
967
+ function recordSkippedMarkdown(skippedMarkdown, sourcePath, reason) {
968
+ skippedMarkdown.push({
969
+ file: formatSourcePath(sourcePath),
970
+ reason,
971
+ });
972
+ }
973
+
974
+ function formatSkippedMarkdownWarning(sourcePath, reason, expected = '', label = 'Skipped Markdown') {
975
+ const lines = [
976
+ `[zeropress-build-pages] ${label}: ${formatSourcePath(sourcePath)}`,
977
+ `Reason: ${reason}`,
978
+ ];
979
+ if (expected) {
980
+ lines.push(expected);
981
+ }
982
+ lines.push('This file was not added to preview-data pages.');
983
+ return lines.join('\n');
710
984
  }
711
985
 
712
986
  async function listMarkdownFiles(dir) {
@@ -780,6 +1054,17 @@ function buildHtmlPageRoute(sourcePath, options = {}) {
780
1054
  }
781
1055
 
782
1056
  function buildRoutePath(relativeSourcePath, sourcePath, options = {}) {
1057
+ if (options.routePath) {
1058
+ if (options.routePath === 'index' && !options.allowRootIndex) {
1059
+ throw new PrebuildMarkdownError(
1060
+ sourcePath,
1061
+ 'front matter path "index" is reserved for the front page.',
1062
+ ' path: docs/index\n path: guide',
1063
+ );
1064
+ }
1065
+ return options.routePath;
1066
+ }
1067
+
783
1068
  const extensionPattern = options.extensionPattern || /\.md$/i;
784
1069
  const withoutExtension = relativeSourcePath.replace(extensionPattern, '').toLowerCase();
785
1070
  const segments = withoutExtension