@zeropress/build-pages 0.5.5 → 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,21 @@ 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,
3743
+ indexing: configuredSite.indexing !== false
3734
3744
  };
3735
3745
  if (configuredSite.footer) {
3736
3746
  site.footer = configuredSite.footer;
@@ -3758,11 +3768,13 @@ function normalizeSiteConfig(value) {
3758
3768
  );
3759
3769
  }
3760
3770
  const configuredSite = isPlainObject(value) ? value : {};
3761
- assertKnownConfigKeys(configuredSite, ["title", "description", "url", "footer"], "site");
3771
+ assertKnownConfigKeys(configuredSite, ["title", "description", "url", "expose_generator", "indexing", "footer"], "site");
3762
3772
  const site = {
3763
3773
  title: readConfigString(configuredSite.title, "Documentation"),
3764
3774
  description: readConfigString(configuredSite.description, "A documentation site."),
3765
- url: readEnv("ZEROPRESS_SITE_URL", readConfigString(configuredSite.url, ""))
3775
+ url: readEnv("ZEROPRESS_SITE_URL", readConfigString(configuredSite.url, "")),
3776
+ expose_generator: readConfigBoolean(configuredSite.expose_generator, true, "site.expose_generator"),
3777
+ indexing: readConfigBoolean(configuredSite.indexing, true, "site.indexing")
3766
3778
  };
3767
3779
  const footer = normalizeFooter(configuredSite.footer);
