@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/dist/prebuild.js CHANGED
@@ -3519,28 +3519,32 @@ var require_gray_matter = __commonJS({
3519
3519
  // src/prebuild.js
3520
3520
  var import_gray_matter = __toESM(require_gray_matter(), 1);
3521
3521
  import fs from "node:fs/promises";
3522
+ import { execFile } from "node:child_process";
3522
3523
  import path from "node:path";
3524
+ import { promisify } from "node:util";
3523
3525
  import { fileURLToPath } from "node:url";
3524
3526
  var __dirname = path.dirname(fileURLToPath(import.meta.url));
3527
+ var execFileAsync = promisify(execFile);
3525
3528
  var rootDir = process.cwd();
3526
3529
  var sourceDir = resolveEnvPath(["ZEROPRESS_BUILD_PAGES_SOURCE"], "docs");
3527
3530
  var publicDir = resolveEnvPath(["ZEROPRESS_BUILD_PAGES_PUBLIC_DIR"], sourceDir);
3528
3531
  var defaultConfigPath = path.join(sourceDir, ".zeropress", "config.json");
3529
3532
  var configPath = resolveOptionalEnvPath(["ZEROPRESS_BUILD_PAGES_CONFIG"], defaultConfigPath);
3530
- var outDir = path.join(rootDir, ".zeropress");
3533
+ var outDir = path.join(rootDir, ".zeropress-build-page");
3531
3534
  var buildPagesConfigPath = path.join(outDir, "build-pages-config.json");
3532
3535
  var previewDataPath = path.join(outDir, "preview-data.json");
3533
3536
  var buildReportPath = path.join(outDir, "build-report.json");
3534
3537
  var skipUntitledMarkdown = readBooleanEnv("ZEROPRESS_SKIP_UNTITLED_MARKDOWN");
3535
3538
  var copyMarkdownSource = readBooleanEnv("ZEROPRESS_COPY_MARKDOWN_SOURCE", true);
3536
3539
  var FRONT_PAGE_TYPES = /* @__PURE__ */ new Set(["theme_index", "markdown", "html"]);
3537
- var BUILD_PAGES_CONFIG_SCHEMA_URL = "https://zeropress.dev/schemas/zeropress-build-pages.config.v0.1.schema.json";
3538
- var PREVIEW_DATA_SCHEMA_URL = "https://zeropress.dev/schemas/preview-data.v0.6.schema.json";
3540
+ var BUILD_PAGES_CONFIG_SCHEMA_URL = "https://schemas.zeropress.dev/build-pages-config/v0.1/schema.json";
3541
+ var PREVIEW_DATA_SCHEMA_URL = "https://schemas.zeropress.dev/preview-data/v0.6/schema.json";
3539
3542
  var FRONT_MATTER_DATA_KEY_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*(?:-[a-zA-Z0-9_]+)*$/;
3540
3543
  var FRONT_MATTER_DATA_MAX_DEPTH = 4;
3541
3544
  var FRONT_MATTER_DATA_MAX_KEYS = 64;
3542
3545
  var FRONT_MATTER_DATA_MAX_ARRAY_LENGTH = 256;
3543
3546
  var FRONT_MATTER_DISCOVERABILITY_VALUES = /* @__PURE__ */ new Set(["default", "noindex", "delist"]);
3547
+ var MARKDOWN_LAST_UPDATED_VALUES = /* @__PURE__ */ new Set(["none", "git"]);
3544
3548
  var markdownDiscoverExcludeRoots = buildMarkdownDiscoverExcludeRoots();
3545
3549
  var PrebuildMarkdownError = class extends Error {
3546
3550
  constructor(sourcePath, reason, expected = "", code = "invalid_markdown") {
@@ -3569,10 +3573,12 @@ async function main() {
3569
3573
  );
3570
3574
  const menus = normalizeMenus(config.menus);
3571
3575
  const customHtmlConfig = normalizeCustomHtmlConfig(config.custom_html);
3576
+ const markdownConfig = normalizeMarkdownConfig(config.markdown);
3572
3577
  const resolvedConfig = buildResolvedConfig(config, {
3573
3578
  frontPageConfig,
3574
3579
  menus,
3575
- customHtmlConfig
3580
+ customHtmlConfig,
3581
+ markdownConfig
3576
3582
  });
3577
3583
  const sourceFiles = await listMarkdownFiles(sourceDir);
3578
3584
  const skippedMarkdown = [];
@@ -3607,21 +3613,29 @@ async function main() {
3607
3613
  const routeBySourcePath = new Map(
3608
3614
  pageInputs.map(({ sourcePath, route }) => [sourcePath, route])
3609
3615
  );
3610
- const pages = pageInputs.map(({ sourcePath, bodyMarkdown, frontMatter, title, route }) => ({
3611
- title,
3612
- slug: route.slug,
3613
- path: route.path,
3614
- meta: {
3615
- ...frontMatter.meta,
3616
- ...copyMarkdownSource ? { source_markdown_url: buildSourceMarkdownUrl(sourcePath) } : {}
3617
- },
3618
- ...frontMatter.data !== void 0 ? { data: frontMatter.data } : {},
3619
- ...frontMatter.discoverability !== "default" ? { discoverability: frontMatter.discoverability } : {},
3620
- content: rewriteMarkdownLinks(bodyMarkdown, sourcePath, routeBySourcePath),
3621
- document_type: "markdown",
3622
- excerpt: frontMatter.description || extractExcerpt(bodyMarkdown, title),
3623
- status: "published"
3624
- }));
3616
+ const collections = normalizeCollections(config.collections, pageInputs, skippedMarkdown);
3617
+ if (Object.keys(collections).length > 0) {
3618
+ resolvedConfig.collections = collections;
3619
+ }
3620
+ const pages = [];
3621
+ for (const { sourcePath, bodyMarkdown, frontMatter, title, route } of pageInputs) {
3622
+ const meta = await buildPageMeta(sourcePath, frontMatter, markdownConfig);
3623
+ pages.push({
3624
+ title,
3625
+ slug: route.slug,
3626
+ path: route.path,
3627
+ meta: {
3628
+ ...meta,
3629
+ ...copyMarkdownSource ? { source_markdown_url: buildSourceMarkdownUrl(sourcePath) } : {}
3630
+ },
3631
+ ...frontMatter.data !== void 0 ? { data: frontMatter.data } : {},
3632
+ ...frontMatter.discoverability !== "default" ? { discoverability: frontMatter.discoverability } : {},
3633
+ content: rewriteMarkdownLinks(bodyMarkdown, sourcePath, routeBySourcePath),
3634
+ document_type: "markdown",
3635
+ excerpt: frontMatter.description || extractExcerpt(bodyMarkdown, title),
3636
+ status: "published"
3637
+ });
3638
+ }
3625
3639
  const frontPageResult = await buildFrontPageData(frontPageConfig, pageInputs, resolvedConfig);
3626
3640
  if (frontPageResult.page) {
3627
3641
  pages.push(frontPageResult.page);
@@ -3644,6 +3658,9 @@ async function main() {
3644
3658
  menus,
3645
3659
  widgets: {}
3646
3660
  };
3661
+ if (Object.keys(collections).length > 0) {
3662
+ previewData.collections = collections;
3663
+ }
3647
3664
  if (customHtml) {
3648
3665
  previewData.custom_html = customHtml;
3649
3666
  }
@@ -3729,7 +3746,7 @@ function buildSiteData(config, frontPage) {
3729
3746
  description: configuredSite.description,
3730
3747
  url: configuredSite.url,
3731
3748
  media_base_url: "",
3732
- locale: "en-US",
3749
+ locale: configuredSite.locale,
3733
3750
  posts_per_page: 10,
3734
3751
  datetime_display: "static",
3735
3752
  date_style: "medium",
@@ -3745,16 +3762,23 @@ function buildSiteData(config, frontPage) {
3745
3762
  search: configuredSite.search !== false,
3746
3763
  indexing: configuredSite.indexing !== false
3747
3764
  };
3765
+ if (configuredSite.logo) {
3766
+ site.logo = configuredSite.logo;
3767
+ }
3768
+ if (configuredSite.meta !== void 0) {
3769
+ site.meta = configuredSite.meta;
3770
+ }
3748
3771
  if (configuredSite.footer) {
3749
3772
  site.footer = configuredSite.footer;
3750
3773
  }
3751
3774
  return site;
3752
3775
  }
3753
- function buildResolvedConfig(config, { frontPageConfig, menus, customHtmlConfig }) {
3776
+ function buildResolvedConfig(config, { frontPageConfig, menus, customHtmlConfig, markdownConfig }) {
3754
3777
  const resolvedConfig = {
3755
3778
  $schema: BUILD_PAGES_CONFIG_SCHEMA_URL,
3756
3779
  version: "0.1",
3757
3780
  site: normalizeSiteConfig(config.site),
3781
+ markdown: markdownConfig,
3758
3782
  front_page: frontPageConfig,
3759
3783
  menus
3760
3784
  };
@@ -3763,6 +3787,23 @@ function buildResolvedConfig(config, { frontPageConfig, menus, customHtmlConfig
3763
3787
  }
3764
3788
  return resolvedConfig;
3765
3789
  }
3790
+ function normalizeMarkdownConfig(value) {
3791
+ if (value === void 0) {
3792
+ return {
3793
+ last_updated: "none"
3794
+ };
3795
+ }
3796
+ if (!isPlainObject(value)) {
3797
+ throw new PrebuildConfigError(
3798
+ "markdown must be an object.",
3799
+ ' "markdown": { "last_updated": "git" }'
3800
+ );
3801
+ }
3802
+ assertKnownConfigKeys(value, ["last_updated"], "markdown");
3803
+ return {
3804
+ last_updated: normalizeLastUpdatedPolicy(value.last_updated, "markdown.last_updated", PrebuildConfigError)
3805
+ };
3806
+ }
3766
3807
  function normalizeSiteConfig(value) {
3767
3808
  if (value !== void 0 && !isPlainObject(value)) {
3768
3809
  throw new PrebuildConfigError(
@@ -3771,21 +3812,83 @@ function normalizeSiteConfig(value) {
3771
3812
  );
3772
3813
  }
3773
3814
  const configuredSite = isPlainObject(value) ? value : {};
3774
- assertKnownConfigKeys(configuredSite, ["title", "description", "url", "expose_generator", "search", "indexing", "footer"], "site");
3815
+ assertKnownConfigKeys(configuredSite, ["title", "description", "url", "logo", "locale", "expose_generator", "search", "indexing", "footer", "meta"], "site");
3775
3816
  const site = {
3776
3817
  title: readConfigString(configuredSite.title, "Documentation"),
3777
3818
  description: readConfigString(configuredSite.description, "A documentation site."),
3778
3819
  url: readEnv("ZEROPRESS_SITE_URL", readConfigString(configuredSite.url, "")),
3820
+ locale: normalizeSiteLocale(configuredSite.locale),
3779
3821
  expose_generator: readConfigBoolean(configuredSite.expose_generator, true, "site.expose_generator"),
3780
3822
  search: readConfigBoolean(configuredSite.search, true, "site.search"),
3781
3823
  indexing: readConfigBoolean(configuredSite.indexing, true, "site.indexing")
3782
3824
  };
3825
+ const logo = normalizeSiteLogo(configuredSite.logo);
3826
+ if (logo) {
3827
+ site.logo = logo;
3828
+ }
3783
3829
  const footer = normalizeFooter(configuredSite.footer);
3784
3830
  if (footer) {
3785
3831
  site.footer = footer;
3786
3832
  }
3833
+ if (configuredSite.meta !== void 0) {
3834
+ site.meta = normalizeSiteMeta(configuredSite.meta, "site.meta");
3835
+ }
3787
3836
  return site;
3788
3837
  }
3838
+ function normalizeSiteLocale(value) {
3839
+ if (value === void 0) {
3840
+ return "en-US";
3841
+ }
3842
+ if (typeof value !== "string") {
3843
+ throw new PrebuildConfigError("site.locale must be a string when provided.");
3844
+ }
3845
+ const locale = value.trim();
3846
+ if (locale.length < 2) {
3847
+ throw new PrebuildConfigError('site.locale must be a non-empty locale string such as "en-US" or "ko-KR".');
3848
+ }
3849
+ return locale;
3850
+ }
3851
+ function normalizeSiteLogo(value) {
3852
+ if (value === void 0) {
3853
+ return void 0;
3854
+ }
3855
+ if (!isPlainObject(value)) {
3856
+ throw new PrebuildConfigError("site.logo must be an object when provided.");
3857
+ }
3858
+ assertKnownConfigKeys(value, ["src", "alt"], "site.logo");
3859
+ const src = readConfigString(value.src, "");
3860
+ if (!src) {
3861
+ throw new PrebuildConfigError(
3862
+ "site.logo.src must be a non-empty URL-like string.",
3863
+ ' "logo": { "src": "/logo.svg", "alt": "My Site" }'
3864
+ );
3865
+ }
3866
+ validateUrlLikeString(src, "site.logo.src");
3867
+ const logo = { src };
3868
+ if (value.alt !== void 0) {
3869
+ if (typeof value.alt !== "string") {
3870
+ throw new PrebuildConfigError("site.logo.alt must be a string when provided.");
3871
+ }
3872
+ const alt = value.alt.trim();
3873
+ if (alt) {
3874
+ logo.alt = alt;
3875
+ }
3876
+ }
3877
+ return logo;
3878
+ }
3879
+ function normalizeSiteMeta(value, pathLabel) {
3880
+ if (!isPlainObject(value)) {
3881
+ throw new PrebuildConfigError(`${pathLabel} must be an object when provided.`);
3882
+ }
3883
+ const meta = {};
3884
+ for (const [key, metaValue] of Object.entries(value)) {
3885
+ if (!isPreviewMetaValue(metaValue)) {
3886
+ throw new PrebuildConfigError(`${pathLabel}.${key} must be a string, number, boolean, or null.`);
3887
+ }
3888
+ meta[key] = metaValue;
3889
+ }
3890
+ return meta;
3891
+ }
3789
3892
  function normalizeFooter(value) {
3790
3893
  if (value === void 0) {
3791
3894
  return void 0;
@@ -3807,6 +3910,22 @@ function normalizeFooter(value) {
3807
3910
  }
3808
3911
  return Object.keys(footer).length ? footer : void 0;
3809
3912
  }
3913
+ function validateUrlLikeString(value, pathLabel) {
3914
+ if (value.startsWith("//")) {
3915
+ throw new PrebuildConfigError(`${pathLabel} must be an absolute URL or a safe relative path.`);
3916
+ }
3917
+ if (value.startsWith("/") || value.startsWith("./") || value.startsWith("../")) {
3918
+ return;
3919
+ }
3920
+ try {
3921
+ const url = new URL(value);
3922
+ if (!url.protocol || !url.hostname) {
3923
+ throw new Error("missing host");
3924
+ }
3925
+ } catch {
3926
+ throw new PrebuildConfigError(`${pathLabel} must be an absolute URL or a safe relative path.`);
3927
+ }
3928
+ }
3810
3929
  function readConfigBoolean(value, fallback, pathName) {
3811
3930
  if (value === void 0) {
3812
3931
  return fallback;
@@ -4176,6 +4295,69 @@ function defaultMenus() {
4176
4295
  }
4177
4296
  };
4178
4297
  }
4298
+ function normalizeCollections(value, pageInputs, skippedMarkdown) {
4299
+ if (value === void 0) {
4300
+ return {};
4301
+ }
4302
+ if (!isPlainObject(value)) {
4303
+ throw new PrebuildConfigError("collections must be an object keyed by collection id.");
4304
+ }
4305
+ const pageBySourcePath = new Map(pageInputs.map((pageInput) => [pageInput.sourcePath, pageInput]));
4306
+ const skippedByFile = new Map(
4307
+ skippedMarkdown.map((entry) => [path.resolve(rootDir, entry.file), entry.reason])
4308
+ );
4309
+ const collections = {};
4310
+ for (const [collectionId, collection] of Object.entries(value)) {
4311
+ validateConfigId(collectionId, `collections.${collectionId}`);
4312
+ if (!isPlainObject(collection)) {
4313
+ throw new PrebuildConfigError(`collections.${collectionId} must be an object.`);
4314
+ }
4315
+ assertKnownConfigKeys(collection, ["title", "description", "items"], `collections.${collectionId}`);
4316
+ if (!Array.isArray(collection.items)) {
4317
+ throw new PrebuildConfigError(`collections.${collectionId}.items must be an array of Markdown source paths.`);
4318
+ }
4319
+ const seenSourcePaths = /* @__PURE__ */ new Set();
4320
+ const items = collection.items.map((item, index) => {
4321
+ const pathLabel = `collections.${collectionId}.items[${index}]`;
4322
+ const normalizedPath = resolveCollectionSourcePath(item, pathLabel);
4323
+ const sourcePath = path.resolve(sourceDir, normalizedPath);
4324
+ if (seenSourcePaths.has(sourcePath)) {
4325
+ throw new PrebuildConfigError(`${pathLabel} duplicates ${normalizedPath} in collections.${collectionId}.`);
4326
+ }
4327
+ seenSourcePaths.add(sourcePath);
4328
+ const pageInput = pageBySourcePath.get(sourcePath);
4329
+ if (!pageInput) {
4330
+ const skippedReason = skippedByFile.get(sourcePath);
4331
+ if (skippedReason) {
4332
+ throw new PrebuildConfigError(`${pathLabel} references skipped Markdown ${normalizedPath}: ${skippedReason}`);
4333
+ }
4334
+ throw new PrebuildConfigError(`${pathLabel} was not discovered as a Markdown page: ${normalizedPath}`);
4335
+ }
4336
+ return {
4337
+ type: "page",
4338
+ slug: pageInput.route.slug
4339
+ };
4340
+ });
4341
+ collections[collectionId] = {
4342
+ title: readConfigString(collection.title, collectionId),
4343
+ ...collection.description !== void 0 ? { description: readConfigString(collection.description, "") } : {},
4344
+ items
4345
+ };
4346
+ }
4347
+ return collections;
4348
+ }
4349
+ function resolveCollectionSourcePath(value, pathLabel) {
4350
+ const normalizedPath = normalizeSourceFilePath(value, pathLabel);
4351
+ if (!normalizedPath.toLowerCase().endsWith(".md")) {
4352
+ throw new PrebuildConfigError(`${pathLabel} must be a Markdown source path ending in .md.`);
4353
+ }
4354
+ return normalizedPath;
4355
+ }
4356
+ function validateConfigId(value, pathLabel) {
4357
+ if (!/^[a-z][a-z0-9_-]{0,63}$/.test(value)) {
4358
+ throw new PrebuildConfigError(`${pathLabel} must use a lowercase config id such as "docs" or "reference-guides".`);
4359
+ }
4360
+ }
4179
4361
  function buildPrebuildReport({
4180
4362
  sourceFiles,
4181
4363
  pageInputs,
@@ -4284,11 +4466,37 @@ function normalizePublishedFrontMatter(frontMatter, sourcePath) {
4284
4466
  title: normalizeFrontMatterTitle(frontMatter.title, sourcePath),
4285
4467
  description: normalizeFrontMatterDescription(frontMatter.description, sourcePath),
4286
4468
  path: normalizeFrontMatterRoutePath(frontMatter.path, sourcePath),
4469
+ last_updated: normalizeFrontMatterLastUpdated(frontMatter.last_updated, sourcePath),
4287
4470
  discoverability: normalizeFrontMatterDiscoverability(frontMatter.discoverability, sourcePath),
4288
4471
  meta: normalizeFrontMatterMeta(frontMatter.meta, sourcePath),
4289
4472
  data: normalizeFrontMatterData(frontMatter.data, sourcePath)
4290
4473
  };
4291
4474
  }
4475
+ function normalizeLastUpdatedPolicy(value, pathLabel, ErrorClass, sourcePath = null) {
4476
+ if (value === void 0) {
4477
+ return "none";
4478
+ }
4479
+ if (typeof value === "string" && MARKDOWN_LAST_UPDATED_VALUES.has(value)) {
4480
+ return value;
4481
+ }
4482
+ if (ErrorClass === PrebuildMarkdownError) {
4483
+ throw new ErrorClass(
4484
+ sourcePath,
4485
+ `${pathLabel} must be one of: ${Array.from(MARKDOWN_LAST_UPDATED_VALUES).join(", ")}.`,
4486
+ " last_updated: none\n last_updated: git"
4487
+ );
4488
+ }
4489
+ throw new ErrorClass(
4490
+ `${pathLabel} must be one of: ${Array.from(MARKDOWN_LAST_UPDATED_VALUES).join(", ")}.`,
4491
+ ' "markdown": { "last_updated": "none" }\n "markdown": { "last_updated": "git" }'
4492
+ );
4493
+ }
4494
+ function normalizeFrontMatterLastUpdated(value, sourcePath) {
4495
+ if (value === void 0) {
4496
+ return void 0;
4497
+ }
4498
+ return normalizeLastUpdatedPolicy(value, "front matter last_updated", PrebuildMarkdownError, sourcePath);
4499
+ }
4292
4500
  function normalizeFrontMatterTitle(value, sourcePath) {
4293
4501
  if (value === void 0) {
4294
4502
  return "";
@@ -4372,6 +4580,74 @@ function normalizeFrontMatterMeta(value, sourcePath) {
4372
4580
  }
4373
4581
  return meta;
4374
4582
  }
4583
+ async function buildPageMeta(sourcePath, frontMatter, markdownConfig) {
4584
+ const meta = {
4585
+ ...frontMatter.meta
4586
+ };
4587
+ if (hasManualLastUpdatedMeta(meta)) {
4588
+ return meta;
4589
+ }
4590
+ const lastUpdatedPolicy = frontMatter.last_updated || markdownConfig.last_updated;
4591
+ if (lastUpdatedPolicy !== "git") {
4592
+ return meta;
4593
+ }
4594
+ const lastUpdatedIso = await readGitLastUpdatedIso(sourcePath);
4595
+ if (!lastUpdatedIso) {
4596
+ return meta;
4597
+ }
4598
+ return {
4599
+ ...meta,
4600
+ last_updated_iso: lastUpdatedIso,
4601
+ last_updated: lastUpdatedIso.slice(0, 10)
4602
+ };
4603
+ }
4604
+ function hasManualLastUpdatedMeta(meta) {
4605
+ return Object.hasOwn(meta, "last_updated") || Object.hasOwn(meta, "last_updated_iso");
4606
+ }
4607
+ async function readGitLastUpdatedIso(sourcePath) {
4608
+ const realSourcePath = await resolveRealPath(sourcePath);
4609
+ const realRootDir = await resolveRealPath(rootDir);
4610
+ const gitPath = path.relative(realRootDir, realSourcePath);
4611
+ try {
4612
+ const { stdout } = await execFileAsync("git", [
4613
+ "-C",
4614
+ realRootDir,
4615
+ "log",
4616
+ "-1",
4617
+ "--format=%cI",
4618
+ "--",
4619
+ gitPath
4620
+ ], {
4621
+ encoding: "utf8"
4622
+ });
4623
+ const value = stdout.trim();
4624
+ if (!value) {
4625
+ warnGitLastUpdated(sourcePath, "no commit date was found for this file.");
4626
+ return "";
4627
+ }
4628
+ if (!/^\d{4}-\d{2}-\d{2}T/.test(value)) {
4629
+ warnGitLastUpdated(sourcePath, `unexpected git date output: ${value}`);
4630
+ return "";
4631
+ }
4632
+ return value;
4633
+ } catch (error) {
4634
+ warnGitLastUpdated(sourcePath, error instanceof Error ? error.message : String(error));
4635
+ return "";
4636
+ }
4637
+ }
4638
+ async function resolveRealPath(value) {
4639
+ try {
4640
+ return await fs.realpath(value);
4641
+ } catch {
4642
+ return value;
4643
+ }
4644
+ }
4645
+ function warnGitLastUpdated(sourcePath, reason) {
4646
+ console.warn([
4647
+ `[zeropress-build-pages] Warning: could not read git last_updated for ${formatSourcePath(sourcePath)}.`,
4648
+ `Reason: ${reason}`
4649
+ ].join("\n"));
4650
+ }
4375
4651
  function isPreviewMetaValue(value) {
4376
4652
  return value === null || typeof value === "string" || typeof value === "number" && Number.isFinite(value) || typeof value === "boolean";
4377
4653
  }
@@ -4589,9 +4865,11 @@ function buildRoutePath(relativeSourcePath, sourcePath, options2 = {}) {
4589
4865
  return routePath;
4590
4866
  }
4591
4867
  function buildSlug(routePath) {
4592
- const segments = routePath.split("/");
4593
- const rawSlug = segments.at(-1) === "index" && segments.length > 1 ? segments.at(-2) : segments.at(-1);
4594
- return sanitizePathSegment(rawSlug || "");
4868
+ const segments = routePath.split("/").filter(Boolean);
4869
+ if (segments.length > 1 && segments.at(-1) === "index") {
4870
+ segments.pop();
4871
+ }
4872
+ return sanitizePathSegment(segments.join("-") || "index");
4595
4873
  }
4596
4874
  function sanitizePathSegment(segment) {
4597
4875
  return segment.replace(/[^a-z0-9.-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeropress/build-pages",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
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.6.2",
43
+ "@zeropress/build": "0.6.4",
44
44
  "gray-matter": "4.0.3"
45
45
  },
46
46
  "devDependencies": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://zeropress.dev/schemas/zeropress-build-pages.config.v0.1.schema.json",
3
+ "$id": "https://schemas.zeropress.dev/build-pages-config/v0.1/schema.json",
4
4
  "title": "ZeroPress Build Pages Config v0.1",
5
5
  "description": "Optional source-directory configuration for @zeropress/build-pages workflows.",
6
6
  "markdownDescription": "Optional source-directory configuration for `@zeropress/build-pages` workflows.",
@@ -21,6 +21,9 @@
21
21
  "site": {
22
22
  "$ref": "#/$defs/site"
23
23
  },
24
+ "markdown": {
25
+ "$ref": "#/$defs/markdown"
26
+ },
24
27
  "front_page": {
25
28
  "$ref": "#/$defs/frontPage"
26
29
  },
@@ -29,6 +32,9 @@
29
32
  },
30
33
  "menus": {
31
34
  "$ref": "#/$defs/menus"
35
+ },
36
+ "collections": {
37
+ "$ref": "#/$defs/collections"
32
38
  }
33
39
  },
34
40
  "$defs": {
@@ -50,6 +56,15 @@
50
56
  "description": "Canonical site URL. Use an empty string or omit this field for local builds without canonical output.",
51
57
  "markdownDescription": "Canonical site URL. Use an empty string or omit this field for local builds without canonical output."
52
58
  },
59
+ "logo": {
60
+ "$ref": "#/$defs/siteLogo"
61
+ },
62
+ "locale": {
63
+ "type": "string",
64
+ "minLength": 2,
65
+ "description": "Site locale used for generated preview-data language metadata, such as html lang and feed language.",
66
+ "markdownDescription": "Site locale used for generated preview-data language metadata, such as HTML `lang` and feed language."
67
+ },
53
68
  "expose_generator": {
54
69
  "type": "boolean",
55
70
  "default": true,
@@ -70,6 +85,29 @@
70
85
  },
71
86
  "footer": {
72
87
  "$ref": "#/$defs/siteFooter"
88
+ },
89
+ "meta": {
90
+ "$ref": "#/$defs/previewMeta"
91
+ }
92
+ }
93
+ },
94
+ "siteLogo": {
95
+ "type": "object",
96
+ "additionalProperties": false,
97
+ "required": ["src"],
98
+ "description": "Optional site logo data copied into generated preview-data.",
99
+ "markdownDescription": "Optional site logo data copied into generated preview-data.",
100
+ "properties": {
101
+ "src": {
102
+ "type": "string",
103
+ "minLength": 1,
104
+ "description": "Logo URL or safe relative path.",
105
+ "markdownDescription": "Logo URL or safe relative path."
106
+ },
107
+ "alt": {
108
+ "type": "string",
109
+ "description": "Optional logo alternative text. Themes may fall back to site.title when omitted.",
110
+ "markdownDescription": "Optional logo alternative text. Themes may fall back to `site.title` when omitted."
73
111
  }
74
112
  }
75
113
  },
@@ -92,6 +130,21 @@
92
130
  }
93
131
  }
94
132
  },
133
+ "markdown": {
134
+ "type": "object",
135
+ "additionalProperties": false,
136
+ "description": "Markdown source processing options.",
137
+ "markdownDescription": "Markdown source processing options.",
138
+ "properties": {
139
+ "last_updated": {
140
+ "type": "string",
141
+ "enum": ["none", "git"],
142
+ "default": "none",
143
+ "description": "Optional page meta enrichment. none does not generate last_updated values; git reads the latest commit date for each Markdown file.",
144
+ "markdownDescription": "Optional page meta enrichment. `none` does not generate `last_updated` values; `git` reads the latest commit date for each Markdown file."
145
+ }
146
+ }
147
+ },
95
148
  "frontPage": {
96
149
  "type": "object",
97
150
  "additionalProperties": false,
@@ -287,23 +340,68 @@
287
340
  "default": []
288
341
  }
289
342
  }
343
+ },
344
+ "collections": {
345
+ "type": "object",
346
+ "description": "Reading-order collections generated from Markdown source paths.",
347
+ "markdownDescription": "Reading-order collections generated from Markdown source paths.",
348
+ "propertyNames": {
349
+ "type": "string",
350
+ "pattern": "^[a-z][a-z0-9_-]{0,63}$"
351
+ },
352
+ "additionalProperties": {
353
+ "$ref": "#/$defs/collection"
354
+ }
355
+ },
356
+ "collection": {
357
+ "type": "object",
358
+ "additionalProperties": false,
359
+ "required": ["items"],
360
+ "properties": {
361
+ "title": {
362
+ "type": "string",
363
+ "minLength": 1
364
+ },
365
+ "description": {
366
+ "type": "string"
367
+ },
368
+ "items": {
369
+ "type": "array",
370
+ "items": {
371
+ "type": "string",
372
+ "minLength": 1,
373
+ "pattern": "^[^\\\\?#]+\\.md$"
374
+ }
375
+ }
376
+ }
290
377
  }
291
378
  },
292
379
  "examples": [
293
380
  {
294
- "$schema": "https://zeropress.dev/schemas/zeropress-build-pages.config.v0.1.schema.json",
381
+ "$schema": "https://schemas.zeropress.dev/build-pages-config/v0.1/schema.json",
295
382
  "version": "0.1",
296
383
  "site": {
297
384
  "title": "ZeroPress Public Docs",
298
385
  "description": "Public documentation.",
299
386
  "url": "https://zeropress.dev",
387
+ "logo": {
388
+ "src": "/logo.svg",
389
+ "alt": "ZeroPress Public Docs"
390
+ },
391
+ "locale": "en-US",
300
392
  "expose_generator": true,
301
393
  "search": true,
302
- "indexing": true
394
+ "indexing": true,
395
+ "meta": {
396
+ "issue": "Spring 2026"
397
+ }
303
398
  },
304
399
  "front_page": {
305
400
  "type": "markdown"
306
401
  },
402
+ "markdown": {
403
+ "last_updated": "git"
404
+ },
307
405
  "custom_html": {
308
406
  "head_end": {
309
407
  "file": ".zeropress/head-end.html"
@@ -327,6 +425,15 @@
327
425
  }
328
426
  ]
329
427
  }
428
+ },
429
+ "collections": {
430
+ "guides": {
431
+ "title": "Guides",
432
+ "items": [
433
+ "getting-started/index.md",
434
+ "deployment/index.md"
435
+ ]
436
+ }
330
437
  }
331
438
  }
332
439
  ]