@zeropress/build-pages 0.5.6 → 0.6.1

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/dist/prebuild.js CHANGED
@@ -3534,7 +3534,12 @@ var skipUntitledMarkdown = readBooleanEnv("ZEROPRESS_SKIP_UNTITLED_MARKDOWN");
3534
3534
  var copyMarkdownSource = readBooleanEnv("ZEROPRESS_COPY_MARKDOWN_SOURCE", true);
3535
3535
  var FRONT_PAGE_TYPES = /* @__PURE__ */ new Set(["theme_index", "markdown", "html"]);
3536
3536
  var BUILD_PAGES_CONFIG_SCHEMA_URL = "https://zeropress.dev/schemas/zeropress-build-pages.config.v0.1.schema.json";
3537
- var PREVIEW_DATA_SCHEMA_URL = "https://zeropress.dev/schemas/preview-data.v0.5.schema.json";
3537
+ var PREVIEW_DATA_SCHEMA_URL = "https://zeropress.dev/schemas/preview-data.v0.6.schema.json";
3538
+ var FRONT_MATTER_DATA_KEY_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*(?:-[a-zA-Z0-9_]+)*$/;
3539
+ var FRONT_MATTER_DATA_MAX_DEPTH = 4;
3540
+ var FRONT_MATTER_DATA_MAX_KEYS = 64;
3541
+ var FRONT_MATTER_DATA_MAX_ARRAY_LENGTH = 256;
3542
+ var FRONT_MATTER_DISCOVERABILITY_VALUES = /* @__PURE__ */ new Set(["default", "noindex", "delist"]);
3538
3543
  var PrebuildMarkdownError = class extends Error {
3539
3544
  constructor(sourcePath, reason, expected = "", code = "invalid_markdown") {
3540
3545
  super(reason);
@@ -3608,6 +3613,8 @@ async function main() {
3608
3613
  ...frontMatter.meta,
3609
3614
  ...copyMarkdownSource ? { source_markdown_url: buildSourceMarkdownUrl(sourcePath) } : {}
3610
3615
  },
3616
+ ...frontMatter.data !== void 0 ? { data: frontMatter.data } : {},
3617
+ ...frontMatter.discoverability !== "default" ? { discoverability: frontMatter.discoverability } : {},
3611
3618
  content: rewriteMarkdownLinks(bodyMarkdown, sourcePath, routeBySourcePath),
3612
3619
  document_type: "markdown",
3613
3620
  excerpt: frontMatter.description || extractExcerpt(bodyMarkdown, title),
@@ -3621,7 +3628,7 @@ async function main() {
3621
3628
  const customHtml = await buildCustomHtmlData(customHtmlConfig);
3622
3629
  const previewData = {
3623
3630
  $schema: PREVIEW_DATA_SCHEMA_URL,
3624
- version: "0.5",
3631
+ version: "0.6",
3625
3632
  generator: "zeropress-build-pages",
3626
3633
  generated_at: (/* @__PURE__ */ new Date()).toISOString(),
3627
3634
  site,
@@ -3719,18 +3726,20 @@ function buildSiteData(config, frontPage) {
3719
3726
  title: configuredSite.title,
3720
3727
  description: configuredSite.description,
3721
3728
  url: configuredSite.url,
3722
- mediaBaseUrl: "",
3729
+ media_base_url: "",
3723
3730
  locale: "en-US",
3724
- postsPerPage: 10,
3725
- dateFormat: "YYYY-MM-DD",
3726
- timeFormat: "HH:mm",
3731
+ posts_per_page: 10,
3732
+ datetime_display: "static",
3733
+ date_style: "medium",
3734
+ time_style: "none",
3727
3735
  timezone: "UTC",
3728
3736
  permalinks: defaultPermalinks(),
3729
3737
  front_page: frontPage,
3730
3738
  post_index: {
3731
3739
  enabled: false
3732
3740
  },
3733
- disallowComments: true,
3741
+ disallow_comments: true,
3742
+ expose_generator: configuredSite.expose_generator !== false,
3734
3743
  indexing: configuredSite.indexing !== false
3735
3744
  };
3736
3745
  if (configuredSite.footer) {
@@ -3759,11 +3768,12 @@ function normalizeSiteConfig(value) {
3759
3768
  );
3760
3769
  }
3761
3770
  const configuredSite = isPlainObject(value) ? value : {};
3762
- assertKnownConfigKeys(configuredSite, ["title", "description", "url", "indexing", "footer"], "site");
3771
+ assertKnownConfigKeys(configuredSite, ["title", "description", "url", "expose_generator", "indexing", "footer"], "site");
3763
3772
  const site = {
3764
3773
  title: readConfigString(configuredSite.title, "Documentation"),
3765
3774
  description: readConfigString(configuredSite.description, "A documentation site."),
3766
3775
  url: readEnv("ZEROPRESS_SITE_URL", readConfigString(configuredSite.url, "")),
3776
+ expose_generator: readConfigBoolean(configuredSite.expose_generator, true, "site.expose_generator"),
3767
3777
  indexing: readConfigBoolean(configuredSite.indexing, true, "site.indexing")
3768
3778
  };
3769
3779
  const footer = normalizeFooter(configuredSite.footer);
@@ -3785,19 +3795,11 @@ function normalizeFooter(value) {
3785
3795
  if (copyrightText) {
3786
3796
  footer.copyright_text = copyrightText;
3787
3797
  }
3788
- if (value.attribution !== void 0 && !isPlainObject(value.attribution)) {
3789
- throw new PrebuildConfigError("site.footer.attribution must be an object.");
3790
- }
3791
- if (isPlainObject(value.attribution)) {
3792
- assertKnownConfigKeys(value.attribution, ["enabled"], "site.footer.attribution");
3793
- if (value.attribution.enabled !== void 0 && typeof value.attribution.enabled !== "boolean") {
3794
- throw new PrebuildConfigError("site.footer.attribution.enabled must be a boolean when provided.");
3798
+ if (value.attribution !== void 0) {
3799
+ if (typeof value.attribution !== "boolean") {
3800
+ throw new PrebuildConfigError("site.footer.attribution must be a boolean when provided.");
3795
3801
  }
3796
- }
3797
- if (isPlainObject(value.attribution) && typeof value.attribution.enabled === "boolean") {
3798
- footer.attribution = {
3799
- enabled: value.attribution.enabled
3800
- };
3802
+ footer.attribution = value.attribution;
3801
3803
  }
3802
3804
  return Object.keys(footer).length ? footer : void 0;
3803
3805
  }
@@ -4143,9 +4145,23 @@ function normalizeMenuItem(item, pathLabel) {
4143
4145
  url,
4144
4146
  type: readConfigString(item.type, "custom"),
4145
4147
  target: readConfigString(item.target, "_self"),
4148
+ ...item.meta !== void 0 ? { meta: normalizeMenuItemMeta(item.meta, `${pathLabel}.meta`) } : {},
4146
4149
  children: Array.isArray(item.children) ? item.children.map((child, index) => normalizeMenuItem(child, `${pathLabel}.children[${index}]`)) : []
4147
4150
  };
4148
4151
  }
4152
+ function normalizeMenuItemMeta(value, pathLabel) {
4153
+ if (!isPlainObject(value)) {
4154
+ throw new PrebuildConfigError(`${pathLabel} must be an object when provided.`);
4155
+ }
4156
+ const meta = {};
4157
+ for (const [key, metaValue] of Object.entries(value)) {
4158
+ if (!isPreviewMetaValue(metaValue)) {
4159
+ throw new PrebuildConfigError(`${pathLabel}.${key} must be a string, number, boolean, or null.`);
4160
+ }
4161
+ meta[key] = metaValue;
4162
+ }
4163
+ return meta;
4164
+ }
4149
4165
  function defaultMenus() {
4150
4166
  return {
4151
4167
  primary: {
@@ -4262,7 +4278,9 @@ function normalizePublishedFrontMatter(frontMatter, sourcePath) {
4262
4278
  title: normalizeFrontMatterTitle(frontMatter.title, sourcePath),
4263
4279
  description: normalizeFrontMatterDescription(frontMatter.description, sourcePath),
4264
4280
  path: normalizeFrontMatterRoutePath(frontMatter.path, sourcePath),
4265
- meta: normalizeFrontMatterMeta(frontMatter.meta, sourcePath)
4281
+ discoverability: normalizeFrontMatterDiscoverability(frontMatter.discoverability, sourcePath),
4282
+ meta: normalizeFrontMatterMeta(frontMatter.meta, sourcePath),
4283
+ data: normalizeFrontMatterData(frontMatter.data, sourcePath)
4266
4284
  };
4267
4285
  }
4268
4286
  function normalizeFrontMatterTitle(value, sourcePath) {
@@ -4305,11 +4323,24 @@ function normalizeFrontMatterRoutePath(value, sourcePath) {
4305
4323
  throw new PrebuildMarkdownError(
4306
4324
  sourcePath,
4307
4325
  "front matter path must be a safe generated route path.",
4308
- " path: guides/install\n path: spec/preview-data-v0.5"
4326
+ " path: guides/install\n path: spec/preview-data-v0.6"
4309
4327
  );
4310
4328
  }
4311
4329
  return routePath;
4312
4330
  }
4331
+ function normalizeFrontMatterDiscoverability(value, sourcePath) {
4332
+ if (value === void 0) {
4333
+ return "default";
4334
+ }
4335
+ if (typeof value === "string" && FRONT_MATTER_DISCOVERABILITY_VALUES.has(value)) {
4336
+ return value;
4337
+ }
4338
+ throw new PrebuildMarkdownError(
4339
+ sourcePath,
4340
+ `front matter discoverability must be one of: ${Array.from(FRONT_MATTER_DISCOVERABILITY_VALUES).join(", ")}.`,
4341
+ " discoverability: default\n discoverability: noindex\n discoverability: delist"
4342
+ );
4343
+ }
4313
4344
  function isSafeRoutePathSegment(segment) {
4314
4345
  return /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/.test(segment) && !segment.includes("..");
4315
4346
  }
@@ -4336,7 +4367,88 @@ function normalizeFrontMatterMeta(value, sourcePath) {
4336
4367
  return meta;
4337
4368
  }
4338
4369
  function isPreviewMetaValue(value) {
4339
- return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
4370
+ return value === null || typeof value === "string" || typeof value === "number" && Number.isFinite(value) || typeof value === "boolean";
4371
+ }
4372
+ function normalizeFrontMatterData(value, sourcePath) {
4373
+ if (value === void 0) {
4374
+ return void 0;
4375
+ }
4376
+ if (!isPlainObject(value)) {
4377
+ throw new PrebuildMarkdownError(
4378
+ sourcePath,
4379
+ "front matter data must be an object when provided."
4380
+ );
4381
+ }
4382
+ validateFrontMatterDataObject(value, sourcePath, "data", 0);
4383
+ return value;
4384
+ }
4385
+ function validateFrontMatterDataValue(value, sourcePath, pathLabel, depth) {
4386
+ if (value === null || typeof value === "string" || typeof value === "boolean") {
4387
+ return;
4388
+ }
4389
+ if (typeof value === "number") {
4390
+ if (!Number.isFinite(value)) {
4391
+ throw new PrebuildMarkdownError(
4392
+ sourcePath,
4393
+ `front matter ${pathLabel} must be a finite number.`
4394
+ );
4395
+ }
4396
+ return;
4397
+ }
4398
+ if (Array.isArray(value)) {
4399
+ validateFrontMatterDataArray(value, sourcePath, pathLabel, depth);
4400
+ return;
4401
+ }
4402
+ if (isPlainObject(value)) {
4403
+ validateFrontMatterDataObject(value, sourcePath, pathLabel, depth);
4404
+ return;
4405
+ }
4406
+ throw new PrebuildMarkdownError(
4407
+ sourcePath,
4408
+ `front matter ${pathLabel} must be JSON-safe structured data.`
4409
+ );
4410
+ }
4411
+ function validateFrontMatterDataObject(object, sourcePath, pathLabel, depth) {
4412
+ if (depth > FRONT_MATTER_DATA_MAX_DEPTH) {
4413
+ throw new PrebuildMarkdownError(
4414
+ sourcePath,
4415
+ `front matter ${pathLabel} nesting must not exceed ${FRONT_MATTER_DATA_MAX_DEPTH} container levels.`
4416
+ );
4417
+ }
4418
+ const entries = Object.entries(object);
4419
+ if (entries.length > FRONT_MATTER_DATA_MAX_KEYS) {
4420
+ throw new PrebuildMarkdownError(
4421
+ sourcePath,
4422
+ `front matter ${pathLabel} must not contain more than ${FRONT_MATTER_DATA_MAX_KEYS} keys.`
4423
+ );
4424
+ }
4425
+ for (const [key, dataValue] of entries) {
4426
+ const childLabel = `${pathLabel}.${key}`;
4427
+ if (!FRONT_MATTER_DATA_KEY_PATTERN.test(key)) {
4428
+ throw new PrebuildMarkdownError(
4429
+ sourcePath,
4430
+ `front matter ${childLabel} uses an invalid key.`
4431
+ );
4432
+ }
4433
+ validateFrontMatterDataValue(dataValue, sourcePath, childLabel, depth + 1);
4434
+ }
4435
+ }
4436
+ function validateFrontMatterDataArray(array, sourcePath, pathLabel, depth) {
4437
+ if (depth > FRONT_MATTER_DATA_MAX_DEPTH) {
4438
+ throw new PrebuildMarkdownError(
4439
+ sourcePath,
4440
+ `front matter ${pathLabel} nesting must not exceed ${FRONT_MATTER_DATA_MAX_DEPTH} container levels.`
4441
+ );
4442
+ }
4443
+ if (array.length > FRONT_MATTER_DATA_MAX_ARRAY_LENGTH) {
4444
+ throw new PrebuildMarkdownError(
4445
+ sourcePath,
4446
+ `front matter ${pathLabel} must not contain more than ${FRONT_MATTER_DATA_MAX_ARRAY_LENGTH} items.`
4447
+ );
4448
+ }
4449
+ array.forEach((dataValue, index) => {
4450
+ validateFrontMatterDataValue(dataValue, sourcePath, `${pathLabel}[${index}]`, depth + 1);
4451
+ });
4340
4452
  }
4341
4453
  function formatFrontMatterValue(value) {
4342
4454
  if (typeof value === "string") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeropress/build-pages",
3
- "version": "0.5.6",
3
+ "version": "0.6.1",
4
4
  "description": "ZeroPress Markdown build action and CLI for static hosting",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -40,7 +40,7 @@
40
40
  "node": ">=18.18.0"
41
41
  },
42
42
  "dependencies": {
43
- "@zeropress/build": "0.5.2",
43
+ "@zeropress/build": "0.6.1",
44
44
  "gray-matter": "4.0.3"
45
45
  },
46
46
  "devDependencies": {
@@ -50,6 +50,12 @@
50
50
  "description": "Canonical site URL. Use an empty string or omit this field for local builds without canonical output.",
51
51
  "markdownDescription": "Canonical site URL. Use an empty string or omit this field for local builds without canonical output."
52
52
  },
53
+ "expose_generator": {
54
+ "type": "boolean",
55
+ "default": true,
56
+ "description": "Whether generated HTML should expose the ZeroPress generator meta tag. Set false for white-label sites.",
57
+ "markdownDescription": "Whether generated HTML should expose `<meta name=\"generator\" content=\"ZeroPress\">`. Set `false` for white-label sites."
58
+ },
53
59
  "indexing": {
54
60
  "type": "boolean",
55
61
  "default": true,
@@ -74,17 +80,6 @@
74
80
  "markdownDescription": "Footer copyright or legal text. ZeroPress does not add a copyright symbol automatically."
75
81
  },
76
82
  "attribution": {
77
- "$ref": "#/$defs/siteFooterAttribution"
78
- }
79
- }
80
- },
81
- "siteFooterAttribution": {
82
- "type": "object",
83
- "additionalProperties": false,
84
- "description": "Optional ZeroPress attribution display policy for themes.",
85
- "markdownDescription": "Optional ZeroPress attribution display policy for themes.",
86
- "properties": {
87
- "enabled": {
88
83
  "type": "boolean",
89
84
  "description": "When false, bundled themes hide the Published with ZeroPress attribution. Missing or true means attribution may be shown.",
90
85
  "markdownDescription": "When `false`, bundled themes hide the Published with ZeroPress attribution. Missing or `true` means attribution may be shown."
@@ -227,6 +222,12 @@
227
222
  "$ref": "#/$defs/menu"
228
223
  }
229
224
  },
225
+ "previewMeta": {
226
+ "type": "object",
227
+ "additionalProperties": {
228
+ "type": ["string", "number", "boolean", "null"]
229
+ }
230
+ },
230
231
  "menu": {
231
232
  "type": "object",
232
233
  "additionalProperties": false,
@@ -248,6 +249,8 @@
248
249
  "type": "object",
249
250
  "additionalProperties": false,
250
251
  "required": ["title", "url"],
252
+ "description": "Menu item copied into generated preview-data.",
253
+ "markdownDescription": "Menu item copied into generated preview-data.",
251
254
  "properties": {
252
255
  "title": {
253
256
  "type": "string",
@@ -267,6 +270,9 @@
267
270
  "enum": ["_self", "_blank"],
268
271
  "default": "_self"
269
272
  },
273
+ "meta": {
274
+ "$ref": "#/$defs/previewMeta"
275
+ },
270
276
  "children": {
271
277
  "type": "array",
272
278
  "items": {
@@ -285,6 +291,7 @@
285
291
  "title": "ZeroPress Public Docs",
286
292
  "description": "Public documentation.",
287
293
  "url": "https://zeropress.dev",
294
+ "expose_generator": true,
288
295
  "indexing": true
289
296
  },
290
297
  "front_page": {
@@ -303,7 +310,14 @@
303
310
  "name": "Primary Menu",
304
311
  "items": [
305
312
  { "title": "Home", "url": "/" },
306
- { "title": "Docs", "url": "/docs/" }
313
+ {
314
+ "title": "Docs",
315
+ "url": "/docs/",
316
+ "meta": {
317
+ "icon": "book-open",
318
+ "badge": "New"
319
+ }
320
+ }
307
321
  ]
308
322
  }
309
323
  }
package/src/action.js CHANGED
@@ -21,7 +21,7 @@ try {
21
21
  }
22
22
 
23
23
  function input(name) {
24
- return process.env[`INPUT_${name.toUpperCase().replace(/-/g, '_')}`]?.trim() || '';
24
+ return process.env[`INPUT_${name.toUpperCase()}`]?.trim() || '';
25
25
  }
26
26
 
27
27
  function booleanInput(name, fallback) {
package/src/prebuild.js CHANGED
@@ -16,7 +16,12 @@ const skipUntitledMarkdown = readBooleanEnv('ZEROPRESS_SKIP_UNTITLED_MARKDOWN');
16
16
  const copyMarkdownSource = readBooleanEnv('ZEROPRESS_COPY_MARKDOWN_SOURCE', true);
17
17
  const FRONT_PAGE_TYPES = new Set(['theme_index', 'markdown', 'html']);
18
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.5.schema.json';
19
+ const PREVIEW_DATA_SCHEMA_URL = 'https://zeropress.dev/schemas/preview-data.v0.6.schema.json';
20
+ const FRONT_MATTER_DATA_KEY_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*(?:-[a-zA-Z0-9_]+)*$/;
21
+ const FRONT_MATTER_DATA_MAX_DEPTH = 4;
22
+ const FRONT_MATTER_DATA_MAX_KEYS = 64;
23
+ const FRONT_MATTER_DATA_MAX_ARRAY_LENGTH = 256;
24
+ const FRONT_MATTER_DISCOVERABILITY_VALUES = new Set(['default', 'noindex', 'delist']);
20
25
 
21
26
  class PrebuildMarkdownError extends Error {
22
27
  constructor(sourcePath, reason, expected = '', code = 'invalid_markdown') {
@@ -99,6 +104,8 @@ async function main() {
99
104
  ...frontMatter.meta,
100
105
  ...(copyMarkdownSource ? { source_markdown_url: buildSourceMarkdownUrl(sourcePath) } : {}),
101
106
  },
107
+ ...(frontMatter.data !== undefined ? { data: frontMatter.data } : {}),
108
+ ...(frontMatter.discoverability !== 'default' ? { discoverability: frontMatter.discoverability } : {}),
102
109
  content: rewriteMarkdownLinks(bodyMarkdown, sourcePath, routeBySourcePath),
103
110
  document_type: 'markdown',
104
111
  excerpt: frontMatter.description || extractExcerpt(bodyMarkdown, title),
@@ -115,7 +122,7 @@ async function main() {
115
122
 
116
123
  const previewData = {
117
124
  $schema: PREVIEW_DATA_SCHEMA_URL,
118
- version: '0.5',
125
+ version: '0.6',
119
126
  generator: 'zeropress-build-pages',
120
127
  generated_at: new Date().toISOString(),
121
128
  site,
@@ -222,18 +229,20 @@ function buildSiteData(config, frontPage) {
222
229
  title: configuredSite.title,
223
230
  description: configuredSite.description,
224
231
  url: configuredSite.url,
225
- mediaBaseUrl: '',
232
+ media_base_url: '',
226
233
  locale: 'en-US',
227
- postsPerPage: 10,
228
- dateFormat: 'YYYY-MM-DD',
229
- timeFormat: 'HH:mm',
234
+ posts_per_page: 10,
235
+ datetime_display: 'static',
236
+ date_style: 'medium',
237
+ time_style: 'none',
230
238
  timezone: 'UTC',
231
239
  permalinks: defaultPermalinks(),
232
240
  front_page: frontPage,
233
241
  post_index: {
234
242
  enabled: false,
235
243
  },
236
- disallowComments: true,
244
+ disallow_comments: true,
245
+ expose_generator: configuredSite.expose_generator !== false,
237
246
  indexing: configuredSite.indexing !== false,
238
247
  };
239
248
 
@@ -269,11 +278,12 @@ function normalizeSiteConfig(value) {
269
278
  }
270
279
 
271
280
  const configuredSite = isPlainObject(value) ? value : {};
272
- assertKnownConfigKeys(configuredSite, ['title', 'description', 'url', 'indexing', 'footer'], 'site');
281
+ assertKnownConfigKeys(configuredSite, ['title', 'description', 'url', 'expose_generator', 'indexing', 'footer'], 'site');
273
282
  const site = {
274
283
  title: readConfigString(configuredSite.title, 'Documentation'),
275
284
  description: readConfigString(configuredSite.description, 'A documentation site.'),
276
285
  url: readEnv('ZEROPRESS_SITE_URL', readConfigString(configuredSite.url, '')),
286
+ expose_generator: readConfigBoolean(configuredSite.expose_generator, true, 'site.expose_generator'),
277
287
  indexing: readConfigBoolean(configuredSite.indexing, true, 'site.indexing'),
278
288
  };
279
289
 
@@ -300,19 +310,11 @@ function normalizeFooter(value) {
300
310
  footer.copyright_text = copyrightText;
301
311
  }
302
312
 
303
- if (value.attribution !== undefined && !isPlainObject(value.attribution)) {
304
- throw new PrebuildConfigError('site.footer.attribution must be an object.');
305
- }
306
- if (isPlainObject(value.attribution)) {
307
- assertKnownConfigKeys(value.attribution, ['enabled'], 'site.footer.attribution');
308
- if (value.attribution.enabled !== undefined && typeof value.attribution.enabled !== 'boolean') {
309
- throw new PrebuildConfigError('site.footer.attribution.enabled must be a boolean when provided.');
313
+ if (value.attribution !== undefined) {
314
+ if (typeof value.attribution !== 'boolean') {
315
+ throw new PrebuildConfigError('site.footer.attribution must be a boolean when provided.');
310
316
  }
311
- }
312
- if (isPlainObject(value.attribution) && typeof value.attribution.enabled === 'boolean') {
313
- footer.attribution = {
314
- enabled: value.attribution.enabled,
315
- };
317
+ footer.attribution = value.attribution;
316
318
  }
317
319
 
318
320
  return Object.keys(footer).length ? footer : undefined;
@@ -712,12 +714,29 @@ function normalizeMenuItem(item, pathLabel) {
712
714
  url,
713
715
  type: readConfigString(item.type, 'custom'),
714
716
  target: readConfigString(item.target, '_self'),
717
+ ...(item.meta !== undefined ? { meta: normalizeMenuItemMeta(item.meta, `${pathLabel}.meta`) } : {}),
715
718
  children: Array.isArray(item.children)
716
719
  ? item.children.map((child, index) => normalizeMenuItem(child, `${pathLabel}.children[${index}]`))
717
720
  : [],
718
721
  };
719
722
  }
720
723
 
724
+ function normalizeMenuItemMeta(value, pathLabel) {
725
+ if (!isPlainObject(value)) {
726
+ throw new PrebuildConfigError(`${pathLabel} must be an object when provided.`);
727
+ }
728
+
729
+ const meta = {};
730
+ for (const [key, metaValue] of Object.entries(value)) {
731
+ if (!isPreviewMetaValue(metaValue)) {
732
+ throw new PrebuildConfigError(`${pathLabel}.${key} must be a string, number, boolean, or null.`);
733
+ }
734
+ meta[key] = metaValue;
735
+ }
736
+
737
+ return meta;
738
+ }
739
+
721
740
  function defaultMenus() {
722
741
  return {
723
742
  primary: {
@@ -844,7 +863,9 @@ function normalizePublishedFrontMatter(frontMatter, sourcePath) {
844
863
  title: normalizeFrontMatterTitle(frontMatter.title, sourcePath),
845
864
  description: normalizeFrontMatterDescription(frontMatter.description, sourcePath),
846
865
  path: normalizeFrontMatterRoutePath(frontMatter.path, sourcePath),
866
+ discoverability: normalizeFrontMatterDiscoverability(frontMatter.discoverability, sourcePath),
847
867
  meta: normalizeFrontMatterMeta(frontMatter.meta, sourcePath),
868
+ data: normalizeFrontMatterData(frontMatter.data, sourcePath),
848
869
  };
849
870
  }
850
871
 
@@ -900,13 +921,28 @@ function normalizeFrontMatterRoutePath(value, sourcePath) {
900
921
  throw new PrebuildMarkdownError(
901
922
  sourcePath,
902
923
  'front matter path must be a safe generated route path.',
903
- ' path: guides/install\n path: spec/preview-data-v0.5',
924
+ ' path: guides/install\n path: spec/preview-data-v0.6',
904
925
  );
905
926
  }
906
927
 
907
928
  return routePath;
908
929
  }
909
930
 
931
+ function normalizeFrontMatterDiscoverability(value, sourcePath) {
932
+ if (value === undefined) {
933
+ return 'default';
934
+ }
935
+ if (typeof value === 'string' && FRONT_MATTER_DISCOVERABILITY_VALUES.has(value)) {
936
+ return value;
937
+ }
938
+
939
+ throw new PrebuildMarkdownError(
940
+ sourcePath,
941
+ `front matter discoverability must be one of: ${Array.from(FRONT_MATTER_DISCOVERABILITY_VALUES).join(', ')}.`,
942
+ ' discoverability: default\n discoverability: noindex\n discoverability: delist',
943
+ );
944
+ }
945
+
910
946
  function isSafeRoutePathSegment(segment) {
911
947
  return (
912
948
  /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/.test(segment)
@@ -943,11 +979,102 @@ function isPreviewMetaValue(value) {
943
979
  return (
944
980
  value === null
945
981
  || typeof value === 'string'
946
- || typeof value === 'number'
982
+ || (typeof value === 'number' && Number.isFinite(value))
947
983
  || typeof value === 'boolean'
948
984
  );
949
985
  }
950
986
 
987
+ function normalizeFrontMatterData(value, sourcePath) {
988
+ if (value === undefined) {
989
+ return undefined;
990
+ }
991
+ if (!isPlainObject(value)) {
992
+ throw new PrebuildMarkdownError(
993
+ sourcePath,
994
+ 'front matter data must be an object when provided.',
995
+ );
996
+ }
997
+
998
+ validateFrontMatterDataObject(value, sourcePath, 'data', 0);
999
+ return value;
1000
+ }
1001
+
1002
+ function validateFrontMatterDataValue(value, sourcePath, pathLabel, depth) {
1003
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') {
1004
+ return;
1005
+ }
1006
+ if (typeof value === 'number') {
1007
+ if (!Number.isFinite(value)) {
1008
+ throw new PrebuildMarkdownError(
1009
+ sourcePath,
1010
+ `front matter ${pathLabel} must be a finite number.`,
1011
+ );
1012
+ }
1013
+ return;
1014
+ }
1015
+ if (Array.isArray(value)) {
1016
+ validateFrontMatterDataArray(value, sourcePath, pathLabel, depth);
1017
+ return;
1018
+ }
1019
+ if (isPlainObject(value)) {
1020
+ validateFrontMatterDataObject(value, sourcePath, pathLabel, depth);
1021
+ return;
1022
+ }
1023
+
1024
+ throw new PrebuildMarkdownError(
1025
+ sourcePath,
1026
+ `front matter ${pathLabel} must be JSON-safe structured data.`,
1027
+ );
1028
+ }
1029
+
1030
+ function validateFrontMatterDataObject(object, sourcePath, pathLabel, depth) {
1031
+ if (depth > FRONT_MATTER_DATA_MAX_DEPTH) {
1032
+ throw new PrebuildMarkdownError(
1033
+ sourcePath,
1034
+ `front matter ${pathLabel} nesting must not exceed ${FRONT_MATTER_DATA_MAX_DEPTH} container levels.`,
1035
+ );
1036
+ }
1037
+
1038
+ const entries = Object.entries(object);
1039
+ if (entries.length > FRONT_MATTER_DATA_MAX_KEYS) {
1040
+ throw new PrebuildMarkdownError(
1041
+ sourcePath,
1042
+ `front matter ${pathLabel} must not contain more than ${FRONT_MATTER_DATA_MAX_KEYS} keys.`,
1043
+ );
1044
+ }
1045
+
1046
+ for (const [key, dataValue] of entries) {
1047
+ const childLabel = `${pathLabel}.${key}`;
1048
+ if (!FRONT_MATTER_DATA_KEY_PATTERN.test(key)) {
1049
+ throw new PrebuildMarkdownError(
1050
+ sourcePath,
1051
+ `front matter ${childLabel} uses an invalid key.`,
1052
+ );
1053
+ }
1054
+ validateFrontMatterDataValue(dataValue, sourcePath, childLabel, depth + 1);
1055
+ }
1056
+ }
1057
+
1058
+ function validateFrontMatterDataArray(array, sourcePath, pathLabel, depth) {
1059
+ if (depth > FRONT_MATTER_DATA_MAX_DEPTH) {
1060
+ throw new PrebuildMarkdownError(
1061
+ sourcePath,
1062
+ `front matter ${pathLabel} nesting must not exceed ${FRONT_MATTER_DATA_MAX_DEPTH} container levels.`,
1063
+ );
1064
+ }
1065
+
1066
+ if (array.length > FRONT_MATTER_DATA_MAX_ARRAY_LENGTH) {
1067
+ throw new PrebuildMarkdownError(
1068
+ sourcePath,
1069
+ `front matter ${pathLabel} must not contain more than ${FRONT_MATTER_DATA_MAX_ARRAY_LENGTH} items.`,
1070
+ );
1071
+ }
1072
+
1073
+ array.forEach((dataValue, index) => {
1074
+ validateFrontMatterDataValue(dataValue, sourcePath, `${pathLabel}[${index}]`, depth + 1);
1075
+ });
1076
+ }
1077
+
951
1078
  function formatFrontMatterValue(value) {
952
1079
  if (typeof value === 'string') {
953
1080
  return `"${value}"`;
@@ -28,7 +28,7 @@
28
28
  {{#else_if site.title}}
29
29
  <p>{{site.title}}</p>
30
30
  {{/if}}
31
- {{#if site.footer.attribution.enabled}}
31
+ {{#if site.footer.attribution}}
32
32
  <p>Published with <a href="https://zeropress.app" target="_blank" rel="noreferrer noopener">ZeroPress</a>.</p>
33
33
  {{/if}}
34
34
  </div>
@@ -2,16 +2,16 @@
2
2
  "name": "ZeroPress Build Pages Docs",
3
3
  "namespace": "zeropress",
4
4
  "slug": "zeropress-docs",
5
- "version": "0.5.0",
5
+ "version": "0.6.0",
6
6
  "license": "MIT",
7
- "runtime": "0.5",
7
+ "runtime": "0.6",
8
8
  "description": "Bundled documentation theme for @zeropress/build-pages",
9
9
  "features": {
10
10
  "comments": false,
11
11
  "newsletter": false,
12
- "postIndex": false
12
+ "post_index": false
13
13
  },
14
- "menuSlots": {
14
+ "menu_slots": {
15
15
  "primary": {
16
16
  "title": "Primary navigation",
17
17
  "description": "Top-level documentation navigation"