3768
3780
  if (footer) {
@@ -3783,22 +3795,23 @@ function normalizeFooter(value) {
3783
3795
  if (copyrightText) {
3784
3796
  footer.copyright_text = copyrightText;
3785
3797
  }
3786
- if (value.attribution !== void 0 && !isPlainObject(value.attribution)) {
3787
- throw new PrebuildConfigError("site.footer.attribution must be an object.");
3788
- }
3789
- if (isPlainObject(value.attribution)) {
3790
- assertKnownConfigKeys(value.attribution, ["enabled"], "site.footer.attribution");
3791
- if (value.attribution.enabled !== void 0 && typeof value.attribution.enabled !== "boolean") {
3792
- 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.");
3793
3801
  }
3794
- }
3795
- if (isPlainObject(value.attribution) && typeof value.attribution.enabled === "boolean") {
3796
- footer.attribution = {
3797
- enabled: value.attribution.enabled
3798
- };
3802
+ footer.attribution = value.attribution;
3799
3803
  }
3800
3804
  return Object.keys(footer).length ? footer : void 0;
3801
3805
  }
3806
+ function readConfigBoolean(value, fallback, pathName) {
3807
+ if (value === void 0) {
3808
+ return fallback;
3809
+ }
3810
+ if (typeof value !== "boolean") {
3811
+ throw new PrebuildConfigError(`${pathName} must be a boolean when provided.`);
3812
+ }
3813
+ return value;
3814
+ }
3802
3815
  async function buildFrontPageData(frontPageConfig, pageInputs, config) {
3803
3816
  if (frontPageConfig.type === "theme_index") {
3804
3817
  return {
@@ -4132,9 +4145,23 @@ function normalizeMenuItem(item, pathLabel) {
4132
4145
  url,
4133
4146
  type: readConfigString(item.type, "custom"),
4134
4147
  target: readConfigString(item.target, "_self"),
4148
+ ...item.meta !== void 0 ? { meta: normalizeMenuItemMeta(item.meta, `${pathLabel}.meta`) } : {},
4135
4149
  children: Array.isArray(item.children) ? item.children.map((child, index) => normalizeMenuItem(child, `${pathLabel}.children[${index}]`)) : []
4136
4150
  };
4137
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
+ }
4138
4165
  function defaultMenus() {
4139
4166
  return {
4140
4167
  primary: {
@@ -4251,7 +4278,9 @@ function normalizePublishedFrontMatter(frontMatter, sourcePath) {
4251
4278
  title: normalizeFrontMatterTitle(frontMatter.title, sourcePath),
4252
4279
  description: normalizeFrontMatterDescription(frontMatter.description, sourcePath),
4253
4280
  path: normalizeFrontMatterRoutePath(frontMatter.path, sourcePath),
4254
- meta: normalizeFrontMatterMeta(frontMatter.meta, sourcePath)
4281
+ discoverability: normalizeFrontMatterDiscoverability(frontMatter.discoverability, sourcePath),
4282
+ meta: normalizeFrontMatterMeta(frontMatter.meta, sourcePath),
4283
+ data: normalizeFrontMatterData(frontMatter.data, sourcePath)
4255
4284
  };
4256
4285
  }
4257
4286
  function normalizeFrontMatterTitle(value, sourcePath) {
@@ -4294,11 +4323,24 @@ function normalizeFrontMatterRoutePath(value, sourcePath) {
4294
4323
  throw new PrebuildMarkdownError(
4295
4324
  sourcePath,
4296
4325
  "front matter path must be a safe generated route path.",
4297
- " path: guides/install\n path: spec/preview-data-v0.5"
4326
+ " path: guides/install\n path: spec/preview-data-v0.6"
4298
4327
  );
4299
4328
  }
4300
4329
  return routePath;
4301
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
+ }
4302
4344
  function isSafeRoutePathSegment(segment) {
4303
4345
  return /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/.test(segment) && !segment.includes("..");
4304
4346
  }
@@ -4325,7 +4367,88 @@ function normalizeFrontMatterMeta(value, sourcePath) {
4325
4367
  return meta;
4326
4368
  }
4327
4369
  function isPreviewMetaValue(value) {
4328
- 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
+ });
4329
4452
  }
4330
4453
  function formatFrontMatterValue(value) {
4331
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.5",
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.1",
43
+ "@zeropress/build": "0.6.1",
44
44
  "gray-matter": "4.0.3"
45
45
  },
46
46
  "devDependencies": {
@@ -50,6 +50,18 @@
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
+ },
59
+ "indexing": {
60
+ "type": "boolean",
61
+ "default": true,
62
+ "description": "Fallback robots.txt indexing policy. Missing or true allows indexing; false writes a fallback robots.txt that disallows all agents unless the source directory provides robots.txt.",
63
+ "markdownDescription": "Fallback `robots.txt` indexing policy. Missing or `true` allows indexing; `false` writes a fallback `robots.txt` that disallows all agents unless the source directory provides `robots.txt`."
64
+ },
53
65
  "footer": {
54
66
  "$ref": "#/$defs/siteFooter"
55
67
  }
@@ -68,17 +80,6 @@
68
80
  "markdownDescription": "Footer copyright or legal text. ZeroPress does not add a copyright symbol automatically."
69
81
  },
70
82
  "attribution": {
71
- "$ref": "#/$defs/siteFooterAttribution"
72
- }
73
- }
74
- },
75
- "siteFooterAttribution": {
76
- "type": "object",
77
- "additionalProperties": false,
78
- "description": "Optional ZeroPress attribution display policy for themes.",
79
- "markdownDescription": "Optional ZeroPress attribution display policy for themes.",
80
- "properties": {
81
- "enabled": {
82
83
  "type": "boolean",
83
84
  "description": "When false, bundled themes hide the Published with ZeroPress attribution. Missing or true means attribution may be shown.",
84
85
  "markdownDescription": "When `false`, bundled themes hide the Published with ZeroPress attribution. Missing or `true` means attribution may be shown."
@@ -221,6 +222,12 @@
221
222
  "$ref": "#/$defs/menu"
222
223
  }
223
224
  },
225
+ "previewMeta": {
226
+ "type": "object",
227
+ "additionalProperties": {
228
+ "type": ["string", "number", "boolean", "null"]
229
+ }
230
+ },
224
231
  "menu": {
225
232
  "type": "object",
226
233
  "additionalProperties": false,
@@ -242,6 +249,8 @@
242
249
  "type": "object",
243
250
  "additionalProperties": false,
244
251
  "required": ["title", "url"],
252
+ "description": "Menu item copied into generated preview-data.",
253
+ "markdownDescription": "Menu item copied into generated preview-data.",
245
254
  "properties": {
246
255
  "title": {
247
256
  "type": "string",
@@ -261,6 +270,9 @@
261
270
  "enum": ["_self", "_blank"],
262
271
  "default": "_self"
263
272
  },
273
+ "meta": {
274
+ "$ref": "#/$defs/previewMeta"
275
+ },
264
276
  "children": {
265
277
  "type": "array",
266
278
  "items": {
@@ -273,12 +285,14 @@
273
285
  },
274
286
  "examples": [
275
287
  {
276
- "$schema": "../schemas/zeropress-build-pages.config.v0.1.schema.json",
288
+ "$schema": "https://zeropress.dev/schemas/zeropress-build-pages.config.v0.1.schema.json",
277
289
  "version": "0.1",
278
290
  "site": {
279
291
  "title": "ZeroPress Public Docs",
280
292
  "description": "Public documentation.",
281
- "url": "https://zeropress.dev"
293
+ "url": "https://zeropress.dev",
294
+ "expose_generator": true,
295
+ "indexing": true
282
296
  },
283
297
  "front_page": {
284
298
  "type": "markdown"
@@ -296,7 +310,14 @@
296
310
  "name": "Primary Menu",
297
311
  "items": [
298
312
  { "title": "Home", "url": "/" },
299
- { "title": "Docs", "url": "/docs/" }
313
+ {
314
+ "title": "Docs",
315
+ "url": "/docs/",
316
+ "meta": {
317
+ "icon": "book-open",
318
+ "badge": "New"
319
+ }
320
+ }
300
321
  ]
301
322
  }
302
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,21 @@ 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,
246
+ indexing: configuredSite.indexing !== false,
237
247
  };
238
248
 
239
249
  if (configuredSite.footer) {
@@ -268,11 +278,13 @@ function normalizeSiteConfig(value) {
268
278
  }
269
279
 
270
280
  const configuredSite = isPlainObject(value) ? value : {};
271
- assertKnownConfigKeys(configuredSite, ['title', 'description', 'url', 'footer'], 'site');
281
+ assertKnownConfigKeys(configuredSite, ['title', 'description', 'url', 'expose_generator', 'indexing', 'footer'], 'site');
272
282
  const site = {
273
283
  title: readConfigString(configuredSite.title, 'Documentation'),
274
284
  description: readConfigString(configuredSite.description, 'A documentation site.'),
275
285
  url: readEnv('ZEROPRESS_SITE_URL', readConfigString(configuredSite.url, '')),
286
+ expose_generator: readConfigBoolean(configuredSite.expose_generator, true, 'site.expose_generator'),
287
+ indexing: readConfigBoolean(configuredSite.indexing, true, 'site.indexing'),
276
288
  };
277
289
 
278
290
  const footer = normalizeFooter(configuredSite.footer);
@@ -298,24 +310,26 @@ function normalizeFooter(value) {
298
310
  footer.copyright_text = copyrightText;
299
311
  }
300
312
 
301
- if (value.attribution !== undefined && !isPlainObject(value.attribution)) {
302
- throw new PrebuildConfigError('site.footer.attribution must be an object.');
303
- }
304
- if (isPlainObject(value.attribution)) {
305
- assertKnownConfigKeys(value.attribution, ['enabled'], 'site.footer.attribution');
306
- if (value.attribution.enabled !== undefined && typeof value.attribution.enabled !== 'boolean') {
307
- 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.');
308
316
  }
309
- }
310
- if (isPlainObject(value.attribution) && typeof value.attribution.enabled === 'boolean') {
311
- footer.attribution = {
312
- enabled: value.attribution.enabled,
313
- };
317
+ footer.attribution = value.attribution;
314
318
  }
315
319
 
316
320
  return Object.keys(footer).length ? footer : undefined;
317
321
  }
318
322
 
323
+ function readConfigBoolean(value, fallback, pathName) {
324
+ if (value === undefined) {
325
+ return fallback;
326
+ }
327
+ if (typeof value !== 'boolean') {
328
+ throw new PrebuildConfigError(`${pathName} must be a boolean when provided.`);
329
+ }
330
+ return value;
331
+ }
332
+
319
333
  async function buildFrontPageData(frontPageConfig, pageInputs, config) {
320
334
  if (frontPageConfig.type === 'theme_index') {
321
335
  return {
@@ -700,12 +714,29 @@ function normalizeMenuItem(item, pathLabel) {
700
714
  url,
701
715
  type: readConfigString(item.type, 'custom'),
702
716
  target: readConfigString(item.target, '_self'),
717
+ ...(item.meta !== undefined ? { meta: normalizeMenuItemMeta(item.meta, `${pathLabel}.meta`) } : {}),
703
718
  children: Array.isArray(item.children)
704
719
  ? item.children.map((child, index) => normalizeMenuItem(child, `${pathLabel}.children[${index}]`))
705
720
  : [],
706
721
  };
707
722
  }
708
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
+
709
740
  function defaultMenus() {
710
741
  return {
711
742
  primary: {
@@ -832,7 +863,9 @@ function normalizePublishedFrontMatter(frontMatter, sourcePath) {
832
863
  title: normalizeFrontMatterTitle(frontMatter.title, sourcePath),
833
864
  description: normalizeFrontMatterDescription(frontMatter.description, sourcePath),
834
865
  path: normalizeFrontMatterRoutePath(frontMatter.path, sourcePath),
866
+ discoverability: normalizeFrontMatterDiscoverability(frontMatter.discoverability, sourcePath),
835
867
  meta: normalizeFrontMatterMeta(frontMatter.meta, sourcePath),
868
+ data: normalizeFrontMatterData(frontMatter.data, sourcePath),
836
869
  };
837
870
  }
838
871
 
@@ -888,13 +921,28 @@ function normalizeFrontMatterRoutePath(value, sourcePath) {
888
921
  throw new PrebuildMarkdownError(
889
922
  sourcePath,
890
923
  'front matter path must be a safe generated route path.',
891
- ' path: guides/install\n path: spec/preview-data-v0.5',
924
+ ' path: guides/install\n path: spec/preview-data-v0.6',
892
925
  );
893
926
  }
894
927
 
895
928
  return routePath;
896
929
  }
897
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
+
898
946
  function isSafeRoutePathSegment(segment) {
899
947
  return (
900
948
  /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/.test(segment)
@@ -931,11 +979,102 @@ function isPreviewMetaValue(value) {
931
979
  return (
932
980
  value === null
933
981
  || typeof value === 'string'
934
- || typeof value === 'number'
982
+ || (typeof value === 'number' && Number.isFinite(value))
935
983
  || typeof value === 'boolean'
936
984
  );
937
985
  }
938
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
+
939
1078
  function formatFrontMatterValue(value) {
940
1079
  if (typeof value === 'string') {
941
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>