blume 1.4.3 → 1.5.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.
Files changed (204) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +16 -12
  3. package/dist/cli/index.js +1784 -633
  4. package/dist/cli/index.js.map +111 -106
  5. package/dist/types/ai/component-markdown.d.ts +14 -4
  6. package/dist/types/core/config-input.d.ts +80 -28
  7. package/dist/types/core/config.d.ts +2 -1
  8. package/dist/types/core/data.d.ts +19 -3
  9. package/dist/types/core/diagnostics.d.ts +5 -1
  10. package/dist/types/core/i18n-ui.d.ts +12 -0
  11. package/dist/types/core/schema.d.ts +112 -15
  12. package/dist/types/core/sources/types.d.ts +3 -1
  13. package/dist/types/core/standard-schema.d.ts +7 -3
  14. package/dist/types/core/types.d.ts +43 -2
  15. package/dist/types/core/ui-packs/index.d.ts +9 -1
  16. package/dist/types/openapi/references.d.ts +6 -5
  17. package/dist/types/seo/x-handle.d.ts +3 -2
  18. package/dist/types/theme/fonts.d.ts +11 -2
  19. package/docs/advanced/api-reference.mdx +8 -6
  20. package/docs/advanced/custom-pages.mdx +5 -1
  21. package/docs/configuration/index.mdx +1 -1
  22. package/docs/configuration/search.mdx +2 -0
  23. package/docs/configuration/seo.mdx +1 -1
  24. package/docs/configuration/theming.mdx +4 -2
  25. package/docs/content/i18n.mdx +1 -1
  26. package/docs/content/meta.mdx +2 -1
  27. package/docs/content/meta.ts +1 -0
  28. package/docs/content/navigation.mdx +35 -1
  29. package/docs/content/versioning.mdx +106 -0
  30. package/docs/reference/cli.mdx +2 -1
  31. package/docs/reference/frontmatter.mdx +3 -0
  32. package/package.json +3 -1
  33. package/skills/blume-migrate/SKILL.md +2 -2
  34. package/skills/blume-migrate/references/docusaurus.md +1 -1
  35. package/skills/blume-migrate/references/fumadocs.md +1 -1
  36. package/skills/blume-migrate/references/mintlify.md +1 -1
  37. package/src/ai/agent-readability.ts +37 -10
  38. package/src/ai/ask-context.ts +5 -1
  39. package/src/ai/ask.ts +10 -1
  40. package/src/ai/component-markdown.ts +80 -43
  41. package/src/ai/llms.ts +40 -16
  42. package/src/ai/mcp/data.ts +48 -12
  43. package/src/ai/mcp/discovery.ts +28 -11
  44. package/src/ai/mcp/server.ts +183 -38
  45. package/src/ai/mcp/tools.ts +3 -3
  46. package/src/ai/skills.ts +32 -9
  47. package/src/ai/visibility.ts +2 -2
  48. package/src/astro/component-slots.ts +2 -0
  49. package/src/astro/examples.ts +6 -2
  50. package/src/astro/generate.ts +64 -34
  51. package/src/astro/integration.ts +13 -2
  52. package/src/astro/islands.ts +16 -9
  53. package/src/astro/templates.ts +181 -40
  54. package/src/audit/agent.ts +2 -2
  55. package/src/audit/checks/content.ts +26 -11
  56. package/src/audit/checks/dns-aid.ts +3 -0
  57. package/src/audit/checks/indexability.ts +24 -6
  58. package/src/audit/checks/llms.ts +9 -4
  59. package/src/audit/checks/network.ts +2 -0
  60. package/src/audit/checks/social.ts +18 -10
  61. package/src/audit/crawl.ts +37 -9
  62. package/src/audit/report.ts +20 -19
  63. package/src/audit/run.ts +5 -2
  64. package/src/audit/snapshot.ts +2 -4
  65. package/src/audit/types.ts +25 -3
  66. package/src/blume-modules.d.ts +5 -1
  67. package/src/cli/commands/audit.ts +9 -4
  68. package/src/cli/commands/build.ts +15 -9
  69. package/src/cli/commands/dev.ts +2 -0
  70. package/src/cli/commands/doctor.ts +2 -0
  71. package/src/cli/commands/eval.ts +7 -3
  72. package/src/cli/commands/init.ts +9 -9
  73. package/src/cli/commands/mcp-stdio.ts +3 -0
  74. package/src/cli/commands/translate.ts +14 -3
  75. package/src/cli/commands/version.ts +85 -0
  76. package/src/cli/dev-lock.ts +31 -10
  77. package/src/cli/eject-scripts.ts +17 -2
  78. package/src/cli/index.ts +2 -0
  79. package/src/cli/init/questions.ts +1 -1
  80. package/src/cli/init/scaffold.ts +22 -15
  81. package/src/cli/internal-error.ts +1 -0
  82. package/src/components/content/auto-type-table.ts +3 -0
  83. package/src/components/content/diff.ts +9 -5
  84. package/src/components/content/github-info.ts +2 -0
  85. package/src/components/islands/ask-ai.tsx +33 -25
  86. package/src/components/islands/hooks.ts +5 -1
  87. package/src/components/islands/webmcp.ts +49 -12
  88. package/src/components/layout/Fonts.astro +23 -3
  89. package/src/components/layout/Header.astro +25 -1
  90. package/src/components/layout/NavSelector.astro +11 -2
  91. package/src/components/layout/NavTree.astro +4 -2
  92. package/src/components/layout/PageLayout.astro +72 -3
  93. package/src/components/layout/ReferenceLayout.astro +2 -1
  94. package/src/components/layout/RootLayout.astro +20 -1
  95. package/src/components/layout/Search.astro +77 -13
  96. package/src/components/layout/VersionBanner.astro +39 -0
  97. package/src/components/layout/analytics-client.ts +8 -5
  98. package/src/components/layout/hydration-hint.ts +1 -1
  99. package/src/components/layout/nav-utils.ts +1 -4
  100. package/src/components/layout/overrides.ts +25 -12
  101. package/src/components/layout/search/algolia.ts +18 -5
  102. package/src/components/layout/search/endpoint.ts +3 -0
  103. package/src/components/layout/search/flexsearch.ts +23 -7
  104. package/src/components/layout/search/orama-cloud.ts +1 -1
  105. package/src/components/layout/search/orama.ts +4 -1
  106. package/src/components/layout/search/pagefind.ts +2 -0
  107. package/src/components/layout/search/types.ts +13 -1
  108. package/src/components/layout/search/typesense.ts +19 -3
  109. package/src/components/openapi/ApiOverview.astro +32 -6
  110. package/src/components/openapi/AsyncApiOperation.astro +237 -0
  111. package/src/components/openapi/Bindings.astro +89 -0
  112. package/src/components/openapi/MethodBadge.astro +3 -0
  113. package/src/components/openapi/Operation.astro +7 -2
  114. package/src/components/openapi/PanelTabs.astro +131 -0
  115. package/src/components/openapi/ParametersTable.astro +2 -0
  116. package/src/components/openapi/RequestPanel.astro +12 -119
  117. package/src/components/openapi/async-snippets.ts +174 -0
  118. package/src/components/openapi/async.ts +348 -0
  119. package/src/components/openapi/helpers.ts +52 -20
  120. package/src/components/openapi/security.ts +102 -29
  121. package/src/components/openapi/snippets.ts +11 -11
  122. package/src/core/component-overrides.ts +28 -23
  123. package/src/core/config-input.ts +89 -28
  124. package/src/core/config.ts +20 -7
  125. package/src/core/content.ts +3 -1
  126. package/src/core/data.ts +19 -3
  127. package/src/core/define-components.ts +5 -0
  128. package/src/core/diagnostics.ts +46 -38
  129. package/src/core/frontmatter.ts +33 -7
  130. package/src/core/graph.ts +137 -53
  131. package/src/core/i18n-ui.ts +15 -0
  132. package/src/core/i18n.ts +16 -8
  133. package/src/core/last-modified.ts +49 -0
  134. package/src/core/load-module.ts +1 -0
  135. package/src/core/manifest.ts +92 -3
  136. package/src/core/meta.ts +44 -14
  137. package/src/core/nav-diagnostics.ts +3 -3
  138. package/src/core/navigation.ts +247 -67
  139. package/src/core/project-graph.ts +26 -3
  140. package/src/core/schema.ts +214 -68
  141. package/src/core/sources/assets.ts +2 -0
  142. package/src/core/sources/cache.ts +6 -0
  143. package/src/core/sources/github-releases.ts +39 -31
  144. package/src/core/sources/mdx-remote.ts +4 -0
  145. package/src/core/sources/normalize.ts +67 -20
  146. package/src/core/sources/notion.ts +49 -17
  147. package/src/core/sources/portable-text.ts +32 -11
  148. package/src/core/sources/sanity.ts +68 -14
  149. package/src/core/sources/types.ts +4 -0
  150. package/src/core/sources/watch.ts +1 -1
  151. package/src/core/standard-schema.ts +9 -3
  152. package/src/core/text-width.ts +26 -0
  153. package/src/core/tsconfig-aliases.ts +9 -5
  154. package/src/core/types.ts +45 -2
  155. package/src/core/ui-packs/index.ts +9 -1
  156. package/src/core/version-cut.ts +301 -0
  157. package/src/core/version.ts +2 -0
  158. package/src/core/versions.ts +170 -0
  159. package/src/deploy/adapter-output.ts +5 -2
  160. package/src/deploy/cloudflare-negotiation.ts +25 -10
  161. package/src/deploy/sitemap.ts +33 -1
  162. package/src/deploy/vercel-negotiation.ts +45 -18
  163. package/src/eval/report.ts +4 -4
  164. package/src/eval/run.ts +2 -2
  165. package/src/eval/schema.ts +1 -1
  166. package/src/markdown/base-links.ts +6 -6
  167. package/src/markdown/directives.ts +7 -1
  168. package/src/markdown/heading-anchors.ts +17 -6
  169. package/src/markdown/index.ts +73 -24
  170. package/src/markdown/inline-code.ts +14 -2
  171. package/src/markdown/language-icon.ts +6 -2
  172. package/src/markdown/mdast.ts +18 -4
  173. package/src/markdown/package-commands.ts +6 -8
  174. package/src/markdown/table-wrap.ts +4 -1
  175. package/src/markdown/twoslash.ts +2 -0
  176. package/src/og/card.ts +33 -12
  177. package/src/og/derive.ts +43 -27
  178. package/src/openapi/asyncapi.ts +366 -0
  179. package/src/openapi/model.ts +126 -57
  180. package/src/openapi/parse.ts +97 -5
  181. package/src/openapi/references.ts +12 -10
  182. package/src/openapi/render-mdx.ts +73 -34
  183. package/src/openapi/scalar.ts +6 -8
  184. package/src/openapi/source.ts +98 -28
  185. package/src/registry/eject.ts +7 -2
  186. package/src/search/documents.ts +25 -5
  187. package/src/search/facets.ts +7 -5
  188. package/src/search/orama-index.ts +66 -20
  189. package/src/search/popular.ts +10 -5
  190. package/src/search/providers.ts +2 -2
  191. package/src/search/sync/index.ts +2 -0
  192. package/src/search/sync/typesense.ts +4 -2
  193. package/src/seo/jsonld.ts +24 -6
  194. package/src/seo/x-handle.ts +8 -3
  195. package/src/theme/chrome-icons.ts +7 -2
  196. package/src/theme/entry.ts +24 -2
  197. package/src/theme/fonts.ts +83 -7
  198. package/src/theme/icons.ts +4 -2
  199. package/src/theme/palette.ts +22 -14
  200. package/src/translate/meta.ts +15 -6
  201. package/src/translate/report.ts +9 -5
  202. package/src/translate/run.ts +10 -4
  203. package/src/translate/validate.ts +52 -17
  204. package/src/translate/work-list.ts +0 -0
package/dist/cli/index.js CHANGED
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
3
3
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
5
  // src/cli/index.ts
6
- import { defineCommand as defineCommand15, runMain } from "citty";
6
+ import { defineCommand as defineCommand16, runMain } from "citty";
7
7
 
8
8
  // src/core/version.ts
9
9
  import { readFileSync } from "node:fs";
@@ -297,7 +297,7 @@ var DOCS_DEPLOYMENT = "/docs/deployment";
297
297
  var DOCS_REFERENCE_CLI = "/docs/reference/cli";
298
298
  var DOCS_CONTENT_SOURCES = "/docs/content/sources";
299
299
  var DOCS_CONTENT_NAVIGATION = "/docs/content/navigation";
300
- var DOCS_PATHS = {
300
+ var DOCS_PATHS = new Map(Object.entries({
301
301
  BLUME_ADAPTER_REQUIRED: DOCS_DEPLOYMENT,
302
302
  BLUME_ASSETS_UNCHECKED: DOCS_REFERENCE_CLI,
303
303
  BLUME_ASSET_FETCH_FAILED: DOCS_CONTENT_SOURCES,
@@ -320,6 +320,7 @@ var DOCS_PATHS = {
320
320
  BLUME_NAV_MISSING_PAGE: DOCS_CONTENT_NAVIGATION,
321
321
  BLUME_NODE_VERSION: "/docs/quickstart",
322
322
  BLUME_SERVER_FEATURE_REQUIRED: DOCS_DEPLOYMENT,
323
+ BLUME_SIDEBAR_DISPLAY_IGNORED: DOCS_CONTENT_NAVIGATION,
323
324
  BLUME_SOURCE_FETCH_FAILED: DOCS_CONTENT_SOURCES,
324
325
  BLUME_SOURCE_MISCONFIGURED: DOCS_CONTENT_SOURCES,
325
326
  BLUME_SOURCE_OFFLINE: DOCS_CONTENT_SOURCES,
@@ -327,16 +328,17 @@ var DOCS_PATHS = {
327
328
  BLUME_SOURCE_UNAVAILABLE: DOCS_CONTENT_SOURCES,
328
329
  BLUME_UNKNOWN_COMPONENT: "/docs/configuration/customization",
329
330
  BLUME_UNKNOWN_ICON: DOCS_CONTENT_NAVIGATION
330
- };
331
+ }));
331
332
  var resolveDocsUrl = (code) => {
332
- const path = DOCS_PATHS[code];
333
+ const path = DOCS_PATHS.get(code);
333
334
  return path ? `${DOCS_BASE}${path}` : undefined;
334
335
  };
335
336
  var enrichDiagnostic = (diagnostic) => diagnostic.docsUrl ? diagnostic : { ...diagnostic, docsUrl: resolveDocsUrl(diagnostic.code) };
336
337
  var REGEXP_SPECIAL = /[$()*+.?[\\\]^{|}]/gu;
337
338
  var escapeRegExp = (value) => value.replaceAll(REGEXP_SPECIAL, String.raw`\$&`);
339
+ var isKeySegment = (segment) => typeof segment === "string";
338
340
  var stepSegment = (source, segment, cursor) => {
339
- if (typeof segment !== "string") {
341
+ if (!isKeySegment(segment)) {
340
342
  return { index: -1, next: cursor, stop: false };
341
343
  }
342
344
  const matcher = new RegExp(`(?<![\\w$])${escapeRegExp(segment)}\\s*[:=]`, "gu");
@@ -1298,10 +1300,10 @@ var GLYPH = {
1298
1300
  warning: "⚠"
1299
1301
  };
1300
1302
  var PREVIEW = 3;
1301
- var TIER_FLAG = {
1302
- external: "--external",
1303
- network: "--url <origin>"
1304
- };
1303
+ var TIER_FLAGS = [
1304
+ { flag: "--external", tier: "external" },
1305
+ { flag: "--url <origin>", tier: "network" }
1306
+ ];
1305
1307
  var rollup = (diagnostics) => {
1306
1308
  const groups = new Map;
1307
1309
  for (const diagnostic of diagnostics) {
@@ -1318,12 +1320,13 @@ var rollup = (diagnostics) => {
1318
1320
  warning: 1
1319
1321
  };
1320
1322
  const checks = [...groups.entries()].map(([id, findings]) => {
1321
- const { category, severity, title } = checkMeta(id);
1323
+ const checkId = id;
1324
+ const { category, severity, title } = checkMeta(checkId);
1322
1325
  return {
1323
1326
  category,
1324
1327
  count: findings.length,
1325
1328
  findings,
1326
- id,
1329
+ id: checkId,
1327
1330
  severity,
1328
1331
  title
1329
1332
  };
@@ -1335,9 +1338,9 @@ var rollup = (diagnostics) => {
1335
1338
  }
1336
1339
  return checks.toSorted((a, b) => (worst.get(a.category) ?? 0) - (worst.get(b.category) ?? 0) || a.category.localeCompare(b.category) || order[a.severity] - order[b.severity] || b.count - a.count);
1337
1340
  };
1338
- var skippedTiers = (tiers) => Object.keys(TIER_FLAG).filter((tier) => !tiers[tier]).map((tier) => {
1341
+ var skippedTiers = (tiers) => TIER_FLAGS.filter(({ tier }) => !tiers[tier]).map(({ flag, tier }) => {
1339
1342
  const label = CHECKS.filter((check) => check.tier === tier).length;
1340
- return ` ${colors2.dim(`⊘ ${tier.padEnd(12)} skipped — pass ${TIER_FLAG[tier]} (${label} checks)`)}`;
1343
+ return ` ${colors2.dim(`⊘ ${tier.padEnd(12)} skipped — pass ${flag} (${label} checks)`)}`;
1341
1344
  });
1342
1345
  var activeChecks = (tiers) => CHECKS.filter((check) => tiers[check.tier]).length;
1343
1346
  var auditCount = (result) => activeChecks(result.tiers) * result.pages;
@@ -1721,6 +1724,9 @@ var assetChecks = {
1721
1724
  tier: "static"
1722
1725
  };
1723
1726
 
1727
+ // src/audit/checks/content.ts
1728
+ import stringWidth from "string-width";
1729
+
1724
1730
  // src/core/frontmatter.ts
1725
1731
  import baseMatter from "gray-matter";
1726
1732
  import { dump, load } from "js-yaml";
@@ -1746,17 +1752,21 @@ var opensWithThematicBreak = (input) => {
1746
1752
  return !input.includes(`
1747
1753
  ---`, 1);
1748
1754
  };
1749
- var passthrough = (input) => ({
1750
- content: input,
1751
- data: {},
1752
- excerpt: "",
1753
- isEmpty: false,
1754
- language: "",
1755
- matter: "",
1756
- orig: input,
1757
- stringify: () => input
1758
- });
1759
- var matter = Object.assign((input, options) => typeof input === "string" && opensWithThematicBreak(input) ? passthrough(input) : baseMatter(input, withYamlEngine(options)), baseMatter, {
1755
+ var passthrough = (input) => {
1756
+ const file = {
1757
+ content: input,
1758
+ data: {},
1759
+ excerpt: "",
1760
+ isEmpty: false,
1761
+ language: "",
1762
+ matter: "",
1763
+ orig: input,
1764
+ stringify: () => input
1765
+ };
1766
+ return file;
1767
+ };
1768
+ var isStringInput = (input) => typeof input === "string";
1769
+ var matter = Object.assign((input, options) => isStringInput(input) && opensWithThematicBreak(input) ? passthrough(input) : baseMatter(input, withYamlEngine(options)), baseMatter, {
1760
1770
  read: (filepath, options) => baseMatter.read(filepath, withYamlEngine(options)),
1761
1771
  stringify: (file, data, options) => baseMatter.stringify(file, data, withYamlEngine(options))
1762
1772
  });
@@ -1776,10 +1786,12 @@ var DEFAULT_THRESHOLDS = {
1776
1786
  };
1777
1787
 
1778
1788
  // src/audit/checks/content.ts
1789
+ var WHITESPACE = /\s+/gu;
1790
+ var isDateValue = (value) => typeof value === "string" || value instanceof Date;
1779
1791
  var frontmatterDate = (source) => {
1780
1792
  try {
1781
1793
  const { date } = frontmatter_default(source).data;
1782
- if (typeof date !== "string" && !(date instanceof Date)) {
1794
+ if (!isDateValue(date)) {
1783
1795
  return null;
1784
1796
  }
1785
1797
  const parsed = date instanceof Date ? date : new Date(date);
@@ -1800,9 +1812,10 @@ var titleChecks = (context, page) => {
1800
1812
  if (page.titles.length > 1) {
1801
1813
  found.push(finding("BLUME_AUDIT_TITLE_MULTIPLE", pageSite(context, page), `Page has ${page.titles.length} <title> tags; search engines use only the first.`));
1802
1814
  }
1803
- if (title.length > titleMax || title.length < titleMin) {
1804
- const direction = title.length > titleMax ? "long" : "short";
1805
- found.push(finding("BLUME_AUDIT_TITLE_LENGTH", pageSite(context, page, ["title"]), `Title is ${title.length} characters too ${direction} (aim for ${titleMin}–${titleMax}).`));
1815
+ const width = stringWidth(title.replaceAll(WHITESPACE, " "));
1816
+ if (width > titleMax || width < titleMin) {
1817
+ const direction = width > titleMax ? "long" : "short";
1818
+ found.push(finding("BLUME_AUDIT_TITLE_LENGTH", pageSite(context, page, ["title"]), `Title renders ${width} columns wide — too ${direction} (aim for ${titleMin}–${titleMax}).`));
1806
1819
  }
1807
1820
  return found;
1808
1821
  };
@@ -1821,9 +1834,10 @@ var descriptionChecks = (context, page) => {
1821
1834
  if (page.descriptions.length > 1) {
1822
1835
  found.push(finding("BLUME_AUDIT_DESCRIPTION_MULTIPLE", pageSite(context, page), `Page has ${page.descriptions.length} meta description tags.`));
1823
1836
  }
1824
- if (description.length > descriptionMax || description.length < descriptionMin) {
1825
- const direction = description.length > descriptionMax ? "long" : "short";
1826
- found.push(finding("BLUME_AUDIT_DESCRIPTION_LENGTH", pageSite(context, page, ["description"]), `Meta description is ${description.length} characters — too ${direction} (aim for ${descriptionMin}–${descriptionMax}).`));
1837
+ const width = stringWidth(description.replaceAll(WHITESPACE, " "));
1838
+ if (width > descriptionMax || width < descriptionMin) {
1839
+ const direction = width > descriptionMax ? "long" : "short";
1840
+ found.push(finding("BLUME_AUDIT_DESCRIPTION_LENGTH", pageSite(context, page, ["description"]), `Meta description renders ${width} columns wide — too ${direction} (aim for ${descriptionMin}–${descriptionMax}).`));
1827
1841
  }
1828
1842
  return found;
1829
1843
  };
@@ -2236,7 +2250,13 @@ var canonicalChecks = (context, page) => {
2236
2250
  if (redirect) {
2237
2251
  found.push(finding("BLUME_AUDIT_CANONICAL_BAD_TARGET", pageSite(context, page, ["seo", "canonical"]), `Canonical points at ${target}, which is a redirect to ${redirect.to}.`));
2238
2252
  } else if (context.byUrl.has(target)) {
2239
- found.push(finding("BLUME_AUDIT_CANONICAL_NOT_SELF", pageSite(context, page, ["seo", "canonical"]), `Page declares ${target} as its canonical, so it will not be indexed itself.`));
2253
+ const version = page.route?.version;
2254
+ const archived = version ? context.project.config.versions?.archived.find((entry) => entry.id === version) : undefined;
2255
+ const latestAlternate = page.route?.versionAlternates.find((alternate) => alternate.version === "");
2256
+ const intentional = archived?.canonical === "latest" && latestAlternate !== undefined && normalizePath(latestAlternate.path) === target;
2257
+ if (!intentional) {
2258
+ found.push(finding("BLUME_AUDIT_CANONICAL_NOT_SELF", pageSite(context, page, ["seo", "canonical"]), `Page declares ${target} as its canonical, so it will not be indexed itself.`));
2259
+ }
2240
2260
  } else {
2241
2261
  found.push(finding("BLUME_AUDIT_CANONICAL_BAD_TARGET", pageSite(context, page, ["seo", "canonical"]), `Canonical points at ${target}, which is not a page on this site.`));
2242
2262
  }
@@ -2407,9 +2427,10 @@ var linkChecks = {
2407
2427
  };
2408
2428
 
2409
2429
  // src/audit/checks/llms.ts
2430
+ var isLlmsToggleObject = (value) => typeof value === "object" && value !== null;
2410
2431
  var llmsConfig = (context) => {
2411
2432
  const value = context.project.config.ai?.llmsTxt;
2412
- if (typeof value === "object" && value !== null) {
2433
+ if (isLlmsToggleObject(value)) {
2413
2434
  return { enabled: value.enabled, openapi: value.openapi };
2414
2435
  }
2415
2436
  return { enabled: value !== false, openapi: true };
@@ -2946,22 +2967,22 @@ var socialChecks = {
2946
2967
  },
2947
2968
  tier: "static"
2948
2969
  };
2970
+ var isJsonLdNode = (value) => typeof value === "object" && value !== null;
2949
2971
  var jsonLdProblems = (node) => {
2950
- if (typeof node !== "object" || node === null) {
2972
+ if (!isJsonLdNode(node)) {
2951
2973
  return ["it is not an object"];
2952
2974
  }
2953
- const record = node;
2954
2975
  const problems = [];
2955
- if (!record["@context"]) {
2976
+ if (!node["@context"]) {
2956
2977
  problems.push("@context");
2957
2978
  }
2958
- const graph = record["@graph"];
2979
+ const graph = node["@graph"];
2959
2980
  if (Array.isArray(graph)) {
2960
- const untyped = graph.filter((entry) => typeof entry !== "object" || entry === null || !entry["@type"]).length;
2981
+ const untyped = graph.filter((entry) => !isJsonLdNode(entry) || !entry["@type"]).length;
2961
2982
  if (untyped > 0) {
2962
2983
  problems.push(`@type on ${untyped} of its ${graph.length} @graph nodes`);
2963
2984
  }
2964
- } else if (!record["@type"]) {
2985
+ } else if (!node["@type"]) {
2965
2986
  problems.push("@type");
2966
2987
  }
2967
2988
  return problems;
@@ -3155,17 +3176,19 @@ var referencesFor = (kind, block, defaultLabel, renderer, display, basePath) =>
3155
3176
  };
3156
3177
  });
3157
3178
  };
3158
- var NO_DISPLAY = { codeSamples: [], expandSchemas: false };
3159
3179
  var resolveReferences = (config) => [
3160
3180
  ...referencesFor("openapi", config.openapi, "API Reference", config.openapi.renderer, {
3161
3181
  codeSamples: config.openapi.codeSamples,
3162
3182
  expandSchemas: config.openapi.expandSchemas
3163
3183
  }, config.basePath),
3164
- ...referencesFor("asyncapi", config.asyncapi, "Events", "scalar", NO_DISPLAY, config.basePath)
3184
+ ...referencesFor("asyncapi", config.asyncapi, "Events", config.asyncapi.renderer, {
3185
+ codeSamples: config.asyncapi.codeSamples,
3186
+ expandSchemas: config.asyncapi.expandSchemas
3187
+ }, config.basePath)
3165
3188
  ];
3166
3189
  var referenceRoutes = (config) => resolveReferences(config).map((ref) => ref.renderer === "blume" ? withBasePath(config.basePath, ref.route) : ref.route);
3167
3190
  var blumeReferenceOf = (ref, seen, usedSlugs) => {
3168
- if (ref.kind !== "openapi" || ref.renderer !== "blume") {
3191
+ if (ref.renderer !== "blume") {
3169
3192
  return null;
3170
3193
  }
3171
3194
  const kept = seen.get(ref.route);
@@ -3279,7 +3302,7 @@ var excludeDirSegments = (patterns) => patterns.map((pattern) => /^(?<dir>[^*/]+
3279
3302
  var ignoringWatchListener = (onChange, ignoreDirs = BLUME_WATCH_IGNORE_DIRS) => {
3280
3303
  const ignore = new Set(ignoreDirs);
3281
3304
  return (_event, filename) => {
3282
- if (typeof filename === "string" && filename.split(/[/\\]/u).some((segment) => ignore.has(segment))) {
3305
+ if (filename !== null && filename.split(/[/\\]/u).some((segment) => ignore.has(segment))) {
3283
3306
  return;
3284
3307
  }
3285
3308
  onChange();
@@ -3492,8 +3515,9 @@ var slugifyFontName = (name) => name.toLowerCase().replaceAll(/[^a-z0-9]+/gu, "-
3492
3515
  var fontVar = (slug) => `--blume-ff-${slug}`;
3493
3516
  var SLOTS = ["display", "body", "mono"];
3494
3517
  var slotCategory = (slot) => slot === "mono" ? "mono" : "sans";
3518
+ var isSlugValue = (value) => typeof value === "string";
3495
3519
  var resolveFontValue = (slot, value) => {
3496
- if (typeof value === "string") {
3520
+ if (isSlugValue(value)) {
3497
3521
  if (!isFontSlug(value)) {
3498
3522
  return null;
3499
3523
  }
@@ -3559,7 +3583,7 @@ var buildFontEntries = (fonts) => {
3559
3583
  return [...entries.values()];
3560
3584
  };
3561
3585
  var slotSlug = (value) => {
3562
- if (typeof value === "string") {
3586
+ if (isSlugValue(value)) {
3563
3587
  return isFontSlug(value) ? value : null;
3564
3588
  }
3565
3589
  return slugifyFontName(value.name);
@@ -3583,7 +3607,47 @@ ${lines.join(`
3583
3607
  }
3584
3608
  ` : "";
3585
3609
  };
3586
- var configuredCssVars = (fonts) => buildFontEntries(fonts).map((entry) => entry.cssVariable);
3610
+ var PRELOAD_WEIGHTS = {
3611
+ body: [400, 500],
3612
+ display: [500, 600],
3613
+ mono: [400]
3614
+ };
3615
+ var entryWeights = (entry) => entry.kind === "remote" ? entry.weights : entry.variants.map((variant) => variant.weight);
3616
+ var preloadWeightsFor = (slot, entry) => {
3617
+ const preferred = PRELOAD_WEIGHTS[slot];
3618
+ const weights = entryWeights(entry);
3619
+ const numeric = weights.filter((weight) => typeof weight === "number");
3620
+ const hits = preferred.filter((weight) => numeric.includes(weight));
3621
+ if (hits.length > 0) {
3622
+ return hits;
3623
+ }
3624
+ return numeric.length === weights.length ? numeric : preferred;
3625
+ };
3626
+ var configuredFonts = (fonts) => {
3627
+ if (!fonts) {
3628
+ return [];
3629
+ }
3630
+ const heads = new Map;
3631
+ for (const slot of SLOTS) {
3632
+ const value = fonts[slot];
3633
+ if (value === undefined) {
3634
+ continue;
3635
+ }
3636
+ const entry = resolveFontValue(slot, value);
3637
+ if (!entry) {
3638
+ continue;
3639
+ }
3640
+ const weights = heads.get(entry.cssVariable) ?? new Set;
3641
+ for (const weight of preloadWeightsFor(slot, entry)) {
3642
+ weights.add(weight);
3643
+ }
3644
+ heads.set(entry.cssVariable, weights);
3645
+ }
3646
+ return [...heads].map(([cssVariable, weights]) => ({
3647
+ cssVariable,
3648
+ preloadWeights: [...weights].toSorted((a, b) => a - b)
3649
+ }));
3650
+ };
3587
3651
 
3588
3652
  // src/astro/templates.ts
3589
3653
  var WORKSPACE_MARKERS = [
@@ -3626,9 +3690,9 @@ var ADAPTER_IMPORTS = {
3626
3690
  node: "@astrojs/node",
3627
3691
  vercel: "@astrojs/vercel"
3628
3692
  };
3629
- var ADAPTER_OPTIONS = {
3630
- node: '{ mode: "standalone" }'
3631
- };
3693
+ var ADAPTER_OPTIONS = new Map([
3694
+ ["node", '{ mode: "standalone" }']
3695
+ ]);
3632
3696
  var WRANGLER_CONFIG_FILES = [
3633
3697
  "wrangler.jsonc",
3634
3698
  "wrangler.json",
@@ -3790,7 +3854,7 @@ var astroConfigTemplate = (options) => {
3790
3854
  if (deployment.adapter === "cloudflare") {
3791
3855
  return resolveCloudflareAdapterArgs(context);
3792
3856
  }
3793
- return ADAPTER_OPTIONS[deployment.adapter] ?? "";
3857
+ return ADAPTER_OPTIONS.get(deployment.adapter) ?? "";
3794
3858
  })();
3795
3859
  const adapterExpr = deployment.adapter === "vercel" ? `withAdapterRoot(adapter(${adapterArgs}), ${JSON.stringify(adapterRoot(context))})` : `adapter(${adapterArgs})`;
3796
3860
  const adapterOption = server && deployment.adapter ? `
@@ -3817,13 +3881,21 @@ var astroConfigTemplate = (options) => {
3817
3881
  ])))},` : "";
3818
3882
  const fontEntries = buildFontEntries(config.theme.fonts);
3819
3883
  const fontsOption = fontEntries.length ? `
3820
- fonts: [${fontEntries.map((font) => font.kind === "local" ? `{ provider: fontProviders.local(), name: ${JSON.stringify(font.name)}, cssVariable: ${JSON.stringify(font.cssVariable)}, fallbacks: ${JSON.stringify(font.fallbacks)}, options: { variants: ${JSON.stringify(font.variants.map((variant) => ({
3821
- ...variant.weight === undefined ? {} : { weight: variant.weight },
3822
- ...variant.style === undefined ? {} : { style: variant.style },
3823
- src: [
3824
- isAbsolute(variant.src) ? variant.src : join8(context.root, variant.src)
3825
- ]
3826
- })))} } }` : `{ provider: fontProviders.${font.provider}(), name: ${JSON.stringify(font.name)}, cssVariable: ${JSON.stringify(font.cssVariable)}, weights: ${JSON.stringify(font.weights)}, fallbacks: ${JSON.stringify(font.fallbacks)} }`).join(", ")}],` : "";
3884
+ fonts: [${fontEntries.map((font) => font.kind === "local" ? `{ provider: fontProviders.local(), name: ${JSON.stringify(font.name)}, cssVariable: ${JSON.stringify(font.cssVariable)}, fallbacks: ${JSON.stringify(font.fallbacks)}, options: { variants: ${JSON.stringify(font.variants.map((variant) => {
3885
+ const face = {};
3886
+ if (variant.weight !== undefined) {
3887
+ face.weight = variant.weight;
3888
+ }
3889
+ if (variant.style !== undefined) {
3890
+ face.style = variant.style;
3891
+ }
3892
+ return {
3893
+ ...face,
3894
+ src: [
3895
+ isAbsolute(variant.src) ? variant.src : join8(context.root, variant.src)
3896
+ ]
3897
+ };
3898
+ }))} } }` : `{ provider: fontProviders.${font.provider}(), name: ${JSON.stringify(font.name)}, cssVariable: ${JSON.stringify(font.cssVariable)}, weights: ${JSON.stringify(font.weights)}, fallbacks: ${JSON.stringify(font.fallbacks)} }`).join(", ")}],` : "";
3827
3899
  const defineConfigImport = astroConfigImportLine({
3828
3900
  hasFonts: fontEntries.length > 0,
3829
3901
  hasSession: sessionOption.length > 0
@@ -3904,6 +3976,11 @@ ${userConfigSetup}export default defineConfig({
3904
3976
  },
3905
3977
  },
3906
3978
  devToolbar: { enabled: false },
3979
+ // Navigations are full document loads (no client router), so the next page's
3980
+ // HTML is fetched on hover/viewport to hide the request latency behind the
3981
+ // user's intent. Pairs with the cross-document view-transition rule in the
3982
+ // theme sheet, which smooths the swap itself.
3983
+ prefetch: { prefetchAll: true },
3907
3984
  vite: {
3908
3985
  plugins: [tailwindcss(), prerenderDepsPlugin(), serverAppResolvePlugin()],
3909
3986
  // Everything hydration can reach must be part of the dev dep optimizer's
@@ -4412,7 +4489,7 @@ export function GET({ props }: { props: { section: string } }) {
4412
4489
  });
4413
4490
  }
4414
4491
  `;
4415
- var ogEndpointTemplate = (customRoutes = [], og = {}) => `// Generated by Blume. Do not edit.
4492
+ var ogEndpointTemplate = (customRoutes = [], og = {}, includeChangelog = false) => `// Generated by Blume. Do not edit.
4416
4493
  import { renderOgImage } from "blume/og";
4417
4494
  import type { OgFont, OgFontFamilies } from "blume/og";
4418
4495
  import data from "blume:data";
@@ -4447,7 +4524,10 @@ export function getStaticPaths() {
4447
4524
  }
4448
4525
  for (const route of data.routes) {
4449
4526
  add(route.path === "/" ? "index" : route.path.slice(1), route.title);
4450
- }
4527
+ }${includeChangelog ? `
4528
+ // The generated changelog index is not a content route, so it needs its own
4529
+ // card. Added last: a custom page or content route owning /changelog wins.
4530
+ add("changelog", data.ui.changelog?.title ?? "Changelog");` : ""}
4451
4531
  return paths;
4452
4532
  }
4453
4533
 
@@ -4638,11 +4718,13 @@ export function getStaticPaths() {
4638
4718
  locale: route.locale,
4639
4719
  route: route.path,
4640
4720
  title: route.title,
4721
+ version: route.version,
4722
+ versionAlternates: route.versionAlternates,
4641
4723
  },
4642
4724
  }));
4643
4725
  }
4644
4726
 
4645
- const { entryId, collection, route, title, indexable, editUrl, lastModified, locale, alternates, fallback } = Astro.props;
4727
+ const { entryId, collection, route, title, indexable, editUrl, lastModified, locale, alternates, fallback, version, versionAlternates } = Astro.props;
4646
4728
  const entry = await getEntry(collection as CollectionKey, entryId);
4647
4729
  if (!entry) {
4648
4730
  return new Response(null, { status: 404 });
@@ -4674,9 +4756,6 @@ const ogGenerated = !seo.image && Boolean(ogPath);
4674
4756
  const x = { ...data.config.x, ...(seo.x?.creator ? { creator: seo.x.creator } : {}) };
4675
4757
 
4676
4758
  const basedRoute = withBase(route);
4677
- const canonical =
4678
- seo.canonical ??
4679
- (base ? \`\${base}\${basedRoute === "/" ? "" : encodeURI(basedRoute)}\` : null);
4680
4759
 
4681
4760
  // Locale resolution. With i18n on, pick the active locale's nav + dictionary,
4682
4761
  // build hreflang alternates, and derive the language-switcher targets.
@@ -4697,7 +4776,20 @@ const stripLocale = (path: string, codeArg: string) => {
4697
4776
  return prefix && path.startsWith(prefix) ? path.slice(prefix.length) || "/" : path;
4698
4777
  };
4699
4778
 
4700
- const navigation = i18n ? (data.navigationByLocale[locale] ?? data.navigation) : data.navigation;
4779
+ // Version resolution. An archived page renders its snapshot's navigation tree,
4780
+ // points its canonical at the latest equivalent (unless configured otherwise),
4781
+ // and shows the old-version notice.
4782
+ const versionsConfig = data.config.versions;
4783
+ const archived = versionsConfig && version
4784
+ ? (versionsConfig.archived.find((v) => v.id === version) ?? null)
4785
+ : null;
4786
+ const latestVersionAlt = (versionAlternates ?? []).find((alt) => alt.version === "");
4787
+
4788
+ const navigation = version
4789
+ ? (data.navigationByVersion[version]?.[i18n ? locale : ""] ?? data.navigation)
4790
+ : i18n
4791
+ ? (data.navigationByLocale[locale] ?? data.navigation)
4792
+ : data.navigation;
4701
4793
  const ui = i18n ? (data.uiByLocale[locale] ?? data.ui) : data.ui;
4702
4794
  const localeMeta = i18n ? i18n.locales.find((l) => l.code === locale) : null;
4703
4795
  const dir = localeMeta?.dir ?? "ltr";
@@ -4709,10 +4801,22 @@ const contentLocale =
4709
4801
  const contentDir = i18n
4710
4802
  ? (i18n.locales.find((l) => l.code === contentLocale)?.dir ?? "ltr")
4711
4803
  : "ltr";
4712
- const absolute = (path: string) => {
4713
- const p = withBase(path);
4714
- return base + (p === "/" ? "" : p);
4715
- };
4804
+ // The root route keeps its trailing slash (\`https://site/\`) so canonical and
4805
+ // hreflang URLs byte-match the sitemap's <loc> for the home page.
4806
+ const absolute = (path: string) => base + withBase(path);
4807
+
4808
+ // An archived page defaults its canonical to the same page in the latest docs
4809
+ // when that page still exists — search engines treat the live page as
4810
+ // authoritative without deindexing version-only content. A page's own
4811
+ // \`seo.canonical\` always wins, and \`canonical: "self"\` keeps the default.
4812
+ const canonical =
4813
+ seo.canonical ??
4814
+ (archived && archived.canonical === "latest" && latestVersionAlt && base
4815
+ ? absolute(latestVersionAlt.path)
4816
+ : base
4817
+ ? \`\${base}\${basedRoute === "/" ? "/" : encodeURI(basedRoute)}\`
4818
+ : null);
4819
+ const effectiveNoindex = Boolean(seo.noindex) || (archived?.noindex ?? false);
4716
4820
 
4717
4821
  const localeAlternates =
4718
4822
  i18n && base
@@ -4736,6 +4840,72 @@ const localeSwitch = i18n
4736
4840
  })
4737
4841
  : [];
4738
4842
 
4843
+ // Version switcher + old-version notice. The switcher auto-populates from the
4844
+ // versions config as a \`kind: "version"\` selector; a user-declared version
4845
+ // selector in \`navigation.selectors\` suppresses it (theirs renders instead).
4846
+ // Fallback version roots compose like real routes — \`{basePath}/{locale?}/{id}\`
4847
+ // (manifest \`versionAlternates\` paths arrive with the base already applied).
4848
+ const versionRootFor = (id: string) => {
4849
+ const logical = id ? \`/\${id}\` : "/";
4850
+ const localized = i18n ? localizeRoute(logical, locale) : logical;
4851
+ const mount = data.config.basePath;
4852
+ if (!mount) {
4853
+ return localized;
4854
+ }
4855
+ return localized === "/" ? mount : \`\${mount}\${localized}\`;
4856
+ };
4857
+ const samePageSwitch = versionsConfig
4858
+ ? versionsConfig.switcher.redirect === "same-page"
4859
+ : true;
4860
+ const userHasVersionSelector = navigation.selectors.some(
4861
+ (selector) => selector.kind === "version"
4862
+ );
4863
+ const versionSelector =
4864
+ versionsConfig && !userHasVersionSelector
4865
+ ? {
4866
+ items: [
4867
+ {
4868
+ id: "",
4869
+ label: versionsConfig.current.label,
4870
+ tag: versionsConfig.current.badge,
4871
+ },
4872
+ ...versionsConfig.archived.map((v) => ({
4873
+ id: v.id,
4874
+ label: v.label ?? v.id,
4875
+ tag: undefined,
4876
+ })),
4877
+ ].map((entry) => {
4878
+ const alt = (versionAlternates ?? []).find(
4879
+ (a) => a.version === entry.id
4880
+ );
4881
+ return {
4882
+ label: entry.label,
4883
+ path: samePageSwitch && alt ? alt.path : versionRootFor(entry.id),
4884
+ ...(entry.tag ? { tag: entry.tag } : {}),
4885
+ };
4886
+ }),
4887
+ kind: "version" as const,
4888
+ label: ui.versions.switcher,
4889
+ }
4890
+ : null;
4891
+
4892
+ const versionNotice =
4893
+ archived && archived.banner !== false
4894
+ ? {
4895
+ latestHref: latestVersionAlt
4896
+ ? latestVersionAlt.path
4897
+ : versionRootFor(""),
4898
+ latestLabel: ui.versions.latest,
4899
+ message:
4900
+ typeof archived.banner === "string"
4901
+ ? archived.banner
4902
+ : ui.versions.notice.replace(
4903
+ "{version}",
4904
+ archived.label ?? archived.id
4905
+ ),
4906
+ }
4907
+ : null;
4908
+
4739
4909
  // The whole page shell is overridable via \`layout.Layout\`; it receives the same
4740
4910
  // props as the built-in RootLayout, plus the \`layout\` map for its inner slots.
4741
4911
  const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
@@ -4760,6 +4930,9 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
4760
4930
  localeAlternates={localeAlternates}
4761
4931
  xDefault={xDefault}
4762
4932
  localeSwitch={localeSwitch}
4933
+ versionSelector={versionSelector}
4934
+ versionNotice={versionNotice}
4935
+ searchVersion={versionsConfig ? version : null}
4763
4936
  page={{ title: seo.title ?? title, description: seo.description ?? frontmatter.description, route }}
4764
4937
  headings={headings}
4765
4938
  toc={data.config.toc}
@@ -4782,7 +4955,7 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
4782
4955
  pageType={frontmatter.type}
4783
4956
  published={frontmatter.date ?? frontmatter.changelog?.date ?? null}
4784
4957
  lastModified={lastModified}
4785
- noindex={seo.noindex}
4958
+ noindex={effectiveNoindex}
4786
4959
  structuredDataEnabled={data.config.structuredData}
4787
4960
  >
4788
4961
  <h1>{title}</h1>
@@ -4935,6 +5108,12 @@ const base = data.config.site ? data.config.site.replace(/\\/$/, "") : null;
4935
5108
  const basedRoute = withBase("/changelog");
4936
5109
  const canonical = base ? base + basedRoute : null;
4937
5110
 
5111
+ // The generated OG card for this route (the /og endpoint emits it alongside
5112
+ // the content-route cards), absolutized like the catch-all's so crawlers get
5113
+ // a full URL when the site is known.
5114
+ const ogPath = data.config.og.enabled ? withBase("/og/changelog.png") : null;
5115
+ const ogImage = ogPath && base ? base + ogPath : ogPath;
5116
+
4938
5117
  // The page chrome (h1, title, description) comes from the same translatable
4939
5118
  // \`changelog\` group as the reveal button; optional chaining tolerates a
4940
5119
  // not-yet-regenerated data snapshot from before these keys existed.
@@ -4942,7 +5121,10 @@ const changelogTitle = data.ui.changelog?.title ?? "Changelog";
4942
5121
  const changelogDescription =
4943
5122
  data.ui.changelog?.description ??
4944
5123
  "Product updates, new features, and fixes from every release.";
4945
- const pageTitle = data.config.title + " " + changelogTitle;
5124
+ // The layout suffixes "- {site title}" itself, so the page title is just the
5125
+ // changelog's own name — prefixing the site title too would double it
5126
+ // ("Acme Changelog - Acme").
5127
+ const pageTitle = changelogTitle;
4946
5128
 
4947
5129
  const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
4948
5130
  ---
@@ -4974,7 +5156,8 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
4974
5156
  fontCssVars={data.fontCssVars}
4975
5157
  searchEnabled={data.config.search.enabled}
4976
5158
  indexable={true}
4977
- ogImage={null}
5159
+ ogImage={ogImage}
5160
+ ogGenerated={Boolean(ogImage)}
4978
5161
  x={data.config.x}
4979
5162
  canonical={canonical}
4980
5163
  exportPdf={${options.exportPdf}}
@@ -5467,6 +5650,9 @@ var sitemapParser = new XMLParser({
5467
5650
  parseTagValue: false,
5468
5651
  removeNSPrefix: true
5469
5652
  });
5653
+ var isUrlsetElement = (value) => typeof value === "object" && value !== null;
5654
+ var isUrlEntry = (value) => typeof value === "object" && value !== null;
5655
+ var isText = (value) => typeof value === "string";
5470
5656
  var parseSitemap = (file, xml, bytes) => {
5471
5657
  const doc = { bytes, file, lastmod: new Map, urls: [] };
5472
5658
  let parsed;
@@ -5480,19 +5666,19 @@ var parseSitemap = (file, xml, bytes) => {
5480
5666
  doc.error = Object.hasOwn(parsed, "sitemapindex") ? "sitemap is an index, not a urlset" : "no <urlset> element";
5481
5667
  return doc;
5482
5668
  }
5483
- const urlset = parsed.urlset;
5484
- const entries = typeof urlset === "object" && urlset !== null ? [urlset.url].flat() : [];
5669
+ const { urlset } = parsed;
5670
+ const entries = isUrlsetElement(urlset) ? [urlset.url].flat() : [];
5485
5671
  for (const entry of entries) {
5486
- if (typeof entry !== "object" || entry === null) {
5672
+ if (!isUrlEntry(entry)) {
5487
5673
  continue;
5488
5674
  }
5489
5675
  const { loc, lastmod } = entry;
5490
- const locText = typeof loc === "string" ? loc.trim() : "";
5676
+ const locText = isText(loc) ? loc.trim() : "";
5491
5677
  if (!locText) {
5492
5678
  continue;
5493
5679
  }
5494
5680
  doc.urls.push(locText);
5495
- if (typeof lastmod === "string" && lastmod.trim() !== "") {
5681
+ if (isText(lastmod) && lastmod.trim() !== "") {
5496
5682
  doc.lastmod?.set(locText, lastmod.trim());
5497
5683
  }
5498
5684
  }
@@ -5740,6 +5926,7 @@ import { relative as relative10 } from "pathe";
5740
5926
 
5741
5927
  // src/core/config.ts
5742
5928
  import { existsSync as existsSync6, readFileSync as readFileSync3 } from "node:fs";
5929
+ import { z as z3 } from "zod";
5743
5930
 
5744
5931
  // src/core/load-module.ts
5745
5932
  import { createJiti } from "jiti";
@@ -5795,8 +5982,9 @@ var resolveProjectContext = (root, config, options) => {
5795
5982
  import { z as z2 } from "zod";
5796
5983
 
5797
5984
  // src/seo/x-handle.ts
5985
+ var isString = (value) => typeof value === "string";
5798
5986
  var normalizeXHandle = (value) => {
5799
- if (typeof value !== "string") {
5987
+ if (!isString(value)) {
5800
5988
  return;
5801
5989
  }
5802
5990
  const handle = value.trim().replace(/^@+/u, "");
@@ -9377,6 +9565,7 @@ var uiStringsObject = z.object({
9377
9565
  search: z.object({
9378
9566
  all: z.string().default("All"),
9379
9567
  allLanguages: z.string().default("All languages"),
9568
+ allVersions: z.string().default("All versions"),
9380
9569
  askAi: z.string().default("Ask AI"),
9381
9570
  askAiHint: z.string().default("Get an instant answer from AI"),
9382
9571
  button: z.string().default("Search"),
@@ -9393,6 +9582,11 @@ var uiStringsObject = z.object({
9393
9582
  }).prefault({}),
9394
9583
  toc: z.object({
9395
9584
  title: z.string().default("On this page")
9585
+ }).prefault({}),
9586
+ versions: z.object({
9587
+ latest: z.string().default("Go to latest"),
9588
+ notice: z.string().default("You're viewing documentation for {version}. It may be out of date."),
9589
+ switcher: z.string().default("Version")
9396
9590
  }).prefault({})
9397
9591
  });
9398
9592
  var uiStringsSchema = uiStringsObject.prefault({});
@@ -9433,12 +9627,16 @@ var resolveUIStrings = (locale, options) => {
9433
9627
  var isStandardSchema = (value) => typeof value === "object" && value !== null && typeof value["~standard"]?.validate === "function";
9434
9628
 
9435
9629
  // src/core/schema.ts
9630
+ var isString2 = (value) => typeof value === "string";
9631
+ var isBoolean = (value) => typeof value === "boolean";
9436
9632
  var iconName = z2.string().min(1);
9437
9633
  var DEFAULT_CONTENT_GLOB = "**/*.{md,mdx}";
9438
9634
  var hydrationMode = z2.enum(["load", "idle", "visible", "media", "only"]);
9439
9635
  var dateSchema = z2.union([z2.string(), z2.date()]).transform((value) => value instanceof Date ? value.toISOString() : value);
9636
+ var sidebarDisplaySchema = z2.enum(["flat", "group", "page"]);
9440
9637
  var sidebarMetaSchema = z2.strictObject({
9441
9638
  badge: z2.string().optional(),
9639
+ display: sidebarDisplaySchema.optional(),
9442
9640
  hidden: z2.boolean().default(false),
9443
9641
  icon: iconName.optional(),
9444
9642
  label: z2.string().optional(),
@@ -9495,11 +9693,12 @@ var pageMetaBaseSchema = z2.strictObject({
9495
9693
  type: z2.string().optional()
9496
9694
  });
9497
9695
  var pageMetaSchema = pageMetaBaseSchema;
9696
+ var BUILT_IN_PAGE_META_KEYS = new Set(pageMetaBaseSchema.keyof().options);
9498
9697
  var customKeySchemaRecord = (where) => z2.record(z2.string(), z2.custom(isStandardSchema, {
9499
9698
  message: "Expected a Standard Schema (e.g. a Zod schema — any Zod version works)."
9500
9699
  })).default({}).superRefine((value, ctx) => {
9501
9700
  for (const key of Object.keys(value)) {
9502
- if (Object.hasOwn(pageMetaBaseSchema.shape, key)) {
9701
+ if (BUILT_IN_PAGE_META_KEYS.has(key)) {
9503
9702
  ctx.addIssue({
9504
9703
  code: z2.ZodIssueCode.custom,
9505
9704
  message: `"${key}" is a built-in frontmatter field and cannot be redeclared via ${where}.`,
@@ -9508,9 +9707,9 @@ var customKeySchemaRecord = (where) => z2.record(z2.string(), z2.custom(isStanda
9508
9707
  }
9509
9708
  }
9510
9709
  });
9511
- var sidebarDisplaySchema = z2.enum(["flat", "group", "page"]);
9512
9710
  var folderMetaSchema = z2.strictObject({
9513
9711
  collapsed: z2.boolean().optional(),
9712
+ display: sidebarDisplaySchema.optional(),
9514
9713
  icon: iconName.optional(),
9515
9714
  order: z2.number().optional(),
9516
9715
  pages: z2.array(z2.string()).optional(),
@@ -9604,7 +9803,7 @@ var githubReleasesSourceSchema = z2.strictObject({
9604
9803
  type: z2.literal("github-releases")
9605
9804
  });
9606
9805
  var customSourceSchema = z2.object({
9607
- source: z2.custom((val) => typeof val === "object" && val !== null && typeof val.load === "function" && typeof val.name === "string", { message: "custom source must be a ContentSource (with name + load)" }),
9806
+ source: z2.custom((val) => typeof val === "object" && val !== null && ("load" in val) && typeof val.load === "function" && ("name" in val) && typeof val.name === "string", { message: "custom source must be a ContentSource (with name + load)" }),
9608
9807
  type: z2.literal("custom")
9609
9808
  });
9610
9809
  var contentSourceSchema = z2.discriminatedUnion("type", [
@@ -9695,7 +9894,7 @@ var localFontSchema = z2.strictObject({
9695
9894
  variants: z2.array(localFontVariantSchema).nonempty()
9696
9895
  });
9697
9896
  var fontValueSchema = z2.union([z2.string(), remoteFontSchema, localFontSchema]).superRefine((value, ctx) => {
9698
- if (typeof value === "string" && !isFontSlug(value)) {
9897
+ if (isString2(value) && !isFontSlug(value)) {
9699
9898
  ctx.addIssue({
9700
9899
  code: z2.ZodIssueCode.custom,
9701
9900
  message: `Unknown font "${value}". Supported fonts: ${FONT_SLUGS.join(", ")}. For any other family, use the object form: { name: "..." } (remote provider) or { name: "...", variants: [...] } (local files).`
@@ -9708,18 +9907,18 @@ var perModeValueSchema = z2.union([
9708
9907
  dark: z2.string().optional(),
9709
9908
  light: z2.string().optional()
9710
9909
  })
9711
- ]).optional().transform((value) => typeof value === "string" ? { dark: value, light: value } : value);
9910
+ ]).optional().transform((value) => isString2(value) ? { dark: value, light: value } : value);
9712
9911
  var themeConfigSchema = z2.strictObject({
9713
9912
  accent: z2.union([
9714
9913
  z2.string(),
9715
9914
  z2.strictObject({ dark: z2.string(), light: z2.string() })
9716
- ]).default("blue").transform((value) => typeof value === "string" ? { dark: value, light: value } : value),
9915
+ ]).default("blue").transform((value) => isString2(value) ? { dark: value, light: value } : value),
9717
9916
  action: z2.string().optional(),
9718
9917
  background: perModeValueSchema,
9719
9918
  backgroundImage: perModeValueSchema,
9720
9919
  fonts: z2.strictObject({
9721
9920
  body: fontValueSchema.default("inter"),
9722
- display: fontValueSchema.default("inter-tight"),
9921
+ display: fontValueSchema.default("inter"),
9723
9922
  mono: fontValueSchema.default("ibm-plex-mono")
9724
9923
  }).prefault({}),
9725
9924
  layout: z2.enum(["sidebar"]).default("sidebar"),
@@ -9796,7 +9995,7 @@ var askAiProviders = [
9796
9995
  ];
9797
9996
  var PRIVATE_JWK_PARAMS = ["d", "p", "q", "dp", "dq", "qi", "oth", "k"];
9798
9997
  var publicJwkSchema = z2.record(z2.string(), z2.unknown()).superRefine((jwk, ctx) => {
9799
- if (typeof jwk.kty !== "string" || jwk.kty.length === 0) {
9998
+ if (!isString2(jwk.kty) || jwk.kty.length === 0) {
9800
9999
  ctx.addIssue({
9801
10000
  code: z2.ZodIssueCode.custom,
9802
10001
  message: 'A JWK must declare its key type ("kty").'
@@ -9857,7 +10056,7 @@ var aiConfigSchema = z2.strictObject({
9857
10056
  enabled: z2.boolean().default(true),
9858
10057
  openapi: z2.boolean().default(true)
9859
10058
  })
9860
- ]).default(true).transform((value) => typeof value === "boolean" ? { enabled: value, openapi: true } : value),
10059
+ ]).default(true).transform((value) => isBoolean(value) ? { enabled: value, openapi: true } : value),
9861
10060
  markdownComponents: z2.record(z2.string(), z2.custom((value) => typeof value === "function", {
9862
10061
  message: "Expected a serializer function."
9863
10062
  })).default({}),
@@ -9892,7 +10091,7 @@ var exportConfigSchema = z2.union([
9892
10091
  epub: z2.boolean().default(false),
9893
10092
  pdf: z2.boolean().default(false)
9894
10093
  })
9895
- ]).transform((value) => typeof value === "boolean" ? { epub: value, pdf: value } : value);
10094
+ ]).transform((value) => isBoolean(value) ? { epub: value, pdf: value } : value);
9896
10095
  var localeSchema = z2.strictObject({
9897
10096
  code: z2.string().min(1),
9898
10097
  dir: z2.enum(["ltr", "rtl"]).default("ltr"),
@@ -9923,6 +10122,36 @@ var i18nConfigSchema = z2.strictObject({
9923
10122
  });
9924
10123
  }
9925
10124
  });
10125
+ var VERSION_ID = /^[A-Za-z][\w.-]*$/u;
10126
+ var archivedVersionSchema = z2.strictObject({
10127
+ banner: z2.union([z2.boolean(), z2.string()]).default(true),
10128
+ canonical: z2.enum(["latest", "self"]).default("latest"),
10129
+ id: z2.string().regex(VERSION_ID, 'Version ids must start with a letter (e.g. "v1.0") and contain only letters, digits, dots, hyphens, and underscores.'),
10130
+ label: z2.string().optional(),
10131
+ noindex: z2.boolean().default(false)
10132
+ });
10133
+ var versionsConfigSchema = z2.strictObject({
10134
+ archived: z2.array(archivedVersionSchema).default([]),
10135
+ current: z2.strictObject({
10136
+ badge: z2.string().optional(),
10137
+ label: z2.string()
10138
+ }),
10139
+ switcher: z2.strictObject({
10140
+ redirect: z2.enum(["same-page", "root"]).default("same-page")
10141
+ }).prefault({})
10142
+ }).superRefine((value, ctx) => {
10143
+ const seen = new Set;
10144
+ for (const [position, version] of value.archived.entries()) {
10145
+ if (seen.has(version.id)) {
10146
+ ctx.addIssue({
10147
+ code: z2.ZodIssueCode.custom,
10148
+ message: `versions.archived declares "${version.id}" more than once.`,
10149
+ path: ["archived", position, "id"]
10150
+ });
10151
+ }
10152
+ seen.add(version.id);
10153
+ }
10154
+ });
9926
10155
  var analyticsScriptSchema = z2.strictObject({
9927
10156
  attributes: z2.record(z2.string(), z2.string()).optional(),
9928
10157
  content: z2.string().optional(),
@@ -10036,18 +10265,18 @@ var githubConfigSchema = z2.strictObject({
10036
10265
  owner: z2.string(),
10037
10266
  repo: z2.string()
10038
10267
  });
10268
+ var isThemeObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
10039
10269
  var codeThemeSchema = z2.custom((value) => {
10040
- if (typeof value === "string") {
10270
+ if (isString2(value)) {
10041
10271
  return true;
10042
10272
  }
10043
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
10273
+ if (!isThemeObject(value)) {
10044
10274
  return false;
10045
10275
  }
10046
- const theme = value;
10047
- const settingsValid = theme.settings === undefined || Array.isArray(theme.settings);
10048
- const tokenColorsValid = theme.tokenColors === undefined || Array.isArray(theme.tokenColors);
10049
- const colorsValid = theme.colors === undefined || typeof theme.colors === "object" && theme.colors !== null && !Array.isArray(theme.colors);
10050
- const hasContent = theme.settings !== undefined || theme.tokenColors !== undefined || theme.colors !== undefined;
10276
+ const settingsValid = value.settings === undefined || Array.isArray(value.settings);
10277
+ const tokenColorsValid = value.tokenColors === undefined || Array.isArray(value.tokenColors);
10278
+ const colorsValid = value.colors === undefined || isThemeObject(value.colors);
10279
+ const hasContent = value.settings !== undefined || value.tokenColors !== undefined || value.colors !== undefined;
10051
10280
  return settingsValid && tokenColorsValid && colorsValid && hasContent;
10052
10281
  }, "Expected a Shiki theme name or custom theme object");
10053
10282
  var codeBlockThemeSchema = z2.strictObject({
@@ -10063,7 +10292,7 @@ var examplesConfigSchema = z2.union([
10063
10292
  css: z2.string().optional(),
10064
10293
  source: z2.string().default("examples")
10065
10294
  })
10066
- ]).transform((value) => typeof value === "string" ? { source: value } : value);
10295
+ ]).transform((value) => isString2(value) ? { source: value } : value);
10067
10296
  var lastModifiedConfigSchema = z2.union([
10068
10297
  z2.boolean(),
10069
10298
  z2.strictObject({ type: z2.enum(["git", "frontmatter"]).default("git") })
@@ -10103,24 +10332,24 @@ var openapiSourceSchema = z2.strictObject({
10103
10332
  spec: z2.string()
10104
10333
  });
10105
10334
  var scalarConfigSchema = z2.record(z2.string(), z2.unknown()).optional();
10106
- var openapiConfigSchema = z2.strictObject({
10107
- codeSamples: z2.array(z2.string()).default(["curl", "js", "python"]),
10335
+ var referenceConfigSchema = (defaults) => z2.strictObject({
10336
+ codeSamples: z2.array(z2.string()).default(defaults.codeSamples),
10108
10337
  enabled: z2.boolean().default(false),
10109
10338
  expandSchemas: z2.boolean().default(false),
10110
10339
  renderer: z2.enum(["blume", "scalar"]).default("blume"),
10111
- route: z2.string().default("/reference"),
10340
+ route: z2.string().default(defaults.route),
10112
10341
  scalar: scalarConfigSchema,
10113
10342
  sources: z2.array(openapiSourceSchema).default([]),
10114
10343
  spec: z2.string().optional(),
10115
10344
  theme: z2.string().optional()
10116
10345
  });
10117
- var asyncapiConfigSchema = z2.strictObject({
10118
- enabled: z2.boolean().default(false),
10119
- route: z2.string().default("/events"),
10120
- scalar: scalarConfigSchema,
10121
- sources: z2.array(openapiSourceSchema).default([]),
10122
- spec: z2.string().optional(),
10123
- theme: z2.string().optional()
10346
+ var openapiConfigSchema = referenceConfigSchema({
10347
+ codeSamples: ["curl", "js", "python"],
10348
+ route: "/reference"
10349
+ });
10350
+ var asyncapiConfigSchema = referenceConfigSchema({
10351
+ codeSamples: [],
10352
+ route: "/events"
10124
10353
  });
10125
10354
  var frontmatterConfigSchema = z2.strictObject({
10126
10355
  extend: customKeySchemaRecord("frontmatter.extend")
@@ -10132,7 +10361,7 @@ var tocConfigSchema = z2.union([
10132
10361
  minHeadingLevel: z2.number().int().min(1).max(6).optional()
10133
10362
  })
10134
10363
  ]).default(true).transform((value) => {
10135
- if (typeof value === "boolean") {
10364
+ if (isBoolean(value)) {
10136
10365
  return { enabled: value, maxLevel: 3, minLevel: 2 };
10137
10366
  }
10138
10367
  return {
@@ -10172,8 +10401,21 @@ var blumeConfigSchema = z2.strictObject({
10172
10401
  seo: seoConfigSchema.prefault({}),
10173
10402
  theme: themeConfigSchema.prefault({}),
10174
10403
  title: z2.string().default("Documentation"),
10175
- toc: tocConfigSchema
10404
+ toc: tocConfigSchema,
10405
+ versions: versionsConfigSchema.optional()
10176
10406
  }).superRefine((config, ctx) => {
10407
+ if (config.versions && config.i18n) {
10408
+ const localeCodes = new Set(config.i18n.locales.map((locale) => locale.code.toLowerCase()));
10409
+ for (const [position, version] of config.versions.archived.entries()) {
10410
+ if (localeCodes.has(version.id.toLowerCase())) {
10411
+ ctx.addIssue({
10412
+ code: z2.ZodIssueCode.custom,
10413
+ message: `Version id "${version.id}" is also a configured locale code — rename the version (e.g. "v${version.id}").`,
10414
+ path: ["versions", "archived", position, "id"]
10415
+ });
10416
+ }
10417
+ }
10418
+ }
10177
10419
  for (const [typeName, typeConfig] of Object.entries(config.content.types)) {
10178
10420
  for (const key of Object.keys(typeConfig.frontmatter)) {
10179
10421
  if (Object.hasOwn(config.frontmatter.extend, key)) {
@@ -10198,9 +10440,12 @@ var blumeConfigSchema = z2.strictObject({
10198
10440
 
10199
10441
  // src/core/config.ts
10200
10442
  var importConfigModule = createModuleLoader();
10443
+ var themeFontsProbeSchema = z3.looseObject({
10444
+ theme: z3.looseObject({ fonts: z3.unknown() }).optional()
10445
+ });
10201
10446
  var loadConfig = async (root, options = {}) => {
10202
10447
  const configFile = findConfigFile(root);
10203
- let raw = {};
10448
+ let raw;
10204
10449
  if (configFile) {
10205
10450
  try {
10206
10451
  raw = await importConfigModule(configFile);
@@ -10213,7 +10458,8 @@ var loadConfig = async (root, options = {}) => {
10213
10458
  });
10214
10459
  }
10215
10460
  }
10216
- const themeFontsConfigured = Boolean(raw && typeof raw === "object" && raw.theme?.fonts !== undefined);
10461
+ const probe2 = themeFontsProbeSchema.safeParse(raw);
10462
+ const themeFontsConfigured = probe2.success && probe2.data.theme?.fonts !== undefined;
10217
10463
  const parsed = blumeConfigSchema.safeParse(raw ?? {});
10218
10464
  if (!parsed.success) {
10219
10465
  const source = configFile && existsSync6(configFile) ? readFileSync3(configFile, "utf-8") : undefined;
@@ -10320,12 +10566,14 @@ var localeTargetPath = (rel, ext, locale, i18n) => {
10320
10566
  }
10321
10567
  return `${locale}/${rel}`;
10322
10568
  };
10323
- var i18nDiagnostics = (pages, i18n) => {
10569
+ var i18nDiagnostics = (pages, i18n, versions) => {
10324
10570
  const configured = new Set(i18n.locales.map((locale) => locale.code.toLowerCase()));
10571
+ const versionDirs = new Set(versions?.archived.map((version) => version.id));
10325
10572
  const seen = new Set;
10326
10573
  const diagnostics = [];
10327
10574
  for (const page of pages) {
10328
- const first = page.source.ref.split("/")[0]?.toLowerCase();
10575
+ const parts = page.source.ref.split("/");
10576
+ const first = (parts[0] && versionDirs.has(parts[0]) ? parts[1] : parts[0])?.toLowerCase();
10329
10577
  if (first && !seen.has(first) && KNOWN_LOCALES.has(first) && !configured.has(first)) {
10330
10578
  seen.add(first);
10331
10579
  diagnostics.push({
@@ -10381,7 +10629,7 @@ var resolveIcon = (name) => {
10381
10629
  var hasIcon = (name) => resolveIcon(name) !== null;
10382
10630
 
10383
10631
  // src/core/nav-diagnostics.ts
10384
- var ICON_SHAPE_HINT = "Use a built-in icon name, an image path/URL, or inline SVG markup.";
10632
+ var ICON_FORMAT_HINT = "Use a built-in icon name, an image path/URL, or inline SVG markup.";
10385
10633
  var flattenNodes = (nodes) => nodes.flatMap((node) => node.kind === "group" ? [node, ...flattenNodes(node.children)] : [node]);
10386
10634
  var collectIcons = (navigation) => {
10387
10635
  const icons = [];
@@ -10430,10 +10678,10 @@ var unknownIconDiagnostics = (icons, suggestion) => {
10430
10678
  }
10431
10679
  return diagnostics;
10432
10680
  };
10433
- var validateNavIcons = (navigation) => unknownIconDiagnostics(collectIcons(navigation), ICON_SHAPE_HINT);
10681
+ var validateNavIcons = (navigation) => unknownIconDiagnostics(collectIcons(navigation), ICON_FORMAT_HINT);
10434
10682
  var validateSearchPopularIcons = (popular) => {
10435
10683
  const icons = popular.flatMap((link) => link.icon ? [{ icon: link.icon, where: `popular link "${link.label}"` }] : []);
10436
- return unknownIconDiagnostics(icons, ICON_SHAPE_HINT);
10684
+ return unknownIconDiagnostics(icons, ICON_FORMAT_HINT);
10437
10685
  };
10438
10686
  var resolvesToPages = (routes, path) => routes.has(path) || [...routes].some((route) => route.startsWith(`${path}/`));
10439
10687
  var validateNavTargets = (navigation, routes) => {
@@ -10537,6 +10785,8 @@ var segmentKey = (raw) => {
10537
10785
  return (group ?? raw).replace(NUMERIC_PREFIX, "");
10538
10786
  };
10539
10787
  var isIndexStem = (stem) => stem.replace(NUMERIC_PREFIX, "") === "index";
10788
+ var stemOf = (filename) => filename.replace(extname(filename), "");
10789
+ var navStem = (navPath) => stemOf(navPath.split("/").at(-1) ?? navPath);
10540
10790
  var createGroup = (key, path, label, order) => ({
10541
10791
  children: [],
10542
10792
  index: new Map,
@@ -10561,7 +10811,7 @@ var pageOrder = (page, filename) => {
10561
10811
  if (page.meta.sidebar.order !== undefined) {
10562
10812
  return { order: page.meta.sidebar.order, orderIsAuthored: true };
10563
10813
  }
10564
- if (isIndexStem(filename.replace(extname(filename), ""))) {
10814
+ if (isIndexStem(stemOf(filename))) {
10565
10815
  return { order: Number.NEGATIVE_INFINITY, orderIsAuthored: false };
10566
10816
  }
10567
10817
  if (page.contentType === "changelog") {
@@ -10583,8 +10833,9 @@ var metaKey = (path, metaPrefix) => {
10583
10833
  }
10584
10834
  return path ? `${metaPrefix}/${path}` : metaPrefix;
10585
10835
  };
10586
- var applyFolderMeta = (group, folderMeta, sharedMeta, metaPrefix) => {
10587
- const meta = folderMeta.get(metaKey(group.path, metaPrefix)) ?? sharedMeta.get(group.path);
10836
+ var applyFolderMeta = (group, folderMeta, sharedMeta, metaPrefix, sharedMetaPrefix, indexDisplay) => {
10837
+ const meta = folderMeta.get(metaKey(group.path, metaPrefix)) ?? sharedMeta.get(metaKey(group.path, sharedMetaPrefix));
10838
+ group.display = indexDisplay.get(group.path) ?? meta?.display;
10588
10839
  if (meta) {
10589
10840
  group.label = meta.title ?? group.label;
10590
10841
  group.icon = meta.icon ?? group.icon;
@@ -10605,15 +10856,15 @@ var applyFolderMeta = (group, folderMeta, sharedMeta, metaPrefix) => {
10605
10856
  }
10606
10857
  for (const child of group.children) {
10607
10858
  if (child.kind === "group") {
10608
- applyFolderMeta(child, folderMeta, sharedMeta, metaPrefix);
10859
+ applyFolderMeta(child, folderMeta, sharedMeta, metaPrefix, sharedMetaPrefix, indexDisplay);
10609
10860
  }
10610
10861
  }
10611
10862
  };
10612
- var indexTitleMismatchDiagnostic = (page, folderPath, folderMeta, sharedMeta, metaPrefix) => {
10863
+ var indexTitleMismatchDiagnostic = (page, folderPath, folderMeta, sharedMeta, metaPrefix, sharedMetaPrefix) => {
10613
10864
  if (!page.meta.title || page.fallback || folderPath === "") {
10614
10865
  return;
10615
10866
  }
10616
- const meta = folderMeta.get(metaKey(folderPath, metaPrefix)) ?? sharedMeta.get(folderPath);
10867
+ const meta = folderMeta.get(metaKey(folderPath, metaPrefix)) ?? sharedMeta.get(metaKey(folderPath, sharedMetaPrefix));
10617
10868
  if (!meta?.title || meta.title === page.title) {
10618
10869
  return;
10619
10870
  }
@@ -10625,6 +10876,46 @@ var indexTitleMismatchDiagnostic = (page, folderPath, folderMeta, sharedMeta, me
10625
10876
  suggestion: `Update the page's frontmatter title to match ("${meta.title}"), or leave it if the divergence is intentional.`
10626
10877
  };
10627
10878
  };
10879
+ var sidebarDisplayIgnoredDiagnostics = (pages, explicitSidebar) => {
10880
+ const diagnostics = [];
10881
+ for (const page of pages) {
10882
+ if (!page.meta.sidebar.display) {
10883
+ continue;
10884
+ }
10885
+ const isIndex = isIndexStem(navStem(page.navPath));
10886
+ if (explicitSidebar) {
10887
+ diagnostics.push({
10888
+ code: "BLUME_SIDEBAR_DISPLAY_IGNORED",
10889
+ file: page.sourcePath ?? page.id,
10890
+ message: `"${page.navPath}" sets sidebar.display, but the explicit navigation.sidebar config owns each group's display mode — the value is ignored.`,
10891
+ severity: "warning",
10892
+ suggestion: "Set display on the matching group in navigation.sidebar, or remove the frontmatter key."
10893
+ });
10894
+ continue;
10895
+ }
10896
+ if (isIndex && !page.navPath.includes("/")) {
10897
+ diagnostics.push({
10898
+ code: "BLUME_SIDEBAR_DISPLAY_IGNORED",
10899
+ file: page.sourcePath ?? page.id,
10900
+ message: `"${page.navPath}" sets sidebar.display, but the content root is not a sidebar group — the value is ignored.`,
10901
+ severity: "warning",
10902
+ suggestion: "Set navigation.sidebar.display in blume.config to change the sidebar-wide mode."
10903
+ });
10904
+ continue;
10905
+ }
10906
+ if (isIndex) {
10907
+ continue;
10908
+ }
10909
+ diagnostics.push({
10910
+ code: "BLUME_SIDEBAR_DISPLAY_IGNORED",
10911
+ file: page.sourcePath ?? page.id,
10912
+ message: `"${page.navPath}" sets sidebar.display, but only a folder's index page can set its group's display mode — the value is ignored.`,
10913
+ severity: "warning",
10914
+ suggestion: "Move display to the folder's index page frontmatter, or set it in the folder's meta.ts."
10915
+ });
10916
+ }
10917
+ return diagnostics;
10918
+ };
10628
10919
  var isAuthoredOrder = (node) => node.kind === "group" || node.orderIsAuthored;
10629
10920
  var duplicateOrderDiagnostics = (nodes) => {
10630
10921
  const byOrder = new Map;
@@ -10671,25 +10962,25 @@ var sortNodes = (nodes, diagnostics) => {
10671
10962
  }
10672
10963
  }
10673
10964
  };
10674
- var hoistPages = (nodes, recurse) => {
10675
- const pages = nodes.filter((node) => node.kind === "page");
10965
+ var hoistPages = (nodes, display, hoist) => {
10676
10966
  const groups = nodes.filter((node) => node.kind === "group");
10677
- nodes.splice(0, nodes.length, ...pages, ...groups);
10678
- if (recurse) {
10679
- for (const group of groups) {
10680
- hoistPages(group.children, recurse);
10681
- }
10967
+ if (hoist || groups.some((group) => (group.display ?? display) === "flat")) {
10968
+ const pages = nodes.filter((node) => node.kind === "page");
10969
+ nodes.splice(0, nodes.length, ...pages, ...groups);
10970
+ }
10971
+ for (const group of groups) {
10972
+ hoistPages(group.children, display, false);
10682
10973
  }
10683
10974
  };
10684
- var hoistTabSections = (nodes, tabPaths, recurse) => {
10975
+ var hoistTabSections = (nodes, tabPaths, display) => {
10685
10976
  for (const node of nodes) {
10686
10977
  if (node.kind !== "group") {
10687
10978
  continue;
10688
10979
  }
10689
10980
  if (node.routePath !== undefined && tabPaths.has(node.routePath)) {
10690
- hoistPages(node.children, recurse);
10981
+ hoistPages(node.children, display, true);
10691
10982
  }
10692
- hoistTabSections(node.children, tabPaths, recurse);
10983
+ hoistTabSections(node.children, tabPaths, display);
10693
10984
  }
10694
10985
  };
10695
10986
  var toNavNode = (node, display) => {
@@ -10708,25 +10999,29 @@ var toNavNode = (node, display) => {
10708
10999
  return {
10709
11000
  children: node.children.map((child) => toNavNode(child, display)),
10710
11001
  collapsed: node.collapsed,
10711
- display,
11002
+ display: node.display ?? display,
10712
11003
  icon: node.icon,
10713
11004
  kind: "group",
10714
11005
  label: node.label,
10715
11006
  path: node.routePath
10716
11007
  };
10717
11008
  };
10718
- var buildFileSystemSidebar = (pages, folderMeta, sharedMeta, metaPrefix, display, tabPaths, diagnostics = []) => {
11009
+ var buildFileSystemSidebar = (pages, folderMeta, sharedMeta, metaPrefix, sharedMetaPrefix, display, tabPaths, diagnostics = []) => {
10719
11010
  const root = createGroup("", "", "", 0);
11011
+ const indexDisplay = new Map;
10720
11012
  for (const page of pages) {
10721
11013
  const parts = page.navPath.split("/");
10722
11014
  const filename = parts.at(-1) ?? page.navPath;
10723
- const stem = filename.replace(extname(filename), "");
11015
+ const stem = stemOf(filename);
10724
11016
  const dirs = parts.slice(0, -1);
10725
11017
  if (isIndexStem(stem)) {
10726
- const diagnostic = indexTitleMismatchDiagnostic(page, dirs.join("/"), folderMeta, sharedMeta, metaPrefix);
11018
+ const diagnostic = indexTitleMismatchDiagnostic(page, dirs.join("/"), folderMeta, sharedMeta, metaPrefix, sharedMetaPrefix);
10727
11019
  if (diagnostic) {
10728
11020
  diagnostics.push(diagnostic);
10729
11021
  }
11022
+ if (page.meta.sidebar.display && !page.fallback) {
11023
+ indexDisplay.set(dirs.join("/"), page.meta.sidebar.display);
11024
+ }
10730
11025
  }
10731
11026
  if (page.meta.sidebar.hidden) {
10732
11027
  continue;
@@ -10760,10 +11055,10 @@ var buildFileSystemSidebar = (pages, folderMeta, sharedMeta, metaPrefix, display
10760
11055
  route: page.route
10761
11056
  });
10762
11057
  }
10763
- applyFolderMeta(root, folderMeta, sharedMeta, metaPrefix);
11058
+ applyFolderMeta(root, folderMeta, sharedMeta, metaPrefix, sharedMetaPrefix, indexDisplay);
10764
11059
  sortNodes(root.children, diagnostics);
10765
- hoistPages(root.children, display === "flat");
10766
- hoistTabSections(root.children, tabPaths, display === "flat");
11060
+ hoistPages(root.children, display, true);
11061
+ hoistTabSections(root.children, tabPaths, display);
10767
11062
  return root.children.map((child) => toNavNode(child, display));
10768
11063
  };
10769
11064
  var normalizeRef = (ref) => {
@@ -10782,8 +11077,9 @@ var routeForRef = (ref, byRoute, basePath) => {
10782
11077
  const normalized = normalizeRef(ref);
10783
11078
  return byRoute.get(normalized)?.route ?? withBasePath(basePath, normalized);
10784
11079
  };
11080
+ var isPageRef = (item) => typeof item === "string";
10785
11081
  var configItemToNode = (item, byRoute, basePath) => {
10786
- if (typeof item === "string") {
11082
+ if (isPageRef(item)) {
10787
11083
  const page = byRoute.get(normalizeRef(item));
10788
11084
  if (!page) {
10789
11085
  return null;
@@ -10826,7 +11122,7 @@ var configItemToNode = (item, byRoute, basePath) => {
10826
11122
  var buildConfigSidebar = (items, byRoute, display, basePath) => {
10827
11123
  const nodes = [];
10828
11124
  for (const item of items) {
10829
- if (typeof item !== "string" && item.items) {
11125
+ if (!isPageRef(item) && item.items) {
10830
11126
  nodes.push({
10831
11127
  badge: item.badge,
10832
11128
  children: buildConfigSidebar(item.items, byRoute, display, basePath),
@@ -10870,29 +11166,46 @@ var withTabHrefs = (tabs, sidebar) => tabs.map((tab) => {
10870
11166
  const href = tab.href ?? resolveTabHref(sidebar, tab.path);
10871
11167
  return href === tab.path ? tab : { ...tab, href };
10872
11168
  });
11169
+ var rebaseNavChrome = (basePath, options) => {
11170
+ const featured = options.featured ?? [];
11171
+ const selectors = options.selectors ?? [];
11172
+ const tabs = options.tabs ?? [];
11173
+ if (!basePath) {
11174
+ return { featured, selectors, tabs };
11175
+ }
11176
+ const rebasePath = (item) => ({
11177
+ ...item,
11178
+ path: withBasePath(basePath, item.path)
11179
+ });
11180
+ return {
11181
+ featured: featured.map((link) => ({
11182
+ ...link,
11183
+ href: withBasePath(basePath, link.href)
11184
+ })),
11185
+ selectors: selectors.map((selector) => ({
11186
+ ...selector,
11187
+ items: selector.items.map(rebasePath)
11188
+ })),
11189
+ tabs: tabs.map((tab) => {
11190
+ const rebased = {
11191
+ ...tab,
11192
+ items: tab.items?.map(rebasePath),
11193
+ path: withBasePath(basePath, tab.path)
11194
+ };
11195
+ if (tab.href) {
11196
+ rebased.href = withBasePath(basePath, tab.href);
11197
+ }
11198
+ return rebased;
11199
+ })
11200
+ };
11201
+ };
10873
11202
  var buildNavigation = (pages, options) => {
10874
11203
  const basePath = options.basePath ?? "";
10875
11204
  const display = options.display ?? "flat";
10876
11205
  const metaPrefix = options.metaPrefix ?? "";
11206
+ const sharedMetaPrefix = options.sharedMetaPrefix ?? "";
10877
11207
  const sharedFolderMeta = options.sharedFolderMeta ?? new Map;
10878
- const rebasePath = (item) => ({
10879
- ...item,
10880
- path: withBasePath(basePath, item.path)
10881
- });
10882
- const featured = basePath ? (options.featured ?? []).map((link) => ({
10883
- ...link,
10884
- href: withBasePath(basePath, link.href)
10885
- })) : options.featured ?? [];
10886
- const selectors = basePath ? (options.selectors ?? []).map((selector) => ({
10887
- ...selector,
10888
- items: selector.items.map(rebasePath)
10889
- })) : options.selectors ?? [];
10890
- const tabs = basePath ? (options.tabs ?? []).map((tab) => ({
10891
- ...tab,
10892
- ...tab.href ? { href: withBasePath(basePath, tab.href) } : {},
10893
- items: tab.items?.map(rebasePath),
10894
- path: withBasePath(basePath, tab.path)
10895
- })) : options.tabs ?? [];
11208
+ const { featured, selectors, tabs } = rebaseNavChrome(basePath, options);
10896
11209
  const byRoute = new Map(pages.map((page) => [
10897
11210
  options.refByLogical ? page.translationKey : page.route,
10898
11211
  page
@@ -10906,6 +11219,8 @@ var buildNavigation = (pages, options) => {
10906
11219
  }
10907
11220
  }
10908
11221
  const rootTabPath = withBasePath(basePath, options.localizedRoot ?? "/");
11222
+ const diagnostics = options.diagnostics ?? [];
11223
+ diagnostics.push(...sidebarDisplayIgnoredDiagnostics(pages, Boolean(options.sidebar)));
10909
11224
  if (options.sidebar) {
10910
11225
  const sidebar2 = buildConfigSidebar(options.sidebar, byRoute, display, basePath);
10911
11226
  return {
@@ -10916,7 +11231,7 @@ var buildNavigation = (pages, options) => {
10916
11231
  tabs: withTabHrefs(tabs, sidebar2)
10917
11232
  };
10918
11233
  }
10919
- const sidebar = buildFileSystemSidebar(pages, options.folderMeta, sharedFolderMeta, metaPrefix, display, new Set(tabs.flatMap((tab) => tab.path === rootTabPath ? [] : [tab.path])), options.diagnostics);
11234
+ const sidebar = buildFileSystemSidebar(pages, options.folderMeta, sharedFolderMeta, metaPrefix, sharedMetaPrefix, display, new Set(tabs.flatMap((tab) => tab.path === rootTabPath ? [] : [tab.path])), diagnostics);
10920
11235
  return {
10921
11236
  featured,
10922
11237
  root: rootTabPath,
@@ -10926,6 +11241,44 @@ var buildNavigation = (pages, options) => {
10926
11241
  };
10927
11242
  };
10928
11243
 
11244
+ // src/core/versions.ts
11245
+ var VERSION_LIKE = /^v\d/iu;
11246
+ var detectVersion = (parts, versions) => {
11247
+ const [first] = parts;
11248
+ if (first && versions.archived.some((version) => version.id === first)) {
11249
+ return { rest: parts.slice(1), version: first };
11250
+ }
11251
+ return { rest: parts, version: "" };
11252
+ };
11253
+ var detectVersionRef = (ref, versions) => {
11254
+ const { version, rest } = detectVersion(ref.split("/"), versions);
11255
+ return { rest: rest.join("/"), version };
11256
+ };
11257
+ var versionizeRoute = (logicalRoute, version) => {
11258
+ if (!version) {
11259
+ return logicalRoute;
11260
+ }
11261
+ return logicalRoute === "/" ? `/${version}` : `/${version}${logicalRoute}`;
11262
+ };
11263
+ var versionsDiagnostics = (pages, versions) => {
11264
+ const configured = new Set(versions.archived.map((version) => version.id));
11265
+ const seen = new Set;
11266
+ const diagnostics = [];
11267
+ for (const page of pages) {
11268
+ const [first] = page.source.ref.split("/");
11269
+ if (first && !seen.has(first) && VERSION_LIKE.test(first) && !configured.has(first)) {
11270
+ seen.add(first);
11271
+ diagnostics.push({
11272
+ code: "BLUME_VERSIONS_UNCONFIGURED_VERSION",
11273
+ message: `Folder "${first}/" looks like a version, but "${first}" is not in versions.archived — its pages are treated as current-version content at /${first}/….`,
11274
+ severity: "warning",
11275
+ suggestion: `Add { id: "${first}" } to versions.archived, or rename the folder if it isn't a snapshot.`
11276
+ });
11277
+ }
11278
+ }
11279
+ return diagnostics;
11280
+ };
11281
+
10929
11282
  // src/core/graph.ts
10930
11283
  var collectRoutes = (pages) => {
10931
11284
  const routes = new Map;
@@ -10964,8 +11317,9 @@ var localePagesFor = (code, real, fallback, fallbackByKey, i18n, basePath) => {
10964
11317
  }
10965
11318
  return [...real, ...filled];
10966
11319
  };
11320
+ var isSingleLabel = (label) => typeof label === "string";
10967
11321
  var resolveLabel = (label, locale, defaultLocale) => {
10968
- if (typeof label === "string") {
11322
+ if (isSingleLabel(label)) {
10969
11323
  return label;
10970
11324
  }
10971
11325
  return label[locale] ?? (defaultLocale === undefined ? undefined : label[defaultLocale]) ?? Object.values(label)[0] ?? "";
@@ -10978,19 +11332,25 @@ var resolveTabLabels = (tabs, locale, defaultLocale) => (tabs ?? []).map((tab) =
10978
11332
  })),
10979
11333
  label: resolveLabel(tab.label, locale, defaultLocale)
10980
11334
  }));
10981
- var buildLocaleNavigation = (code, pages, fallback, fallbackByKey, options, i18n, diagnostics) => {
11335
+ var buildLocaleNavigation = (code, pages, fallback, fallbackByKey, options, i18n, diagnostics, version = "") => {
10982
11336
  const localizePath = (path) => path.startsWith("/") ? localizeRoute(path, code, i18n) : path;
10983
- const tabs = resolveTabLabels(options.navigation.tabs, code, i18n.defaultLocale).map((tab) => ({
10984
- ...tab,
10985
- ...tab.href ? { href: localizePath(tab.href) } : {},
10986
- items: tab.items?.map((item) => ({
10987
- ...item,
10988
- path: localizePath(item.path)
10989
- })),
10990
- path: localizePath(tab.path)
10991
- }));
11337
+ const tabs = resolveTabLabels(options.navigation.tabs, code, i18n.defaultLocale).map((tab) => {
11338
+ const localized = {
11339
+ ...tab,
11340
+ items: tab.items?.map((item) => ({
11341
+ ...item,
11342
+ path: localizePath(item.path)
11343
+ })),
11344
+ path: localizePath(tab.path)
11345
+ };
11346
+ if (localized.href) {
11347
+ localized.href = localizePath(localized.href);
11348
+ }
11349
+ return localized;
11350
+ });
10992
11351
  const real = pages.filter((page) => page.locale === code);
10993
11352
  const localePages = localePagesFor(code, real, fallback, fallbackByKey, i18n, options.basePath ?? "");
11353
+ const localeDir = i18n.parser === "dir" && code !== i18n.defaultLocale ? code : "";
10994
11354
  return buildNavigation(localePages, {
10995
11355
  basePath: options.basePath ?? "",
10996
11356
  diagnostics,
@@ -11000,16 +11360,17 @@ var buildLocaleNavigation = (code, pages, fallback, fallbackByKey, options, i18n
11000
11360
  href: localizePath(link.href)
11001
11361
  })),
11002
11362
  folderMeta: options.folderMeta,
11003
- localizedRoot: localizeRoute("/", code, i18n),
11004
- metaPrefix: i18n.parser === "dir" && code !== i18n.defaultLocale ? code : "",
11363
+ localizedRoot: localizeRoute(versionizeRoute("/", version), code, i18n),
11364
+ metaPrefix: [version, localeDir].filter(Boolean).join("/"),
11005
11365
  refByLogical: true,
11006
11366
  selectors: options.navigation.selectors,
11007
11367
  sharedFolderMeta: options.sharedFolderMeta,
11008
- sidebar: options.navigation.sidebar.items,
11368
+ sharedMetaPrefix: version,
11369
+ sidebar: version ? undefined : options.navigation.sidebar.items,
11009
11370
  tabs
11010
11371
  });
11011
11372
  };
11012
- var buildI18nNavigation = (pages, options, i18n, diagnostics) => {
11373
+ var buildI18nNavigation = (pages, options, i18n, diagnostics, version = "") => {
11013
11374
  const fallback = resolveFallbackLocale(i18n);
11014
11375
  const fallbackByKey = new Map;
11015
11376
  if (fallback) {
@@ -11023,7 +11384,7 @@ var buildI18nNavigation = (pages, options, i18n, diagnostics) => {
11023
11384
  const seen = new Set;
11024
11385
  for (const { code } of i18n.locales) {
11025
11386
  const localeDiagnostics = [];
11026
- navigationByLocale[code] = buildLocaleNavigation(code, pages, fallback, fallbackByKey, options, i18n, localeDiagnostics);
11387
+ navigationByLocale[code] = buildLocaleNavigation(code, pages, fallback, fallbackByKey, options, i18n, localeDiagnostics, version);
11027
11388
  for (const diagnostic of localeDiagnostics) {
11028
11389
  const key = `${diagnostic.code}
11029
11390
  ${diagnostic.file ?? ""}
@@ -11042,11 +11403,37 @@ ${diagnostic.message}`;
11042
11403
  };
11043
11404
  return { navigation, navigationByLocale };
11044
11405
  };
11406
+ var buildVersionNavigation = (id2, versionPages, options, diagnostics) => {
11407
+ const { i18n } = options;
11408
+ if (i18n) {
11409
+ return buildI18nNavigation(versionPages, options, i18n, diagnostics, id2).navigationByLocale;
11410
+ }
11411
+ return {
11412
+ "": buildNavigation(versionPages, {
11413
+ basePath: options.basePath ?? "",
11414
+ diagnostics,
11415
+ display: options.navigation.sidebar.display,
11416
+ featured: options.navigation.featured,
11417
+ folderMeta: options.folderMeta,
11418
+ localizedRoot: versionizeRoute("/", id2),
11419
+ metaPrefix: id2,
11420
+ selectors: options.navigation.selectors,
11421
+ sharedFolderMeta: options.sharedFolderMeta,
11422
+ sharedMetaPrefix: id2,
11423
+ tabs: resolveTabLabels(options.navigation.tabs, "")
11424
+ })
11425
+ };
11426
+ };
11045
11427
  var buildContentGraph = (pages, options) => {
11046
11428
  const { diagnostics, routes } = collectRoutes(pages);
11047
11429
  const { i18n } = options;
11048
- const { navigation, navigationByLocale } = i18n ? buildI18nNavigation(pages, options, i18n, diagnostics) : {
11049
- navigation: buildNavigation(pages, {
11430
+ const currentPages = options.versions ? pages.filter((page) => page.version === "") : pages;
11431
+ let navigationByLocale = {};
11432
+ let navigation;
11433
+ if (i18n) {
11434
+ ({ navigation, navigationByLocale } = buildI18nNavigation(currentPages, options, i18n, diagnostics));
11435
+ } else {
11436
+ navigation = buildNavigation(currentPages, {
11050
11437
  basePath: options.basePath ?? "",
11051
11438
  diagnostics,
11052
11439
  display: options.navigation.sidebar.display,
@@ -11056,14 +11443,18 @@ var buildContentGraph = (pages, options) => {
11056
11443
  sharedFolderMeta: options.sharedFolderMeta,
11057
11444
  sidebar: options.navigation.sidebar.items,
11058
11445
  tabs: resolveTabLabels(options.navigation.tabs, "")
11059
- }),
11060
- navigationByLocale: {}
11061
- };
11062
- diagnostics.push(...validateNavIcons(navigation), ...validateNavStructure(navigation, pages));
11446
+ });
11447
+ }
11448
+ const navigationByVersion = {};
11449
+ for (const { id: id2 } of options.versions?.archived ?? []) {
11450
+ navigationByVersion[id2] = buildVersionNavigation(id2, pages.filter((page) => page.version === id2), options, diagnostics);
11451
+ }
11452
+ diagnostics.push(...validateNavIcons(navigation), ...validateNavStructure(navigation, currentPages));
11063
11453
  return {
11064
11454
  diagnostics,
11065
11455
  navigation,
11066
11456
  navigationByLocale,
11457
+ navigationByVersion,
11067
11458
  pages,
11068
11459
  routes
11069
11460
  };
@@ -11124,11 +11515,42 @@ var gitLastModifiedTimes = (root, contentRoots, sourcePaths) => {
11124
11515
  return new Map;
11125
11516
  }
11126
11517
  };
11518
+ var isShallowGitRepository = (root) => {
11519
+ try {
11520
+ return execFileSync("git", ["-C", root, "rev-parse", "--is-shallow-repository"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim() === "true";
11521
+ } catch {
11522
+ return false;
11523
+ }
11524
+ };
11525
+ var lastModifiedShallowWarning = (root, undatedCount) => {
11526
+ if (undatedCount === 0 || !isShallowGitRepository(root)) {
11527
+ return [];
11528
+ }
11529
+ return [
11530
+ {
11531
+ code: "BLUME_SHALLOW_GIT_HISTORY",
11532
+ message: `lastModified is on, but this build runs in a shallow git clone, so ${undatedCount} page(s) have no git-derived date — their sitemap <lastmod> and "Last updated" stamps are omitted.`,
11533
+ severity: "warning",
11534
+ suggestion: "Fetch full history in CI: set the VERCEL_DEEP_CLONE=true environment variable on Vercel, or fetch-depth: 0 for actions/checkout."
11535
+ }
11536
+ ];
11537
+ };
11127
11538
 
11128
11539
  // src/core/manifest.ts
11540
+ var versionAlternateKey = (versionKey, locale) => `${versionKey}\x00${locale}`;
11541
+ var versionAlternatesFor = (byKey, versionKey, locale) => {
11542
+ const key = versionAlternateKey(versionKey, locale);
11543
+ const existing = byKey.get(key);
11544
+ if (existing) {
11545
+ return existing;
11546
+ }
11547
+ const list2 = [];
11548
+ byKey.set(key, list2);
11549
+ return list2;
11550
+ };
11129
11551
  var MANIFEST_VERSION = 1;
11130
11552
  var contentIndexable = (page, config) => !page.meta.search.exclude && (!page.meta.sidebar.hidden || config.search.indexing.includeHiddenPages);
11131
- var buildFallbackRoutes = (graph, i18n, alternatesByKey, basePath) => {
11553
+ var buildFallbackRoutes = (graph, i18n, alternatesByKey, basePath, versionAlternatesByKey) => {
11132
11554
  const fallback = resolveFallbackLocale(i18n);
11133
11555
  if (!fallback) {
11134
11556
  return [];
@@ -11144,6 +11566,11 @@ var buildFallbackRoutes = (graph, i18n, alternatesByKey, basePath) => {
11144
11566
  if (present.has(key)) {
11145
11567
  continue;
11146
11568
  }
11569
+ const path = withBasePath(basePath, localizeRoute(key, code, i18n));
11570
+ const versionAlternates = versionAlternatesByKey ? versionAlternatesFor(versionAlternatesByKey, source.versionKey, code) : [];
11571
+ if (versionAlternatesByKey) {
11572
+ versionAlternates.push({ path, version: source.version });
11573
+ }
11147
11574
  routes.push({
11148
11575
  alternates: alternatesByKey.get(key) ?? [],
11149
11576
  collection: source.collection ?? "docs",
@@ -11157,10 +11584,12 @@ var buildFallbackRoutes = (graph, i18n, alternatesByKey, basePath) => {
11157
11584
  indexable: false,
11158
11585
  lastModified: source.lastModified,
11159
11586
  locale: code,
11160
- path: withBasePath(basePath, localizeRoute(key, code, i18n)),
11587
+ path,
11161
11588
  source: source.source,
11162
11589
  sourcePath: source.sourcePath,
11163
- title: source.title
11590
+ title: source.title,
11591
+ version: source.version,
11592
+ versionAlternates
11164
11593
  });
11165
11594
  }
11166
11595
  }
@@ -11178,6 +11607,12 @@ var buildManifest = (options) => {
11178
11607
  alternatesByKey.set(page.translationKey, list2);
11179
11608
  }
11180
11609
  }
11610
+ const versionAlternatesByKey = config.versions ? new Map : undefined;
11611
+ if (versionAlternatesByKey) {
11612
+ for (const page of graph.pages) {
11613
+ versionAlternatesFor(versionAlternatesByKey, page.versionKey, page.locale).push({ path: page.route, version: page.version });
11614
+ }
11615
+ }
11181
11616
  const routes = graph.pages.map((page) => ({
11182
11617
  alternates: alternatesByKey.get(page.translationKey) ?? [],
11183
11618
  collection: page.collection ?? "docs",
@@ -11193,10 +11628,18 @@ var buildManifest = (options) => {
11193
11628
  path: page.route,
11194
11629
  source: page.source,
11195
11630
  sourcePath: page.sourcePath,
11196
- title: page.title
11631
+ title: page.title,
11632
+ version: page.version,
11633
+ versionAlternates: versionAlternatesByKey ? versionAlternatesFor(versionAlternatesByKey, page.versionKey, page.locale) : []
11197
11634
  }));
11198
11635
  if (i18n) {
11199
- routes.push(...buildFallbackRoutes(graph, i18n, alternatesByKey, config.basePath));
11636
+ routes.push(...buildFallbackRoutes(graph, i18n, alternatesByKey, config.basePath, versionAlternatesByKey));
11637
+ }
11638
+ if (versionAlternatesByKey && config.versions) {
11639
+ const rank = new Map(config.versions.archived.map((version, index) => [version.id, index + 1]));
11640
+ for (const list2 of versionAlternatesByKey.values()) {
11641
+ list2.sort((a, b) => (rank.get(a.version) ?? 0) - (rank.get(b.version) ?? 0));
11642
+ }
11200
11643
  }
11201
11644
  routes.sort((a, b) => a.path.localeCompare(b.path));
11202
11645
  return {
@@ -11220,7 +11663,8 @@ var META_FILES = [
11220
11663
  "**/meta.$.js",
11221
11664
  "**/meta.$.mjs"
11222
11665
  ];
11223
- var resolveMeta = async (mod) => typeof mod === "function" ? await mod() : mod;
11666
+ var isMetaFactory = (mod) => typeof mod === "function";
11667
+ var resolveMeta = async (mod) => isMetaFactory(mod) ? await mod() : mod;
11224
11668
  var metaKeyFor = (prefix, dir) => {
11225
11669
  const clean = prefix ? prefix.replaceAll(/^\/+|\/+$/gu, "") : "";
11226
11670
  if (!clean) {
@@ -11229,8 +11673,9 @@ var metaKeyFor = (prefix, dir) => {
11229
11673
  return dir ? `${clean}/${dir}` : clean;
11230
11674
  };
11231
11675
  var discoverFolderMeta = async (sources, options = {}) => {
11232
- const list2 = typeof sources === "string" ? [{ root: sources }] : sources;
11676
+ const list2 = Array.isArray(sources) ? sources : [{ root: sources }];
11233
11677
  const localeDirs = new Set(options.localeDirs);
11678
+ const versionDirs = new Set(options.versionDirs);
11234
11679
  const load2 = createModuleLoader();
11235
11680
  const meta = new Map;
11236
11681
  const shared = new Map;
@@ -11259,7 +11704,12 @@ var discoverFolderMeta = async (sources, options = {}) => {
11259
11704
  for (const entry of loaded) {
11260
11705
  const dir = relative8(source.root, dirname6(entry.file));
11261
11706
  const [head, ...tail] = dir.split("/");
11262
- const key = head && localeDirs.has(head) ? `${head}/${metaKeyFor(source.prefix, tail.join("/"))}`.replace(/\/$/u, "") : metaKeyFor(source.prefix, dir);
11707
+ const version = head && versionDirs.has(head) ? head : "";
11708
+ const afterVersion = version ? tail : [head ?? "", ...tail];
11709
+ const [localeHead, ...localeTail] = afterVersion;
11710
+ const locale = localeHead && localeDirs.has(localeHead) ? localeHead : "";
11711
+ const rest = (locale ? localeTail : afterVersion).filter(Boolean).join("/");
11712
+ const key = [version, locale, metaKeyFor(source.prefix, rest)].filter(Boolean).join("/");
11263
11713
  if (!entry.ok) {
11264
11714
  diagnostics.push({
11265
11715
  code: "BLUME_META_LOAD_FAILED",
@@ -11331,7 +11781,7 @@ var CODE_FENCE = /^(?<delimiter>```|~~~)/u;
11331
11781
  var nextFenceState = (line, fence) => {
11332
11782
  const trimmed = line.trimStart();
11333
11783
  const delimiter = trimmed.match(CODE_FENCE)?.groups?.delimiter;
11334
- if (delimiter === undefined) {
11784
+ if (delimiter !== "```" && delimiter !== "~~~") {
11335
11785
  return fence;
11336
11786
  }
11337
11787
  if (fence === null) {
@@ -11527,13 +11977,16 @@ var deriveTitle = (meta, headings, id2) => {
11527
11977
  return titleCase(stripNumericPrefix(base.replace(extname2(base), "")));
11528
11978
  };
11529
11979
  var trimSlashes = (value) => value.replaceAll(/^\/+|\/+$/gu, "");
11980
+ var isStringValue = (value) => typeof value === "string";
11530
11981
  var withPrefix = (prefix, path) => {
11531
11982
  const clean = prefix ? trimSlashes(prefix) : "";
11532
11983
  return clean ? `${clean}/${path}` : path;
11533
11984
  };
11985
+ var isKeyCarrier = (segment) => typeof segment === "object" && segment !== null;
11986
+ var isSymbolKey = (key) => typeof key === "symbol";
11534
11987
  var segmentKey2 = (segment) => {
11535
- const key = typeof segment === "object" && segment !== null ? segment.key : segment;
11536
- return typeof key === "symbol" ? String(key) : key;
11988
+ const key = isKeyCarrier(segment) ? segment.key : segment;
11989
+ return isSymbolKey(key) ? String(key) : key;
11537
11990
  };
11538
11991
  var validateCustomKeys = (data, extend) => {
11539
11992
  const custom = {};
@@ -11564,7 +12017,7 @@ var validateCustomKeys = (data, extend) => {
11564
12017
  };
11565
12018
  };
11566
12019
  var parseEntryMeta = (entry, ctx) => {
11567
- const entryType = typeof entry.data.type === "string" ? entry.data.type : ctx.defaultType;
12020
+ const entryType = isStringValue(entry.data.type) ? entry.data.type : ctx.defaultType;
11568
12021
  const typeExtend = ctx.typeFrontmatter?.[entryType];
11569
12022
  const extend = ctx.frontmatterExtend || typeExtend ? { ...ctx.frontmatterExtend, ...typeExtend } : undefined;
11570
12023
  const known = extend ? Object.fromEntries(Object.entries(entry.data).filter(([key]) => !Object.hasOwn(extend, key))) : entry.data;
@@ -11600,13 +12053,16 @@ var normalizeEntry = (entry, ctx) => {
11600
12053
  if (meta.noindex) {
11601
12054
  meta.seo.noindex = true;
11602
12055
  }
12056
+ const { versions } = ctx;
12057
+ const { version, rest: versionlessRef } = versions ? detectVersionRef(entry.ref, versions) : { rest: entry.ref, version: "" };
11603
12058
  const { i18n } = ctx;
11604
- const { navPath: rawNavPath, locales } = i18n ? localePlacement(entry.ref, ext, i18n) : { locales: [""], navPath: entry.ref };
12059
+ const { navPath: rawNavPath, locales } = i18n ? localePlacement(versionlessRef, ext, i18n) : { locales: [""], navPath: versionlessRef };
11605
12060
  const navPath = withPrefix(ctx.source.prefix, rawNavPath);
11606
12061
  const slugInput = meta.slug ?? entry.slug;
11607
12062
  const slug = slugInput ? trimSlashes(slugInput) : "";
11608
12063
  const routeInput = withPrefix(ctx.source.prefix, slug ? `${slug}${ext}` : rawNavPath);
11609
- const { segments, groups, route: logicalRoute } = mapRoute(routeInput);
12064
+ const { segments, groups, route: versionKey } = mapRoute(routeInput);
12065
+ const logicalRoute = versionizeRoute(versionKey, version);
11610
12066
  const headings = extractHeadings(entry.body.text);
11611
12067
  const { staged } = ctx.source;
11612
12068
  const base = {
@@ -11630,7 +12086,9 @@ var normalizeEntry = (entry, ctx) => {
11630
12086
  source: { name: ctx.source.name, ref: entry.ref },
11631
12087
  sourcePath: entry.sourcePath,
11632
12088
  title: deriveTitle(meta, headings, navPath),
11633
- translationKey: logicalRoute
12089
+ translationKey: logicalRoute,
12090
+ version,
12091
+ versionKey
11634
12092
  };
11635
12093
  const pages = locales.map((locale) => ({
11636
12094
  ...base,
@@ -11740,6 +12198,7 @@ var operationKey = (method, path, operationId) => {
11740
12198
  return fromId || slugify(`${method}-${path}`);
11741
12199
  };
11742
12200
  var isOperation = (value) => typeof value === "object" && value !== null;
12201
+ var hasTagName = (tag) => typeof tag.name === "string";
11743
12202
  var tagSlugger = () => {
11744
12203
  const assigned = new Map;
11745
12204
  const taken = new Set;
@@ -11758,16 +12217,45 @@ var tagSlugger = () => {
11758
12217
  return slug;
11759
12218
  };
11760
12219
  };
11761
- var extractOperations = (document, baseRoute) => {
12220
+ var operationCollector = (baseRoute, tagMeta) => {
11762
12221
  const operations = [];
11763
12222
  const tagOrder = [];
11764
12223
  const tagsSeen = new Set;
11765
- const tagMeta = new Map((document.tags ?? []).map((tag) => [tag.name, tag.description ?? ""]));
11766
12224
  const seen = new Set;
11767
- const warnings = [];
11768
12225
  const slugForTag = tagSlugger();
11769
- for (const [path, rawItem] of Object.entries(document.paths ?? {})) {
11770
- const item = rawItem;
12226
+ const add2 = (entry) => {
12227
+ const tagSlug = slugForTag(entry.tag);
12228
+ if (!tagsSeen.has(entry.tag)) {
12229
+ tagsSeen.add(entry.tag);
12230
+ tagOrder.push(entry.tag);
12231
+ }
12232
+ let { key } = entry;
12233
+ while (seen.has(key)) {
12234
+ key = `${key}-${entry.method}`;
12235
+ }
12236
+ seen.add(key);
12237
+ operations.push({
12238
+ ...entry,
12239
+ key,
12240
+ route: `${baseRoute === "/" ? "" : baseRoute}/${tagSlug}/${key}`,
12241
+ tagSlug
12242
+ });
12243
+ };
12244
+ const finish = () => ({
12245
+ operations,
12246
+ tags: tagOrder.map((name) => ({
12247
+ description: tagMeta.get(name) ?? "",
12248
+ name,
12249
+ slug: slugForTag(name)
12250
+ }))
12251
+ });
12252
+ return { add: add2, finish };
12253
+ };
12254
+ var extractOperations = (document, baseRoute) => {
12255
+ const warnings = [];
12256
+ const tagMeta = new Map((document.tags ?? []).filter(hasTagName).map((tag) => [tag.name, tag.description ?? ""]));
12257
+ const collector = operationCollector(baseRoute, tagMeta);
12258
+ for (const [path, item] of Object.entries(document.paths ?? {})) {
11771
12259
  if (!item) {
11772
12260
  continue;
11773
12261
  }
@@ -11780,78 +12268,207 @@ var extractOperations = (document, baseRoute) => {
11780
12268
  if (!isOperation(operation)) {
11781
12269
  continue;
11782
12270
  }
11783
- const tag = operation.tags?.[0] ?? UNTAGGED;
11784
- const tagSlug = slugForTag(tag);
11785
- if (!tagsSeen.has(tag)) {
11786
- tagsSeen.add(tag);
11787
- tagOrder.push(tag);
11788
- }
11789
- let key = operationKey(method, path, operation.operationId);
11790
- while (seen.has(key)) {
11791
- key = `${key}-${method}`;
11792
- }
11793
- seen.add(key);
11794
- operations.push({
12271
+ collector.add({
11795
12272
  deprecated: operation.deprecated ?? false,
11796
12273
  description: operation.description ?? "",
11797
- key,
12274
+ key: operationKey(method, path, operation.operationId),
11798
12275
  method,
11799
12276
  operationId: operation.operationId,
11800
12277
  path,
11801
- route: `${baseRoute === "/" ? "" : baseRoute}/${tagSlug}/${key}`,
11802
12278
  summary: operation.summary ?? "",
11803
- tag,
11804
- tagSlug
12279
+ tag: operation.tags?.[0] ?? UNTAGGED
11805
12280
  });
11806
12281
  }
11807
12282
  }
11808
- const tags = tagOrder.map((name) => ({
11809
- description: tagMeta.get(name) ?? "",
11810
- name,
11811
- slug: slugForTag(name)
11812
- }));
11813
- return { operations, tags, warnings };
12283
+ return { ...collector.finish(), warnings };
11814
12284
  };
11815
12285
 
11816
- // src/openapi/parse.ts
11817
- import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
11818
- import { setTimeout as sleep } from "node:timers/promises";
11819
- import { normalize as normalize2, upgrade } from "@scalar/openapi-parser";
11820
- import pRetry, { AbortError } from "p-retry";
11821
- import { isAbsolute as isAbsolute3, join as join12 } from "pathe";
11822
- var URL_SPEC = /^https?:\/\//u;
11823
- var FETCH_TIMEOUT_MS = 15000;
11824
- var MAX_ATTEMPTS = 3;
11825
- var BASE_BACKOFF_MS = 500;
11826
- var MAX_RETRY_WAIT_MS = 1e4;
11827
- var SECOND_MS = 1000;
11828
- var RETRYABLE_STATUS = new Set([408, 425, 429, 500, 502, 503, 504]);
11829
- var USER_AGENT = "blume (+https://github.com/haydenbleasel/blume)";
11830
- var PROXY_ENV_VARS = [
11831
- "HTTP_PROXY",
11832
- "http_proxy",
11833
- "HTTPS_PROXY",
11834
- "https_proxy",
11835
- "ALL_PROXY",
11836
- "all_proxy"
11837
- ];
11838
-
11839
- class InvalidSpecError extends Error {
11840
- constructor(message) {
11841
- super(message);
11842
- this.name = "InvalidSpecError";
11843
- }
11844
- }
11845
- var proxyInstalled = false;
11846
- var ensureProxyDispatcher = async () => {
11847
- if (proxyInstalled || !PROXY_ENV_VARS.some((name) => process.env[name])) {
11848
- return;
11849
- }
11850
- try {
11851
- const { EnvHttpProxyAgent, setGlobalDispatcher } = await import("undici");
11852
- setGlobalDispatcher(new EnvHttpProxyAgent);
11853
- proxyInstalled = true;
11854
- } catch {}
12286
+ // src/openapi/asyncapi.ts
12287
+ var ASYNCAPI_ACTIONS = ["send", "receive"];
12288
+ var unescapePointer = (token) => token.replaceAll("~1", "/").replaceAll("~0", "~");
12289
+ var CHANNEL_REF = /^#\/channels\/(?<id>.+)$/u;
12290
+ var isString3 = (value) => typeof value === "string";
12291
+ var channelIdOf = (channel) => {
12292
+ const id2 = CHANNEL_REF.exec(channel?.$ref ?? "")?.groups?.id;
12293
+ return id2 === undefined ? undefined : unescapePointer(id2);
12294
+ };
12295
+ var channelAddress = (channelId, channel) => {
12296
+ const address = channel?.address;
12297
+ return isString3(address) && address !== "" ? address : channelId;
12298
+ };
12299
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
12300
+ var COMPONENT_SECTION_REF = /^#\/components\/(?<section>channels|operations)\/(?<name>[^/]+)$/u;
12301
+ var inlineComponentRefs = (document) => {
12302
+ const maps = [
12303
+ [document.channels, "channels"],
12304
+ [document.operations, "operations"]
12305
+ ];
12306
+ for (const [map, section] of maps) {
12307
+ if (!isObject(map)) {
12308
+ continue;
12309
+ }
12310
+ const table = document.components?.[section];
12311
+ for (const [id2, entry] of Object.entries(map)) {
12312
+ if (!isObject(entry) || !isString3(entry.$ref)) {
12313
+ continue;
12314
+ }
12315
+ const groups = COMPONENT_SECTION_REF.exec(entry.$ref)?.groups;
12316
+ if (groups?.section !== section) {
12317
+ continue;
12318
+ }
12319
+ const resolved = isObject(table) ? table[unescapePointer(groups.name ?? "")] : undefined;
12320
+ if (isObject(resolved) && !isString3(resolved.$ref)) {
12321
+ map[id2] = resolved;
12322
+ }
12323
+ }
12324
+ }
12325
+ };
12326
+ var TRAIT_REF = /^#\/components\/(?<section>operationTraits|messageTraits)\/(?<name>[^/]+)$/u;
12327
+ var applyAsyncApiTraits = (document) => {
12328
+ const resolveTrait = (trait) => {
12329
+ if (!isObject(trait)) {
12330
+ return;
12331
+ }
12332
+ if (!isString3(trait.$ref)) {
12333
+ return trait;
12334
+ }
12335
+ const groups = TRAIT_REF.exec(trait.$ref)?.groups;
12336
+ const section = groups?.section ? document.components?.[groups.section] : undefined;
12337
+ const resolved = section?.[groups?.name ?? ""];
12338
+ return isObject(resolved) ? resolved : undefined;
12339
+ };
12340
+ const mergeTraits = (node) => {
12341
+ const { traits } = node;
12342
+ if (!Array.isArray(traits)) {
12343
+ return;
12344
+ }
12345
+ delete node.traits;
12346
+ const merged = {};
12347
+ for (const trait of traits) {
12348
+ Object.assign(merged, resolveTrait(trait));
12349
+ }
12350
+ for (const [key, value] of Object.entries(merged)) {
12351
+ if (!(key in node)) {
12352
+ node[key] = value;
12353
+ }
12354
+ }
12355
+ };
12356
+ for (const operation of Object.values(document.operations ?? {})) {
12357
+ if (isObject(operation)) {
12358
+ mergeTraits(operation);
12359
+ }
12360
+ }
12361
+ const channelMaps = [
12362
+ ...Object.values(document.channels ?? {}),
12363
+ ...Object.values(document.components?.channels ?? {})
12364
+ ];
12365
+ const messageMaps = [
12366
+ ...channelMaps.map((channel) => isObject(channel) ? channel.messages : undefined),
12367
+ document.components?.messages
12368
+ ];
12369
+ for (const messages of messageMaps) {
12370
+ if (!isObject(messages)) {
12371
+ continue;
12372
+ }
12373
+ for (const message of Object.values(messages)) {
12374
+ if (isObject(message) && !isString3(message.$ref)) {
12375
+ mergeTraits(message);
12376
+ }
12377
+ }
12378
+ }
12379
+ return document;
12380
+ };
12381
+ var normalizeAsyncApiDocument = (document) => {
12382
+ inlineComponentRefs(document);
12383
+ return applyAsyncApiTraits(document);
12384
+ };
12385
+ var operationSite = (id2, operation, channels) => {
12386
+ const action = ASYNCAPI_ACTIONS.find((a) => a === operation.action);
12387
+ if (!action) {
12388
+ return `Operation "${id2}" declares no send/receive action; it is missing from the reference.`;
12389
+ }
12390
+ const channelId = channelIdOf(operation.channel);
12391
+ const channel = channelId === undefined ? undefined : channels[channelId];
12392
+ if (channelId === undefined || !isObject(channel) || isString3(channel.$ref)) {
12393
+ return `Operation "${id2}" references a channel that isn't declared under "channels"; it is missing from the reference.`;
12394
+ }
12395
+ return { action, address: channelAddress(channelId, channel), channelId };
12396
+ };
12397
+ var extractAsyncApiOperations = (document, baseRoute) => {
12398
+ const warnings = [];
12399
+ const tagMeta = new Map((document.info?.tags ?? []).filter((tag) => typeof tag?.name === "string").map((tag) => [tag.name, tag.description ?? ""]));
12400
+ const collector = operationCollector(baseRoute, tagMeta);
12401
+ const channels = document.channels ?? {};
12402
+ for (const [id2, operation] of Object.entries(document.operations ?? {})) {
12403
+ if (!isObject(operation)) {
12404
+ continue;
12405
+ }
12406
+ if (isString3(operation.$ref)) {
12407
+ warnings.push(`Operation "${id2}" is a reference that doesn't resolve to a components operation; it is missing from the reference.`);
12408
+ continue;
12409
+ }
12410
+ const site = operationSite(id2, operation, channels);
12411
+ if (isString3(site)) {
12412
+ warnings.push(site);
12413
+ continue;
12414
+ }
12415
+ const { action, address, channelId } = site;
12416
+ const tag = operation.tags?.find((candidate) => typeof candidate?.name === "string")?.name;
12417
+ collector.add({
12418
+ channelId,
12419
+ deprecated: operation.deprecated === true,
12420
+ description: operation.description ?? "",
12421
+ key: operationKey(action, address, id2),
12422
+ method: action,
12423
+ operationId: id2,
12424
+ path: address,
12425
+ summary: operation.title ?? operation.summary ?? "",
12426
+ tag: tag ?? address
12427
+ });
12428
+ }
12429
+ return { ...collector.finish(), warnings };
12430
+ };
12431
+
12432
+ // src/openapi/parse.ts
12433
+ import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
12434
+ import { setTimeout as sleep } from "node:timers/promises";
12435
+ import { convert } from "@asyncapi/converter";
12436
+ import { normalize as normalize2, upgrade } from "@scalar/openapi-parser";
12437
+ import pRetry, { AbortError } from "p-retry";
12438
+ import { isAbsolute as isAbsolute3, join as join12 } from "pathe";
12439
+ var URL_SPEC = /^https?:\/\//u;
12440
+ var FETCH_TIMEOUT_MS = 15000;
12441
+ var MAX_ATTEMPTS = 3;
12442
+ var BASE_BACKOFF_MS = 500;
12443
+ var MAX_RETRY_WAIT_MS = 1e4;
12444
+ var SECOND_MS = 1000;
12445
+ var RETRYABLE_STATUS = new Set([408, 425, 429, 500, 502, 503, 504]);
12446
+ var USER_AGENT = "blume (+https://github.com/haydenbleasel/blume)";
12447
+ var PROXY_ENV_VARS = [
12448
+ "HTTP_PROXY",
12449
+ "http_proxy",
12450
+ "HTTPS_PROXY",
12451
+ "https_proxy",
12452
+ "ALL_PROXY",
12453
+ "all_proxy"
12454
+ ];
12455
+
12456
+ class InvalidSpecError extends Error {
12457
+ constructor(message) {
12458
+ super(message);
12459
+ this.name = "InvalidSpecError";
12460
+ }
12461
+ }
12462
+ var proxyInstalled = false;
12463
+ var ensureProxyDispatcher = async () => {
12464
+ if (proxyInstalled || !PROXY_ENV_VARS.some((name) => process.env[name])) {
12465
+ return;
12466
+ }
12467
+ try {
12468
+ const { EnvHttpProxyAgent, setGlobalDispatcher } = await import("undici");
12469
+ setGlobalDispatcher(new EnvHttpProxyAgent);
12470
+ proxyInstalled = true;
12471
+ } catch {}
11855
12472
  };
11856
12473
  var retryAfterMs = (response) => {
11857
12474
  const header = response.headers.get("retry-after");
@@ -11969,27 +12586,74 @@ var readSpecText = async (spec, root, options) => {
11969
12586
  throw error;
11970
12587
  }
11971
12588
  };
12589
+ var isApiDocument = (value) => typeof value === "object" && value !== null;
11972
12590
  var parseSpec = async (spec, root, options = {}) => {
11973
12591
  const { text, warnings } = await readSpecText(spec, root, options);
11974
12592
  const normalized = normalize2(text);
11975
12593
  const { specification } = upgrade(normalized);
11976
- if (specification === null || typeof specification !== "object") {
12594
+ if (!isApiDocument(specification)) {
11977
12595
  throw new InvalidSpecError(`${spec} is not a valid OpenAPI document (expected a YAML or JSON object).`);
11978
12596
  }
11979
12597
  return { document: specification, warnings };
11980
12598
  };
12599
+ var isAsyncApiDocument = (value) => typeof value === "object" && value !== null && ("asyncapi" in value) && typeof value.asyncapi === "string" && value.asyncapi !== "";
12600
+ var parseAsyncApiSpec = async (spec, root, options = {}) => {
12601
+ const { text, warnings } = await readSpecText(spec, root, options);
12602
+ const normalized = normalize2(text);
12603
+ if (!isAsyncApiDocument(normalized)) {
12604
+ throw new InvalidSpecError(`${spec} is not a valid AsyncAPI document (expected a YAML or JSON object with an \`asyncapi\` version field).`);
12605
+ }
12606
+ const version = normalized.asyncapi;
12607
+ let document = normalized;
12608
+ if (!version.startsWith("3.")) {
12609
+ const captured = [];
12610
+ const originalWarn = console.warn;
12611
+ console.warn = (...args) => {
12612
+ captured.push(args.map(String).join(" "));
12613
+ };
12614
+ try {
12615
+ document = convert(document, "3.0.0");
12616
+ } catch (error) {
12617
+ throw new InvalidSpecError(`${spec} could not be converted to AsyncAPI 3.0 (${error.message}).`);
12618
+ } finally {
12619
+ console.warn = originalWarn;
12620
+ }
12621
+ warnings.push(...captured.map((message) => `Converting ${spec} to AsyncAPI 3.0: ${message}`));
12622
+ }
12623
+ return { document: normalizeAsyncApiDocument(document), warnings };
12624
+ };
11981
12625
 
11982
12626
  // src/openapi/render-mdx.ts
11983
12627
  import { fromMarkdown as fromMarkdown2 } from "mdast-util-from-markdown";
11984
12628
  import { toString as mdastToString } from "mdast-util-to-string";
11985
- var MDX_UNSAFE = /[<{}]/gu;
11986
- var ENTITIES = {
11987
- "<": "&lt;",
11988
- "{": "&#123;",
11989
- "}": "&#125;"
12629
+ import stringWidth3 from "string-width";
12630
+
12631
+ // src/core/text-width.ts
12632
+ import stringWidth2 from "string-width";
12633
+ var columnsPrefix = (text, max) => {
12634
+ let end = 0;
12635
+ let used = 0;
12636
+ const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
12637
+ for (const { index, segment } of graphemes.segment(text)) {
12638
+ const width = stringWidth2(segment);
12639
+ if (used + width > max) {
12640
+ break;
12641
+ }
12642
+ used += width;
12643
+ end = index + segment.length;
12644
+ }
12645
+ return text.slice(0, end);
11990
12646
  };
12647
+
12648
+ // src/openapi/render-mdx.ts
12649
+ var MDX_UNSAFE = /[<{}]/gu;
12650
+ var ENTITIES = new Map([
12651
+ ["<", "&lt;"],
12652
+ ["{", "&#123;"],
12653
+ ["}", "&#125;"]
12654
+ ]);
11991
12655
  var MDX_ESM_KEYWORD = /^(?<keyword>import|export)\b/gmu;
11992
- var escapeProse = (text) => text.replace(MDX_UNSAFE, (char) => ENTITIES[char] ?? char).replace(MDX_ESM_KEYWORD, (keyword) => `&#${keyword.codePointAt(0)};${keyword.slice(1)}`);
12656
+ var escapeProse = (text) => text.replace(MDX_UNSAFE, (char) => ENTITIES.get(char) ?? char).replace(MDX_ESM_KEYWORD, (keyword) => `&#${keyword.codePointAt(0)};${keyword.slice(1)}`);
11993
12657
  var codeSpans = (text) => {
11994
12658
  const spans = [];
11995
12659
  const collect = (node) => {
@@ -12022,29 +12686,28 @@ var mdxSafe = (text) => {
12022
12686
  return out + escapeProse(text.slice(cursor));
12023
12687
  };
12024
12688
  var META_DESCRIPTION_MAX = 160;
12025
- var WHITESPACE = /\s+/gu;
12689
+ var WHITESPACE2 = /\s+/gu;
12026
12690
  var TRAILING_WORD = /\s+\S*$/u;
12027
12691
  var plainProse = (markdown) => {
12028
12692
  const tree = fromMarkdown2(markdown);
12029
12693
  const first = tree.children.find((node) => node.type === "paragraph") ?? tree.children[0];
12030
- return first ? mdastToString(first).replace(WHITESPACE, " ").trim() : "";
12694
+ return first ? mdastToString(first).replace(WHITESPACE2, " ").trim() : "";
12031
12695
  };
12032
12696
  var clip = (text, max) => {
12033
12697
  if (max <= 0) {
12034
12698
  return "";
12035
12699
  }
12036
- if (text.length <= max) {
12700
+ if (stringWidth3(text) <= max) {
12037
12701
  return text;
12038
12702
  }
12039
- const head = text.slice(0, max - 1);
12703
+ const head = columnsPrefix(text, max - 1);
12040
12704
  const onWordBoundary = head.replace(TRAILING_WORD, "");
12041
- return `${onWordBoundary.length >= max / 2 ? onWordBoundary : head}…`;
12705
+ return `${stringWidth3(onWordBoundary) >= max / 2 ? onWordBoundary : head}…`;
12042
12706
  };
12043
12707
  var apiName = (spec) => spec.title || spec.label;
12044
12708
  var operationDescription = (spec, operation) => {
12045
- const endpoint = `${operation.method.toUpperCase()} ${operation.path}`;
12046
- const suffix = `Reference for the ${endpoint} endpoint in the ${apiName(spec)} API.`;
12047
- const prose = clip(plainProse(operation.description || operation.summary), META_DESCRIPTION_MAX - suffix.length - 1);
12709
+ const suffix = spec.kind === "asyncapi" ? `Reference for the ${operation.method} operation on ${operation.path} in the ${apiName(spec)} API.` : `Reference for the ${operation.method.toUpperCase()} ${operation.path} endpoint in the ${apiName(spec)} API.`;
12710
+ const prose = clip(plainProse(operation.description || operation.summary), META_DESCRIPTION_MAX - stringWidth3(suffix) - 1);
12048
12711
  return clip([prose, suffix].filter(Boolean).join(" "), META_DESCRIPTION_MAX);
12049
12712
  };
12050
12713
  var withDescription = (description, component) => description.trim() ? `${mdxSafe(description.trim())}
@@ -12054,19 +12717,29 @@ var operationMdx = (spec, operation, reference) => {
12054
12717
  const method = operation.method.toUpperCase();
12055
12718
  const title = operation.summary || `${method} ${operation.path}`;
12056
12719
  const description = operation.description.trim() === operation.summary.trim() ? "" : operation.description;
12720
+ const flags = {};
12721
+ if (reference?.includeInLlms === false) {
12722
+ flags.ai = { exclude: true };
12723
+ }
12724
+ if (operation.deprecated) {
12725
+ flags.deprecated = true;
12726
+ }
12727
+ const searchFlags = {};
12728
+ if (reference?.includeInSearch === false) {
12729
+ searchFlags.exclude = true;
12730
+ }
12731
+ const seo = {
12732
+ description: operationDescription(spec, operation)
12733
+ };
12734
+ if (reference?.noindex) {
12735
+ seo.noindex = true;
12736
+ }
12057
12737
  return {
12058
12738
  body: withDescription(description, `<Operation source="${spec.slug}" id="${operation.key}" />`),
12059
12739
  data: {
12060
- ...reference?.includeInLlms === false ? { ai: { exclude: true } } : {},
12061
- ...operation.deprecated ? { deprecated: true } : {},
12062
- search: {
12063
- ...reference?.includeInSearch === false ? { exclude: true } : {},
12064
- tags: [operation.tag, method]
12065
- },
12066
- seo: {
12067
- description: operationDescription(spec, operation),
12068
- ...reference?.noindex ? { noindex: true } : {}
12069
- },
12740
+ ...flags,
12741
+ search: { ...searchFlags, tags: [operation.tag, method] },
12742
+ seo,
12070
12743
  sidebar: { badge: method, label: operation.summary || operation.path },
12071
12744
  title,
12072
12745
  type: "openapi-operation"
@@ -12107,6 +12780,19 @@ var overviewMdx = (spec, reference) => {
12107
12780
 
12108
12781
  `));
12109
12782
  }
12783
+ const flags = {};
12784
+ if (reference?.includeInLlms === false) {
12785
+ flags.ai = { exclude: true };
12786
+ }
12787
+ if (reference?.includeInSearch === false) {
12788
+ flags.search = { exclude: true };
12789
+ }
12790
+ const seo = {
12791
+ description: clip(plainProse(spec.description), META_DESCRIPTION_MAX) || `${apiName(spec)} API reference.`
12792
+ };
12793
+ if (reference?.noindex) {
12794
+ seo.noindex = true;
12795
+ }
12110
12796
  return {
12111
12797
  body: [
12112
12798
  withDescription(spec.description, `<ApiOverview source="${spec.slug}" />`),
@@ -12115,12 +12801,8 @@ var overviewMdx = (spec, reference) => {
12115
12801
 
12116
12802
  `),
12117
12803
  data: {
12118
- ...reference?.includeInLlms === false ? { ai: { exclude: true } } : {},
12119
- ...reference?.includeInSearch === false ? { search: { exclude: true } } : {},
12120
- seo: {
12121
- description: clip(plainProse(spec.description), META_DESCRIPTION_MAX) || `${apiName(spec)} API reference.`,
12122
- ...reference?.noindex ? { noindex: true } : {}
12123
- },
12804
+ ...flags,
12805
+ seo,
12124
12806
  sidebar: { label: "Overview" },
12125
12807
  title: apiName(spec)
12126
12808
  }
@@ -12128,14 +12810,14 @@ var overviewMdx = (spec, reference) => {
12128
12810
  };
12129
12811
 
12130
12812
  // src/openapi/source.ts
12131
- var isOpenApiSource = (source) => source.kind === "openapi-source";
12813
+ var isOpenApiSource = (source) => ("kind" in source) && source.kind === "openapi-source";
12132
12814
  var routeToRef = (route) => route.replace(/^\/+/u, "");
12133
12815
  var toEntry = (rendered, ref) => {
12134
12816
  const raw = frontmatter_default.stringify(`${rendered.body}
12135
12817
  `, rendered.data);
12136
12818
  return {
12137
12819
  body: { format: "mdx", text: rendered.body },
12138
- data: rendered.data,
12820
+ data: { ...rendered.data },
12139
12821
  hash: hashText(raw),
12140
12822
  raw,
12141
12823
  ref
@@ -12154,22 +12836,43 @@ var tagFolderMeta = (spec, tags) => {
12154
12836
  { title: tag.name }
12155
12837
  ]));
12156
12838
  };
12839
+ var parseReference = async (reference, ctx) => {
12840
+ const options = { cacheDir: ctx.cacheDir, refresh: ctx.refresh };
12841
+ if (reference.kind === "asyncapi") {
12842
+ const { document: document2, warnings: warnings2 } = await parseAsyncApiSpec(reference.spec, ctx.projectRoot, options);
12843
+ const extracted2 = extractAsyncApiOperations(document2, reference.route);
12844
+ return {
12845
+ document: document2,
12846
+ extractWarnings: extracted2.warnings,
12847
+ operations: extracted2.operations,
12848
+ tags: extracted2.tags,
12849
+ warnings: warnings2
12850
+ };
12851
+ }
12852
+ const { document, warnings } = await parseSpec(reference.spec, ctx.projectRoot, options);
12853
+ const extracted = extractOperations(document, reference.route);
12854
+ return {
12855
+ document,
12856
+ extractWarnings: extracted.warnings,
12857
+ operations: extracted.operations,
12858
+ tags: extracted.tags,
12859
+ warnings
12860
+ };
12861
+ };
12157
12862
  var openApiSource = (references, ctx) => {
12158
12863
  let parsed = {};
12159
12864
  const loadReference = async (reference) => {
12865
+ const kindLabel = reference.kind === "asyncapi" ? "AsyncAPI" : "OpenAPI";
12866
+ const codePrefix = reference.kind === "asyncapi" ? "BLUME_ASYNCAPI" : "BLUME_OPENAPI";
12160
12867
  try {
12161
- const { document, warnings } = await parseSpec(reference.spec, ctx.projectRoot, { cacheDir: ctx.cacheDir, refresh: ctx.refresh });
12162
- const {
12163
- operations,
12164
- tags,
12165
- warnings: extractWarnings
12166
- } = extractOperations(document, reference.route);
12868
+ const { document, warnings, operations, tags, extractWarnings } = await parseReference(reference, ctx);
12167
12869
  const info = document.info ?? { title: reference.label, version: "" };
12168
12870
  const spec = {
12169
12871
  codeSamples: reference.display.codeSamples,
12170
12872
  description: info.description ?? "",
12171
12873
  document,
12172
12874
  expandSchemas: reference.display.expandSchemas,
12875
+ kind: reference.kind,
12173
12876
  label: reference.label,
12174
12877
  operations: Object.fromEntries(operations.map((operation) => [
12175
12878
  operation.key,
@@ -12187,21 +12890,21 @@ var openApiSource = (references, ctx) => {
12187
12890
  return {
12188
12891
  diagnostics: [
12189
12892
  ...warnings.map((message) => ({
12190
- code: "BLUME_OPENAPI_STALE",
12893
+ code: reference.kind === "asyncapi" ? "BLUME_ASYNCAPI_SPEC_WARNING" : "BLUME_OPENAPI_STALE",
12191
12894
  message,
12192
12895
  severity: "warning"
12193
12896
  })),
12194
12897
  ...extractWarnings.map((message) => ({
12195
- code: "BLUME_OPENAPI_REF_PATH_ITEM",
12196
- message: `In OpenAPI spec "${reference.spec}": ${message}`,
12898
+ code: reference.kind === "asyncapi" ? "BLUME_ASYNCAPI_SKIPPED_OPERATION" : "BLUME_OPENAPI_REF_PATH_ITEM",
12899
+ message: `In ${kindLabel} spec "${reference.spec}": ${message}`,
12197
12900
  severity: "warning"
12198
12901
  })),
12199
12902
  ...operations.length === 0 ? [
12200
12903
  {
12201
- code: "BLUME_OPENAPI_EMPTY",
12202
- message: `OpenAPI spec "${reference.spec}" for ${reference.route} declares no operations; its API reference is empty.`,
12904
+ code: `${codePrefix}_EMPTY`,
12905
+ message: `${kindLabel} spec "${reference.spec}" for ${reference.route} declares no operations; its API reference is empty.`,
12203
12906
  severity: "warning",
12204
- suggestion: "Check the spec points at an OpenAPI document with operations under `paths`."
12907
+ suggestion: reference.kind === "asyncapi" ? "Check the spec points at an AsyncAPI document with `channels` and `operations`." : "Check the spec points at an OpenAPI document with operations under `paths`."
12205
12908
  }
12206
12909
  ] : []
12207
12910
  ],
@@ -12212,10 +12915,10 @@ var openApiSource = (references, ctx) => {
12212
12915
  };
12213
12916
  } catch (error) {
12214
12917
  return {
12215
- code: "BLUME_OPENAPI_UNAVAILABLE",
12216
- message: `Could not load OpenAPI spec "${reference.spec}" for ${reference.route} (${error.message}); its reference pages were skipped.`,
12918
+ code: `${codePrefix}_UNAVAILABLE`,
12919
+ message: `Could not load ${kindLabel} spec "${reference.spec}" for ${reference.route} (${error.message}); its reference pages were skipped.`,
12217
12920
  severity: ctx.mode === "build" ? "error" : "warning",
12218
- suggestion: error instanceof InvalidSpecError ? "Point the spec at an OpenAPI document (a YAML or JSON file with an object at the top level)." : "Check the spec URL/path is reachable from the build environment; behind a proxy, set HTTP(S)_PROXY."
12921
+ suggestion: error instanceof InvalidSpecError ? `Point the spec at ${reference.kind === "asyncapi" ? "an AsyncAPI" : "an OpenAPI"} document (a YAML or JSON file with an object at the top level).` : "Check the spec URL/path is reachable from the build environment; behind a proxy, set HTTP(S)_PROXY."
12219
12922
  };
12220
12923
  }
12221
12924
  };
@@ -12320,6 +13023,7 @@ import { fromMarkdown as fromMarkdown3 } from "mdast-util-from-markdown";
12320
13023
  import { gfmFromMarkdown } from "mdast-util-gfm";
12321
13024
  import { toString as mdastToString2 } from "mdast-util-to-string";
12322
13025
  import { gfm } from "micromark-extension-gfm";
13026
+ import stringWidth4 from "string-width";
12323
13027
  var DEFAULT_BASE_URL = "https://api.github.com";
12324
13028
  var DEFAULT_LIMIT = 100;
12325
13029
  var PER_PAGE = 100;
@@ -12329,35 +13033,24 @@ var EDGE_DASHES = /^-+|-+$/gu;
12329
13033
  var DESCRIPTION_MAX = 160;
12330
13034
  var DESCRIPTION_MIN = 110;
12331
13035
  var CHANGESET_HASH = /^(?<mark>\s*(?:[-*+]|\d+[.)])\s+)[0-9a-f]{7,40}:\s+/gmu;
12332
- var WHITESPACE2 = /\s+/gu;
13036
+ var WHITESPACE3 = /\s+/gu;
12333
13037
  var TRAILING_FRAGMENT = /[\s,;:.—–-]+$/u;
12334
13038
  var NON_PROSE = new Set(["code", "heading", "html", "thematicBreak"]);
12335
- var graphemePrefix = (text, max) => {
12336
- let end = 0;
12337
- const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
12338
- for (const { index, segment } of graphemes.segment(text)) {
12339
- if (index + segment.length > max) {
12340
- break;
12341
- }
12342
- end = index + segment.length;
12343
- }
12344
- return text.slice(0, end);
12345
- };
12346
13039
  var releaseDescription = (body) => {
12347
13040
  const tree = fromMarkdown3(body.replaceAll(CHANGESET_HASH, "$<mark>"), {
12348
13041
  extensions: [gfm()],
12349
13042
  mdastExtensions: [gfmFromMarkdown()]
12350
13043
  });
12351
- const text = tree.children.filter((node) => !NON_PROSE.has(node.type)).map((node) => mdastToString2(node, { includeHtml: false, includeImageAlt: false })).join(" ").replaceAll(WHITESPACE2, " ").trim();
13044
+ const text = tree.children.filter((node) => !NON_PROSE.has(node.type)).map((node) => mdastToString2(node, { includeHtml: false, includeImageAlt: false })).join(" ").replaceAll(WHITESPACE3, " ").trim();
12352
13045
  if (!text) {
12353
13046
  return;
12354
13047
  }
12355
- if (text.length <= DESCRIPTION_MAX) {
13048
+ if (stringWidth4(text) <= DESCRIPTION_MAX) {
12356
13049
  return text;
12357
13050
  }
12358
- const slice = graphemePrefix(text, DESCRIPTION_MAX - 1);
13051
+ const slice = columnsPrefix(text, DESCRIPTION_MAX - 1);
12359
13052
  const boundary = slice.lastIndexOf(" ");
12360
- const head = (boundary >= DESCRIPTION_MIN ? slice.slice(0, boundary) : slice).replace(TRAILING_FRAGMENT, "");
13053
+ const head = (boundary !== -1 && stringWidth4(slice.slice(0, boundary)) >= DESCRIPTION_MIN ? slice.slice(0, boundary) : slice).replace(TRAILING_FRAGMENT, "");
12361
13054
  return `${head}…`;
12362
13055
  };
12363
13056
  var slugifyTag = (tag) => tag.toLowerCase().replaceAll(NON_SLUG2, "-").replaceAll(EDGE_DASHES, "");
@@ -12380,18 +13073,20 @@ var releaseToEntry = (release) => {
12380
13073
  const description = releaseDescription(body);
12381
13074
  const data = {
12382
13075
  changelog: { category, version },
12383
- date,
12384
- ...description ? { seo: { description } } : {},
12385
- title,
12386
- type: "changelog"
13076
+ date
12387
13077
  };
13078
+ if (description) {
13079
+ data.seo = { description };
13080
+ }
13081
+ data.title = title;
13082
+ data.type = "changelog";
12388
13083
  const raw = frontmatter_default.stringify(`${body}
12389
13084
  `, data);
12390
13085
  const fallbackRef = `release-${release.id}`;
12391
13086
  const ref = `${slugifyTag(release.tag_name) || fallbackRef}.md`;
12392
13087
  return {
12393
13088
  body: { format: "md", text: body },
12394
- data,
13089
+ data: { ...data },
12395
13090
  editUrl: release.html_url,
12396
13091
  hash: hashText(raw),
12397
13092
  lastModified: date,
@@ -12688,7 +13383,12 @@ var richToMarkdown = (rich = []) => rich.map((node) => {
12688
13383
  }
12689
13384
  return node.href ? `[${text}](${node.href})` : text;
12690
13385
  }).join("");
12691
- var blockField = (block) => block[block.type]?.rich_text ?? [];
13386
+ var isBlockPayload = (value) => typeof value === "object";
13387
+ var payloadOf = (block) => {
13388
+ const value = block[block.type];
13389
+ return isBlockPayload(value) ? value : undefined;
13390
+ };
13391
+ var blockField = (block) => payloadOf(block)?.rich_text ?? [];
12692
13392
  var RATE_LIMITED = 429;
12693
13393
  var MAX_RETRIES = 4;
12694
13394
  var BASE_DELAY_MS = 500;
@@ -12720,7 +13420,7 @@ var LIST_BLOCKS = new Set([
12720
13420
  ]);
12721
13421
  var isListItem = (block) => block !== undefined && LIST_BLOCKS.has(block.type);
12722
13422
  var renderLeaf = (block) => {
12723
- const data = block[block.type] ?? {};
13423
+ const data = payloadOf(block) ?? {};
12724
13424
  const text = richToMarkdown(blockField(block));
12725
13425
  switch (block.type) {
12726
13426
  case "paragraph": {
@@ -12756,9 +13456,8 @@ ${text}
12756
13456
  \`\`\``;
12757
13457
  }
12758
13458
  case "image": {
12759
- const media = data;
12760
- const url = media.external?.url ?? media.file?.url;
12761
- return url ? `![${richToMarkdown(media.caption)}](${url})` : "";
13459
+ const url = data.external?.url ?? data.file?.url;
13460
+ return url ? `![${richToMarkdown(data.caption)}](${url})` : "";
12762
13461
  }
12763
13462
  default: {
12764
13463
  return null;
@@ -12878,7 +13577,7 @@ ${nested}`;
12878
13577
  };
12879
13578
  const orderOf = (page) => {
12880
13579
  const order = page.properties[props.order ?? "Order"]?.number;
12881
- return typeof order === "number" ? order : undefined;
13580
+ return order === null ? undefined : order;
12882
13581
  };
12883
13582
  const frontmatter = (page) => {
12884
13583
  const data = {};
@@ -12965,14 +13664,14 @@ ${nested}`;
12965
13664
  import { join as join16 } from "pathe";
12966
13665
 
12967
13666
  // src/core/sources/portable-text.ts
12968
- var HEADING_STYLES = {
12969
- h1: "# ",
12970
- h2: "## ",
12971
- h3: "### ",
12972
- h4: "#### ",
12973
- h5: "##### ",
12974
- h6: "###### "
12975
- };
13667
+ var HEADING_STYLES = new Map([
13668
+ ["h1", "# "],
13669
+ ["h2", "## "],
13670
+ ["h3", "### "],
13671
+ ["h4", "#### "],
13672
+ ["h5", "##### "],
13673
+ ["h6", "###### "]
13674
+ ]);
12976
13675
  var MARKDOWN_SPECIALS = /[\\`*_[\]~<]/gu;
12977
13676
  var escapeText = (text) => text.replaceAll(MARKDOWN_SPECIALS, String.raw`\$&`);
12978
13677
  var renderSpan = (span, defs) => {
@@ -13014,6 +13713,7 @@ var renderChildren = (block) => {
13014
13713
  const defs = new Map((block.markDefs ?? []).map((def) => [def._key, def]));
13015
13714
  return (block.children ?? []).map((span) => renderSpan(span, defs)).join("");
13016
13715
  };
13716
+ var isAltText = (value) => typeof value === "string";
13017
13717
  var renderBlock = (block, options) => {
13018
13718
  const custom = options.serializers?.[block._type];
13019
13719
  if (custom) {
@@ -13021,7 +13721,7 @@ var renderBlock = (block, options) => {
13021
13721
  }
13022
13722
  if (block._type === "image") {
13023
13723
  const url = options.imageUrl?.(block);
13024
- const alt = typeof block.alt === "string" ? block.alt : "";
13724
+ const alt = isAltText(block.alt) ? block.alt : "";
13025
13725
  return url ? `![${alt}](${url})` : "";
13026
13726
  }
13027
13727
  if (block._type !== "block") {
@@ -13036,7 +13736,7 @@ var renderBlock = (block, options) => {
13036
13736
  if (block.style === "blockquote") {
13037
13737
  return `> ${inline}`;
13038
13738
  }
13039
- return `${HEADING_STYLES[block.style ?? "normal"] ?? ""}${inline}`;
13739
+ return `${HEADING_STYLES.get(block.style ?? "normal") ?? ""}${inline}`;
13040
13740
  };
13041
13741
  var portableTextToMarkdown = (blocks, options = {}) => {
13042
13742
  const lines = blocks.map((block) => renderBlock(block, options));
@@ -13059,10 +13759,11 @@ var portableTextToMarkdown = (blocks, options = {}) => {
13059
13759
 
13060
13760
  // src/core/sources/sanity.ts
13061
13761
  var IMAGE_REF = /^image-(?<id>[a-f0-9]+)-(?<dims>\d+x\d+)-(?<ext>\w+)$/u;
13762
+ var isTraversable = (value) => typeof value === "object" && value !== null;
13062
13763
  var getPath = (doc, path) => {
13063
13764
  let current = doc;
13064
13765
  for (const key of path.split(".")) {
13065
- if (current && typeof current === "object") {
13766
+ if (isTraversable(current)) {
13066
13767
  current = current[key];
13067
13768
  } else {
13068
13769
  return;
@@ -13070,7 +13771,8 @@ var getPath = (doc, path) => {
13070
13771
  }
13071
13772
  return current;
13072
13773
  };
13073
- var asString = (value) => typeof value === "string" ? value : undefined;
13774
+ var isStringValue2 = (value) => typeof value === "string";
13775
+ var asString = (value) => isStringValue2(value) ? value : undefined;
13074
13776
  var imageUrlFromRef = (ref, projectId, dataset) => {
13075
13777
  const match = ref.match(IMAGE_REF);
13076
13778
  if (!match?.groups) {
@@ -13129,7 +13831,7 @@ var sanitySource = (options, ctx) => {
13129
13831
  const raw = frontmatter_default.stringify(markdown, data);
13130
13832
  return {
13131
13833
  body: { format: "md", text: markdown },
13132
- data,
13834
+ data: { ...data },
13133
13835
  hash: hashText(raw),
13134
13836
  lastModified: asString(getPath(doc, fields.lastModified ?? "_updatedAt")),
13135
13837
  raw,
@@ -13354,7 +14056,8 @@ var normalizeLoadedEntries = (loaded, config) => {
13354
14056
  prefix: source.prefix,
13355
14057
  staged: source.staged
13356
14058
  },
13357
- typeFrontmatter
14059
+ typeFrontmatter,
14060
+ versions: config.versions
13358
14061
  });
13359
14062
  if (normalized.pages.length === 0 && normalized.diagnostics.length > 0) {
13360
14063
  droppedPages += 1;
@@ -13387,7 +14090,10 @@ var scanProject = async (root, options = {}) => {
13387
14090
  const localeDirs = config.i18n && config.i18n.parser === "dir" ? config.i18n.locales.flatMap((locale) => locale.code === config.i18n?.defaultLocale ? [] : [locale.code]) : undefined;
13388
14091
  const [loaded, folderMeta] = await Promise.all([
13389
14092
  Promise.all(sources.map(async (source) => ({ source, ...await source.load() }))),
13390
- discoverFolderMeta(metaSources, { localeDirs })
14093
+ discoverFolderMeta(metaSources, {
14094
+ localeDirs,
14095
+ versionDirs: config.versions?.archived.map((version) => version.id)
14096
+ })
13391
14097
  ]);
13392
14098
  const sharedFolderMeta = new Map([
13393
14099
  ...loaded.flatMap(({ folderMeta: sourceMeta }) => Object.entries(sourceMeta ?? {})),
@@ -13400,6 +14106,7 @@ var scanProject = async (root, options = {}) => {
13400
14106
  } = normalizeLoadedEntries(loaded, config);
13401
14107
  const pages = mode === "build" && !preview ? allPages.filter((page) => !page.meta.draft) : allPages;
13402
14108
  const lastModified = resolveLastModifiedConfig(config.lastModified);
14109
+ const lastModifiedWarnings = [];
13403
14110
  if (lastModified.enabled && lastModified.source === "git") {
13404
14111
  const fsPaths = pages.map((page) => page.sourcePath).filter((path) => path !== undefined);
13405
14112
  const contentRoots = sources.flatMap((source) => source.staged || !source.contentRoot ? [] : [source.contentRoot]);
@@ -13409,16 +14116,20 @@ var scanProject = async (root, options = {}) => {
13409
14116
  page.lastModified = gitTimes.get(page.sourcePath);
13410
14117
  }
13411
14118
  }
14119
+ const undated = pages.filter((page) => page.sourcePath && !page.lastModified).length;
14120
+ lastModifiedWarnings.push(...lastModifiedShallowWarning(context.root, undated));
13412
14121
  }
13413
14122
  const graph = buildContentGraph(pages, {
13414
14123
  basePath: config.basePath,
13415
14124
  folderMeta: folderMeta.meta,
13416
14125
  i18n: config.i18n,
13417
14126
  navigation: config.navigation,
13418
- sharedFolderMeta
14127
+ sharedFolderMeta,
14128
+ versions: config.versions
13419
14129
  });
13420
14130
  const manifest = buildManifest({ config, context, graph });
13421
- const i18nWarnings = config.i18n ? i18nDiagnostics(pages, config.i18n) : [];
14131
+ const i18nWarnings = config.i18n ? i18nDiagnostics(pages, config.i18n, config.versions) : [];
14132
+ const versionWarnings = config.versions ? versionsDiagnostics(pages, config.versions) : [];
13422
14133
  return {
13423
14134
  config,
13424
14135
  context,
@@ -13427,7 +14138,9 @@ var scanProject = async (root, options = {}) => {
13427
14138
  ...folderMeta.diagnostics,
13428
14139
  ...entryIdDiagnostics(pages, resolveDocsCollection(config, context).base),
13429
14140
  ...graph.diagnostics,
13430
- ...i18nWarnings
14141
+ ...i18nWarnings,
14142
+ ...versionWarnings,
14143
+ ...lastModifiedWarnings
13431
14144
  ],
13432
14145
  droppedPages,
13433
14146
  graph,
@@ -13547,9 +14260,10 @@ var auditCommand = defineCommand2({
13547
14260
  return;
13548
14261
  }
13549
14262
  const root = process.cwd();
13550
- const gate = args["fail-on"] ?? (args.strict ? "warning" : "error");
13551
- if (!SEVERITIES.includes(gate)) {
13552
- logger.error(`Invalid --fail-on "${gate}" (use ${SEVERITIES.join(" | ")}).`);
14263
+ const gateInput = args["fail-on"] ?? (args.strict ? "warning" : "error");
14264
+ const gate = SEVERITIES.find((severity) => severity === gateInput);
14265
+ if (gate === undefined) {
14266
+ logger.error(`Invalid --fail-on "${gateInput}" (use ${SEVERITIES.join(" | ")}).`);
13553
14267
  process.exit(1);
13554
14268
  }
13555
14269
  const agents = Object.keys(AGENTS).filter((kind) => args[kind]);
@@ -13748,12 +14462,8 @@ var buildAgentReadability = (project) => {
13748
14462
  return site ? absoluteUrl(site, based) : based;
13749
14463
  };
13750
14464
  const negotiates = config.deployment.output === "server" && (config.deployment.adapter === "vercel" || config.deployment.adapter === "cloudflare");
13751
- const artifacts = {
13752
- markdown: {
13753
- ...negotiates ? { contentNegotiation: "text/markdown" } : {},
13754
- pattern: abs("/{route}.md")
13755
- }
13756
- };
14465
+ const markdown = negotiates ? { contentNegotiation: "text/markdown", pattern: abs("/{route}.md") } : { pattern: abs("/{route}.md") };
14466
+ const artifacts = { markdown };
13757
14467
  if (config.ai.llmsTxt.enabled) {
13758
14468
  artifacts.llmsFullTxt = abs("/llms-full.txt");
13759
14469
  artifacts.llmsTxt = abs("/llms.txt");
@@ -13934,6 +14644,8 @@ var parseYouTubeId = (input) => {
13934
14644
  };
13935
14645
 
13936
14646
  // src/ai/component-markdown.ts
14647
+ var isString4 = (value) => typeof value === "string";
14648
+ var isNumber = (value) => typeof value === "number";
13937
14649
  var evaluateExpression = (raw, frontmatter) => {
13938
14650
  try {
13939
14651
  const value = new Function("frontmatter", `"use strict"; return (${raw});`)(frontmatter);
@@ -13952,7 +14664,7 @@ var readProps = (node, frontmatter) => {
13952
14664
  }
13953
14665
  if (attribute.value === null || attribute.value === undefined) {
13954
14666
  props[attribute.name] = true;
13955
- } else if (typeof attribute.value === "string") {
14667
+ } else if (isString4(attribute.value)) {
13956
14668
  props[attribute.name] = attribute.value;
13957
14669
  } else {
13958
14670
  const result = evaluateExpression(attribute.value.value, frontmatter);
@@ -14003,9 +14715,10 @@ var cellCode = (value) => {
14003
14715
  const text = cellText(value);
14004
14716
  return text && !text.includes("`") ? `\`${text}\`` : text;
14005
14717
  };
14718
+ var isTypeEntryMap = (value) => typeof value === "object" && value !== null;
14006
14719
  var typeTable = ({ children, props }) => {
14007
14720
  const { type } = props;
14008
- if (type === null || typeof type !== "object") {
14721
+ if (!isTypeEntryMap(type)) {
14009
14722
  return null;
14010
14723
  }
14011
14724
  const entries = Object.entries(type);
@@ -14013,7 +14726,7 @@ var typeTable = ({ children, props }) => {
14013
14726
  const prop = cellCode(`${name}${info.required ? "" : "?"}`);
14014
14727
  const typeCell = info.typeDescriptionLink ? `[${cellCode(info.type)}](${cellText(info.typeDescriptionLink)})` : cellCode(info.type);
14015
14728
  const defaultCell = info.default === undefined ? "-" : cellCode(info.default);
14016
- const description = cellText([info.description, info.typeDescription].filter((part) => typeof part === "string" && part !== "").join(" "));
14729
+ const description = cellText([info.description, info.typeDescription].filter((part) => isString4(part) && part !== "").join(" "));
14017
14730
  return [prop, typeCell, defaultCell, description];
14018
14731
  });
14019
14732
  const table = rows.length > 0 ? markdownTable([["Prop", "Type", "Default", "Description"], ...rows], {
@@ -14024,8 +14737,8 @@ var typeTable = ({ children, props }) => {
14024
14737
  `);
14025
14738
  };
14026
14739
  var callout = ({ children, props }) => {
14027
- const type = typeof props.type === "string" ? props.type : "info";
14028
- const label = typeof props.title === "string" && props.title !== "" ? props.title : type.charAt(0).toUpperCase() + type.slice(1);
14740
+ const type = isString4(props.type) ? props.type : "info";
14741
+ const label = isString4(props.title) && props.title !== "" ? props.title : type.charAt(0).toUpperCase() + type.slice(1);
14029
14742
  if (!children) {
14030
14743
  return `> **${label}**`;
14031
14744
  }
@@ -14053,7 +14766,7 @@ var steps = ({ childComponents, children }) => {
14053
14766
  return children;
14054
14767
  }
14055
14768
  return items.map((step, index) => {
14056
- const title = typeof step.props.title === "string" && step.props.title !== "" ? `**${step.props.title}**` : "";
14769
+ const title = isString4(step.props.title) && step.props.title !== "" ? `**${step.props.title}**` : "";
14057
14770
  const content = [title, step.children].filter(Boolean).join(`
14058
14771
 
14059
14772
  `);
@@ -14068,7 +14781,7 @@ var tabs = ({ childComponents, children }) => {
14068
14781
  return children;
14069
14782
  }
14070
14783
  return items.map((tab, index) => {
14071
- const title = typeof tab.props.title === "string" && tab.props.title !== "" ? tab.props.title : `Tab ${index + 1}`;
14784
+ const title = isString4(tab.props.title) && tab.props.title !== "" ? tab.props.title : `Tab ${index + 1}`;
14072
14785
  return tab.children ? `**${title}**
14073
14786
 
14074
14787
  ${tab.children}` : `**${title}**`;
@@ -14078,17 +14791,17 @@ ${tab.children}` : `**${title}**`;
14078
14791
  };
14079
14792
  var youtube = ({ props }) => {
14080
14793
  let input = "";
14081
- if (typeof props.id === "string") {
14794
+ if (isString4(props.id)) {
14082
14795
  input = props.id;
14083
- } else if (typeof props.url === "string") {
14796
+ } else if (isString4(props.url)) {
14084
14797
  input = props.url;
14085
14798
  }
14086
14799
  const videoId = parseYouTubeId(input);
14087
14800
  if (!videoId) {
14088
14801
  return null;
14089
14802
  }
14090
- const start = typeof props.start === "number" && props.start > 0 ? `&t=${Math.floor(props.start)}s` : "";
14091
- const title = typeof props.title === "string" && props.title !== "" ? props.title : "Watch on YouTube";
14803
+ const start = isNumber(props.start) && props.start > 0 ? `&t=${Math.floor(props.start)}s` : "";
14804
+ const title = isString4(props.title) && props.title !== "" ? props.title : "Watch on YouTube";
14092
14805
  return `[${title}](https://www.youtube.com/watch?v=${videoId}${start})`;
14093
14806
  };
14094
14807
  var fencedBlock = (lang, code) => {
@@ -14102,7 +14815,7 @@ ${fence}`;
14102
14815
  };
14103
14816
  var exampleComponentSerializers = (examples) => ({
14104
14817
  Component: ({ props }) => {
14105
- const path = typeof props.path === "string" ? props.path : undefined;
14818
+ const path = isString4(props.path) ? props.path : undefined;
14106
14819
  const example = path === undefined ? undefined : examples[path];
14107
14820
  return example ? fencedBlock(example.lang, example.source) : null;
14108
14821
  }
@@ -14212,19 +14925,28 @@ var pageUrl = (route, site, base = "") => {
14212
14925
  const path = withBasePath(base, route);
14213
14926
  return encodeURI(site ? absoluteUrl(site, path) : path);
14214
14927
  };
14215
- var eligiblePages = (project) => project.graph.pages.filter((page) => !(page.meta.ai.exclude || page.meta.draft || page.meta.sidebar.hidden || page.meta.seo.noindex && page.source.name !== "openapi") && (project.config.ai.llmsTxt.openapi || page.source.name !== "openapi"));
14928
+ var eligiblePages = (project, options = {}) => project.graph.pages.filter((page) => !(page.meta.ai.exclude || page.meta.draft || page.meta.sidebar.hidden || page.meta.seo.noindex && page.source.name !== "openapi" || options.versions === "current" && page.version !== "") && (project.config.ai.llmsTxt.openapi || page.source.name !== "openapi"));
14216
14929
  var indexedNavigations = (project) => {
14217
- const { i18n } = project.config;
14218
- if (i18n) {
14219
- return i18n.locales.flatMap(({ code, label }) => {
14220
- const nav = project.graph.navigationByLocale[code];
14221
- if (!nav) {
14222
- return [];
14223
- }
14224
- return [{ label: code === i18n.defaultLocale ? undefined : label, nav }];
14930
+ const { i18n, versions } = project.config;
14931
+ const current = i18n ? i18n.locales.flatMap(({ code, label }) => {
14932
+ const nav = project.graph.navigationByLocale[code];
14933
+ if (!nav) {
14934
+ return [];
14935
+ }
14936
+ return [
14937
+ { label: code === i18n.defaultLocale ? undefined : label, nav }
14938
+ ];
14939
+ }) : [{ nav: project.graph.navigation }];
14940
+ const archived = (versions?.archived ?? []).flatMap((version) => {
14941
+ const byLocale = project.graph.navigationByVersion[version.id] ?? {};
14942
+ const versionLabel = `${version.label ?? version.id} (archived)`;
14943
+ return Object.entries(byLocale).flatMap(([code, nav]) => {
14944
+ const locale = i18n?.locales.find((entry) => entry.code === code);
14945
+ const label = locale && code !== i18n?.defaultLocale ? `${locale.label} — ${versionLabel}` : versionLabel;
14946
+ return [{ label, nav }];
14225
14947
  });
14226
- }
14227
- return [{ nav: project.graph.navigation }];
14948
+ });
14949
+ return [...current, ...archived];
14228
14950
  };
14229
14951
  var buildLlmsIndex = (project) => {
14230
14952
  const { config } = project;
@@ -14301,7 +15023,7 @@ var buildLlmsIndex = (project) => {
14301
15023
  };
14302
15024
  var buildFull = async (project) => {
14303
15025
  const { config } = project;
14304
- const pages = eligiblePages(project).toSorted((a, b) => a.route.localeCompare(b.route));
15026
+ const pages = eligiblePages(project, { versions: "current" }).toSorted((a, b) => a.route.localeCompare(b.route));
14305
15027
  const components = {
14306
15028
  ...exampleComponentSerializers(project.examples ?? {}),
14307
15029
  ...config.ai.markdownComponents
@@ -14446,6 +15168,7 @@ var collectEntries = async (dir, prefix = "") => {
14446
15168
  }
14447
15169
  return entries;
14448
15170
  };
15171
+ var isString5 = (value) => typeof value === "string";
14449
15172
  var skillMeta = (raw, dirName) => {
14450
15173
  let data;
14451
15174
  try {
@@ -14456,8 +15179,8 @@ var skillMeta = (raw, dirName) => {
14456
15179
  warning: `Skill "${dirName}" has unparsable SKILL.md frontmatter; skipped.`
14457
15180
  };
14458
15181
  }
14459
- const name = typeof data.name === "string" ? data.name : "";
14460
- const description = typeof data.description === "string" ? data.description : "";
15182
+ const name = isString5(data.name) ? data.name : "";
15183
+ const description = isString5(data.description) ? data.description : "";
14461
15184
  if (!(name && description)) {
14462
15185
  return {
14463
15186
  meta: null,
@@ -14582,6 +15305,8 @@ var MAX_RULES = 100;
14582
15305
  var MAX_RULE_LENGTH = 100;
14583
15306
  var FALLBACK_RULES = ["/*", "!/_astro/*", "!/*.md", "!/*.mdx", "!/*.txt"];
14584
15307
  var basePrefix = (base) => base && base !== "/" ? base.replace(/\/$/u, "") : "";
15308
+ var isJsonObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
15309
+ var isJsonString = (value) => typeof value === "string";
14585
15310
  var isNegativeRule = (rule) => rule.startsWith("!");
14586
15311
  var ruleBody = (rule) => isNegativeRule(rule) ? rule.slice(1) : rule;
14587
15312
  var buildRunWorkerFirstRules = (routePaths, base) => {
@@ -14836,15 +15561,15 @@ var injectWorkerNegotiation = (wranglerText, options) => {
14836
15561
  } catch {
14837
15562
  return null;
14838
15563
  }
14839
- if (config === null || typeof config !== "object" || Array.isArray(config)) {
15564
+ if (!isJsonObject(config)) {
14840
15565
  return null;
14841
15566
  }
14842
15567
  const { main } = config;
14843
- if (typeof main !== "string" || main.length === 0 || main === NEGOTIATION_WORKER_FILE) {
15568
+ if (!isJsonString(main) || main.length === 0 || main === NEGOTIATION_WORKER_FILE) {
14844
15569
  return null;
14845
15570
  }
14846
- const assets = config.assets;
14847
- if (assets === null || typeof assets !== "object" || typeof assets.binding !== "string") {
15571
+ const { assets } = config;
15572
+ if (!isJsonObject(assets) || !isJsonString(assets.binding)) {
14848
15573
  return null;
14849
15574
  }
14850
15575
  const {
@@ -15059,6 +15784,16 @@ var buildSitemapFiles = (project) => {
15059
15784
  }
15060
15785
  const base = siteRoot(site);
15061
15786
  const deployBase = normalizeBasePath(project.config.deployment.base);
15787
+ const { versions } = project.config;
15788
+ const archivedById = new Map((versions?.archived ?? []).map((version) => [version.id, version]));
15789
+ const currentKeys = versions ? new Set(project.graph.pages.flatMap((page) => page.version === "" ? [`${page.versionKey}\x00${page.locale}`] : [])) : null;
15790
+ const archivedExcluded = (page) => {
15791
+ if (!page.version) {
15792
+ return false;
15793
+ }
15794
+ const archived = archivedById.get(page.version);
15795
+ return Boolean(archived?.noindex || archived?.canonical === "latest" && currentKeys?.has(`${page.versionKey}\x00${page.locale}`));
15796
+ };
15062
15797
  const seen = new Set;
15063
15798
  const urls = [];
15064
15799
  const pushUrl = (route, lastModified) => {
@@ -15070,7 +15805,7 @@ var buildSitemapFiles = (project) => {
15070
15805
  urls.push(` <url><loc>${loc}</loc>${lastmodTag(lastModified)}</url>`);
15071
15806
  };
15072
15807
  for (const page of project.graph.pages) {
15073
- if (page.meta.draft || page.meta.sidebar.hidden || page.meta.seo.noindex || ERROR_ROUTES3.has(page.route)) {
15808
+ if (page.meta.draft || page.meta.sidebar.hidden || page.meta.seo.noindex || ERROR_ROUTES3.has(page.route) || archivedExcluded(page)) {
15074
15809
  continue;
15075
15810
  }
15076
15811
  pushUrl(withBasePath(deployBase, page.route), page.lastModified);
@@ -15109,6 +15844,7 @@ ${references.join(`
15109
15844
 
15110
15845
  // src/deploy/vercel-negotiation.ts
15111
15846
  var ACCEPT_MARKDOWN_HEADER_VALUE = "(.*,)?\\s*text/(x-)?markdown(\\s*[;,].*)?$";
15847
+ var isString6 = (value) => typeof value === "string";
15112
15848
  var ACCEPT_MARKDOWN_CONDITION = [
15113
15849
  { key: "accept", type: "header", value: ACCEPT_MARKDOWN_HEADER_VALUE }
15114
15850
  ];
@@ -15163,12 +15899,14 @@ var buildNegotiationRoutes = (routePaths, homeTokens) => {
15163
15899
  return { headerRoutes, rewriteRoutes };
15164
15900
  };
15165
15901
  var HOME_SRC = "^/$";
15166
- var isNegotiationRoute = (route) => route.has?.some((condition) => condition.value === ACCEPT_MARKDOWN_HEADER_VALUE) === true || route.continue === true && route.headers?.vary === "Accept" && typeof route.src === "string" && Object.keys(route).length === 3 || route.continue === true && typeof route.headers?.link === "string" && route.src === HOME_SRC && Object.keys(route).length === 3;
15902
+ var TRAILING_SLASH_REDIRECT = {
15903
+ headers: { Location: "/$1" },
15904
+ src: "^/(.+)/$",
15905
+ status: 308
15906
+ };
15907
+ var isNegotiationRoute = (route) => route.has?.some((condition) => condition.value === ACCEPT_MARKDOWN_HEADER_VALUE) === true || route.continue === true && route.headers?.vary === "Accept" && isString6(route.src) && Object.keys(route).length === 3 || route.continue === true && isString6(route.headers?.link) && route.src === HOME_SRC && Object.keys(route).length === 3 || route.status === TRAILING_SLASH_REDIRECT.status && route.src === TRAILING_SLASH_REDIRECT.src;
15167
15908
  var injectNegotiationRoutes = (configText, routePaths, homeLinkHeader, contentTypeOverrides, homeTokens) => {
15168
15909
  const overrideEntries = Object.entries(contentTypeOverrides ?? {});
15169
- if (routePaths.length === 0 && !homeLinkHeader && overrideEntries.length === 0) {
15170
- return null;
15171
- }
15172
15910
  let config;
15173
15911
  try {
15174
15912
  config = JSON.parse(configText);
@@ -15194,7 +15932,7 @@ var injectNegotiationRoutes = (configText, routePaths, homeLinkHeader, contentTy
15194
15932
  src: HOME_SRC
15195
15933
  });
15196
15934
  }
15197
- routes.splice(filesystemIndex, 0, ...headerRoutes, ...rewriteRoutes);
15935
+ routes.splice(filesystemIndex, 0, ...headerRoutes, ...rewriteRoutes, TRAILING_SLASH_REDIRECT);
15198
15936
  config.routes = routes;
15199
15937
  return `${JSON.stringify(config, null, "\t")}
15200
15938
  `;
@@ -15220,6 +15958,7 @@ import { gfmFromMarkdown as gfmFromMarkdown2 } from "mdast-util-gfm";
15220
15958
  import { gfm as gfm2 } from "micromark-extension-gfm";
15221
15959
 
15222
15960
  // src/search/facets.ts
15961
+ var isFacetValue = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean";
15223
15962
  var pageFacets = (page, config) => {
15224
15963
  const declared = config.content.types[page.contentType]?.facets;
15225
15964
  if (!declared || declared.length === 0 || !page.custom) {
@@ -15228,7 +15967,7 @@ var pageFacets = (page, config) => {
15228
15967
  const facets = {};
15229
15968
  for (const key of declared) {
15230
15969
  const value = page.custom[key];
15231
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
15970
+ if (isFacetValue(value)) {
15232
15971
  facets[key] = String(value);
15233
15972
  }
15234
15973
  }
@@ -15237,7 +15976,7 @@ var pageFacets = (page, config) => {
15237
15976
 
15238
15977
  // src/search/documents.ts
15239
15978
  var HTML_OR_JSX = /<\/?[a-zA-Z][^\n<>]*>|<\/?>/gu;
15240
- var WHITESPACE3 = /\s+/gu;
15979
+ var WHITESPACE4 = /\s+/gu;
15241
15980
  var INLINE_PARENTS = new Set([
15242
15981
  "delete",
15243
15982
  "emphasis",
@@ -15289,7 +16028,7 @@ var toPlainText = (markdown) => {
15289
16028
  });
15290
16029
  const out = [];
15291
16030
  collectText(tree, out);
15292
- return out.join("").replaceAll(WHITESPACE3, " ").trim();
16031
+ return out.join("").replaceAll(WHITESPACE4, " ").trim();
15293
16032
  };
15294
16033
  var buildCrumbIndex = (sidebar) => {
15295
16034
  const index = new Map;
@@ -15323,7 +16062,11 @@ var buildCrumbIndex = (sidebar) => {
15323
16062
  var buildSearchDocuments = async (project, options) => {
15324
16063
  const pageById = new Map(project.graph.pages.map((page) => [page.id, page]));
15325
16064
  const byLocale = Object.values(project.graph.navigationByLocale ?? {});
15326
- const sidebars = byLocale.length > 0 ? byLocale.map((nav) => nav.sidebar) : [project.graph.navigation?.sidebar ?? []];
16065
+ const byVersion = Object.values(project.graph.navigationByVersion ?? {}).flatMap((locales) => Object.values(locales)).map((nav) => nav.sidebar);
16066
+ const sidebars = [
16067
+ ...byLocale.length > 0 ? byLocale.map((nav) => nav.sidebar) : [project.graph.navigation?.sidebar ?? []],
16068
+ ...byVersion
16069
+ ];
15327
16070
  const crumbs = new Map;
15328
16071
  for (const sidebar of sidebars) {
15329
16072
  for (const [route, crumb] of buildCrumbIndex(sidebar)) {
@@ -15346,18 +16089,22 @@ var buildSearchDocuments = async (project, options) => {
15346
16089
  const tags = page?.meta?.search?.tags;
15347
16090
  const crumb = crumbs.get(route.path);
15348
16091
  const facets = page ? pageFacets(page, project.config) : undefined;
15349
- return {
16092
+ const document = {
15350
16093
  breadcrumb: crumb?.breadcrumb ?? [],
15351
16094
  content: body,
15352
16095
  contentType: route.contentType,
15353
16096
  description: page?.description ?? "",
15354
- ...facets ? { facets } : {},
15355
16097
  locale: route.locale,
15356
16098
  route: route.path,
15357
16099
  section: crumb?.section || "Docs",
15358
16100
  tags: tags && tags.length > 0 ? tags : undefined,
15359
- title: route.title
16101
+ title: route.title,
16102
+ version: route.version
15360
16103
  };
16104
+ if (facets) {
16105
+ document.facets = facets;
16106
+ }
16107
+ return document;
15361
16108
  }));
15362
16109
  };
15363
16110
  var toSearchRecords = (documents) => documents.map((doc) => ({
@@ -15367,7 +16114,8 @@ var toSearchRecords = (documents) => documents.map((doc) => ({
15367
16114
  locale: doc.locale,
15368
16115
  tag: doc.tags?.[0],
15369
16116
  title: doc.title,
15370
- url: doc.route
16117
+ url: doc.route,
16118
+ version: doc.version || "current"
15371
16119
  }));
15372
16120
 
15373
16121
  // src/search/sync/algolia.ts
@@ -15451,7 +16199,8 @@ var syncTypesense = async (records, config) => {
15451
16199
  { name: "content", type: "string" },
15452
16200
  { name: "url", type: "string" },
15453
16201
  { facet: true, name: "tag", optional: true, type: "string" },
15454
- { facet: true, name: "locale", optional: true, type: "string" }
16202
+ { facet: true, name: "locale", optional: true, type: "string" },
16203
+ { facet: true, name: "version", optional: true, type: "string" }
15455
16204
  ],
15456
16205
  name: config.collection
15457
16206
  });
@@ -15462,7 +16211,8 @@ var syncTypesense = async (records, config) => {
15462
16211
  locale: record.locale,
15463
16212
  tag: record.tag,
15464
16213
  title: record.title,
15465
- url: record.url
16214
+ url: record.url,
16215
+ version: record.version
15466
16216
  }));
15467
16217
  await client.collections(config.collection).documents().import(documents, { action: "upsert" });
15468
16218
  };
@@ -15500,6 +16250,8 @@ import {
15500
16250
  import { join as join21 } from "pathe";
15501
16251
  var lockPath = (outDir) => join21(outDir, "dev.lock");
15502
16252
  var isValidPid = (pid) => typeof pid === "number" && Number.isInteger(pid) && pid > 0;
16253
+ var isPortNumber = (port) => typeof port === "number";
16254
+ var isLockRecord = (data) => typeof data === "object" && data !== null;
15503
16255
  var parseLock = (raw) => {
15504
16256
  let data;
15505
16257
  try {
@@ -15510,10 +16262,10 @@ var parseLock = (raw) => {
15510
16262
  if (isValidPid(data)) {
15511
16263
  return { pid: data };
15512
16264
  }
15513
- if (typeof data === "object" && data !== null) {
16265
+ if (isLockRecord(data)) {
15514
16266
  const { pid, port } = data;
15515
16267
  if (isValidPid(pid)) {
15516
- return typeof port === "number" ? { pid, port } : { pid };
16268
+ return isPortNumber(port) ? { pid, port } : { pid };
15517
16269
  }
15518
16270
  }
15519
16271
  return null;
@@ -15534,10 +16286,13 @@ var readDevLock = (outDir) => {
15534
16286
  const lock = parseLock(readFileSync5(path, "utf-8"));
15535
16287
  return lock && isProcessAlive(lock.pid) ? lock : null;
15536
16288
  };
15537
- var lockPayload = (port) => JSON.stringify({
15538
- pid: process.pid,
15539
- ...port === undefined ? {} : { port }
15540
- });
16289
+ var lockPayload = (port) => {
16290
+ const info = { pid: process.pid };
16291
+ if (port !== undefined) {
16292
+ info.port = port;
16293
+ }
16294
+ return JSON.stringify(info);
16295
+ };
15541
16296
  var writeLock = (outDir, port) => {
15542
16297
  writeFileSync(lockPath(outDir), lockPayload(port));
15543
16298
  };
@@ -15669,27 +16424,39 @@ var buildMcpData = async (project) => {
15669
16424
  }
15670
16425
  const page = pageById.get(route.id);
15671
16426
  const facets = page ? pageFacets(page, config) : undefined;
15672
- routes.push({
16427
+ const entry = {
15673
16428
  contentType: route.contentType,
15674
16429
  description: page?.description,
15675
- ...facets ? { facets } : {},
15676
16430
  indexable: route.indexable,
15677
16431
  lastModified: route.lastModified ?? null,
16432
+ locale: route.locale,
15678
16433
  route: route.path,
15679
- title: route.title
15680
- });
16434
+ title: route.title,
16435
+ version: route.version
16436
+ };
16437
+ if (facets) {
16438
+ entry.facets = facets;
16439
+ }
16440
+ routes.push(entry);
15681
16441
  }
15682
- return {
16442
+ const data = {
15683
16443
  base: normalizeBasePath(config.deployment.base),
15684
16444
  defaultLocale: config.i18n?.defaultLocale,
15685
- documents: documents.map((doc) => ({
15686
- content: doc.content,
15687
- contentType: doc.contentType,
15688
- description: doc.description,
15689
- ...doc.facets ? { facets: doc.facets } : {},
15690
- route: doc.route,
15691
- title: doc.title
15692
- })),
16445
+ documents: documents.map((doc) => {
16446
+ const document = {
16447
+ content: doc.content,
16448
+ contentType: doc.contentType,
16449
+ description: doc.description,
16450
+ locale: doc.locale,
16451
+ route: doc.route,
16452
+ title: doc.title,
16453
+ version: doc.version
16454
+ };
16455
+ if (doc.facets) {
16456
+ document.facets = doc.facets;
16457
+ }
16458
+ return document;
16459
+ }),
15693
16460
  instructions: config.ai.mcp.instructions,
15694
16461
  name: config.ai.mcp.name ?? config.title,
15695
16462
  navigation: graph.navigation,
@@ -15698,6 +16465,14 @@ var buildMcpData = async (project) => {
15698
16465
  site: config.deployment.site ?? null,
15699
16466
  version: manifest.blumeVersion
15700
16467
  };
16468
+ if (config.versions) {
16469
+ data.archivedVersions = config.versions.archived.map((version) => version.id);
16470
+ data.navigationByVersion = graph.navigationByVersion;
16471
+ }
16472
+ if (config.i18n) {
16473
+ data.navigationByLocale = graph.navigationByLocale;
16474
+ }
16475
+ return data;
15701
16476
  };
15702
16477
 
15703
16478
  // src/ai/mcp/tools.ts
@@ -15705,7 +16480,7 @@ var READ_ONLY = { openWorldHint: false, readOnlyHint: true };
15705
16480
  var MCP_TOOLS = [
15706
16481
  {
15707
16482
  annotations: READ_ONLY,
15708
- description: 'Full-text search across the documentation. Returns matching pages with their title, route, content type, and a short excerpt; pass `contentTypes` to search only pages of certain types (e.g. `rfc`, `changelog`), and `filters` to require facet values the site declares per type (e.g. `{"status": "enforced"}`). Use this first to discover relevant pages, then `get_page` to read one in full.',
16483
+ description: 'Full-text search across the documentation. Returns matching pages with their title, route, content type, and a short excerpt; pass `contentTypes` to search only pages of certain types (e.g. `rfc`, `changelog`), `filters` to require facet values the site declares per type (e.g. `{"status": "enforced"}`), and `locale` to search one language. On a versioned site results default to the current docs — pass `version` to search an archived version (e.g. `"v1.0"`) or `"all"` for every version. Use this first to discover relevant pages, then `get_page` to read one in full.',
15709
16484
  name: "search_docs",
15710
16485
  title: "Search documentation"
15711
16486
  },
@@ -15717,13 +16492,13 @@ var MCP_TOOLS = [
15717
16492
  },
15718
16493
  {
15719
16494
  annotations: READ_ONLY,
15720
- description: "List every documentation page with its route, title, description, content type, and any declared facet values; pass `contentTypes` and/or `filters` to narrow the list. Useful for enumerating the docs, discovering the types and facets in use, or finding a page when search is too narrow.",
16495
+ description: 'List every documentation page with its route, title, description, content type, and any declared facet values; pass `contentTypes`, `filters`, and/or `locale` to narrow the list. On a versioned site the list defaults to the current docs — pass `version` for an archived version or `"all"`. Useful for enumerating the docs, discovering the types and facets in use, or finding a page when search is too narrow.',
15721
16496
  name: "list_pages",
15722
16497
  title: "List pages"
15723
16498
  },
15724
16499
  {
15725
16500
  annotations: READ_ONLY,
15726
- description: "Return the documentation navigation tree (header tabs and the sidebar hierarchy), reflecting how the docs are organized for readers.",
16501
+ description: "Return the documentation navigation tree (header tabs and the sidebar hierarchy), reflecting how the docs are organized for readers. Pass `locale` for a language's tree and, on a versioned site, `version` for an archived snapshot's tree.",
15727
16502
  name: "get_navigation",
15728
16503
  title: "Get navigation"
15729
16504
  }
@@ -15761,12 +16536,11 @@ var reverseDnsName = (input) => {
15761
16536
  var HTTP_URL2 = /^https?:\/\//u;
15762
16537
  var buildMcpServerCard = (input) => {
15763
16538
  const url = serverUrl(input);
15764
- return {
16539
+ const card = {
15765
16540
  $schema: SERVER_CARD_SCHEMA,
15766
16541
  capabilities: { tools: { listChanged: false } },
15767
16542
  description: truncate(`Model Context Protocol server for the ${input.name} documentation.`),
15768
16543
  name: reverseDnsName(input),
15769
- ...HTTP_URL2.test(url) ? { remotes: [{ type: "streamable-http", url }] } : {},
15770
16544
  serverInfo: { name: input.name, version: input.version },
15771
16545
  title: truncate(input.name),
15772
16546
  tools: MCP_TOOLS.map((tool) => ({
@@ -15778,9 +16552,15 @@ var buildMcpServerCard = (input) => {
15778
16552
  transport: "streamable-http",
15779
16553
  transports: [{ endpoint: url, type: "streamable-http" }],
15780
16554
  url,
15781
- version: input.version,
15782
- ...input.site ? { websiteUrl: siteRoot(input.site) } : {}
16555
+ version: input.version
15783
16556
  };
16557
+ if (HTTP_URL2.test(url)) {
16558
+ card.remotes = [{ type: "streamable-http", url }];
16559
+ }
16560
+ if (input.site) {
16561
+ card.websiteUrl = siteRoot(input.site);
16562
+ }
16563
+ return card;
15784
16564
  };
15785
16565
 
15786
16566
  // src/core/builtin-tags.ts
@@ -15853,12 +16633,13 @@ import { dirname as dirname8, extname as extname7, isAbsolute as isAbsolute6, re
15853
16633
  import ts from "typescript";
15854
16634
  var GROUPS = ["mdx", "layout", "islands"];
15855
16635
  var GROUP_SET = new Set(GROUPS);
15856
- var FRAMEWORK_BY_EXT = {
15857
- jsx: "react",
15858
- svelte: "svelte",
15859
- tsx: "react",
15860
- vue: "vue"
15861
- };
16636
+ var isGroup = (name) => GROUP_SET.has(name);
16637
+ var FRAMEWORK_BY_EXT = new Map([
16638
+ ["jsx", "react"],
16639
+ ["svelte", "svelte"],
16640
+ ["tsx", "react"],
16641
+ ["vue", "vue"]
16642
+ ]);
15862
16643
  var FRAMEWORK_LABEL = {
15863
16644
  react: "React",
15864
16645
  svelte: "Svelte",
@@ -15881,6 +16662,7 @@ var HYDRATION_MODES = new Set([
15881
16662
  "only",
15882
16663
  "visible"
15883
16664
  ]);
16665
+ var isHydrationMode = (value) => HYDRATION_MODES.has(value);
15884
16666
  var emptyAnalysis = () => ({
15885
16667
  islands: [],
15886
16668
  layout: [],
@@ -15962,7 +16744,7 @@ var toImport = (specifier, imported, dir) => {
15962
16744
  }
15963
16745
  }
15964
16746
  return {
15965
- framework: FRAMEWORK_BY_EXT[extension] ?? null,
16747
+ framework: FRAMEWORK_BY_EXT.get(extension) ?? null,
15966
16748
  name: imported,
15967
16749
  path
15968
16750
  };
@@ -15991,7 +16773,7 @@ var applyDescriptorProperty = (descriptor, property, imports, dir) => {
15991
16773
  } else if (ts.isIdentifier(init)) {
15992
16774
  descriptor.source = resolveIdentifier(init.text, imports, dir);
15993
16775
  }
15994
- } else if (name === "client" && ts.isStringLiteral(init) && HYDRATION_MODES.has(init.text)) {
16776
+ } else if (name === "client" && ts.isStringLiteral(init) && isHydrationMode(init.text)) {
15995
16777
  descriptor.client = init.text;
15996
16778
  } else if (name === "media" && ts.isStringLiteral(init)) {
15997
16779
  descriptor.media = init.text;
@@ -16025,13 +16807,14 @@ var finalize = (key, group, descriptor, label, identifier, warnings) => {
16025
16807
  if (!client && source?.framework) {
16026
16808
  warnings.push(`Override "${key}" points to a ${FRAMEWORK_LABEL[source.framework]} component (${label}) but has no hydration mode, so it renders as static HTML with no interactivity. Add one, e.g. \`${key}: { component: ${JSON.stringify(label)}, client: "load" }\`.`);
16027
16809
  }
16028
- return {
16029
- identifier,
16030
- key,
16031
- ...client ? { client } : {},
16032
- ...media ? { media } : {},
16033
- source
16034
- };
16810
+ const normalized = { identifier, key, source };
16811
+ if (client) {
16812
+ normalized.client = client;
16813
+ }
16814
+ if (media) {
16815
+ normalized.media = media;
16816
+ }
16817
+ return normalized;
16035
16818
  };
16036
16819
  var normalizeEntry2 = (entry, group, imports, dir, warnings) => {
16037
16820
  const defaultClient = group === "islands" ? "visible" : undefined;
@@ -16084,14 +16867,13 @@ var collectGroupOverrides = (property, imports, dir, result) => {
16084
16867
  return;
16085
16868
  }
16086
16869
  const name = propName(property.name);
16087
- if (!(name && GROUP_SET.has(name)) || !ts.isObjectLiteralExpression(property.initializer)) {
16870
+ if (!(name && isGroup(name)) || !ts.isObjectLiteralExpression(property.initializer)) {
16088
16871
  return;
16089
16872
  }
16090
- const group = name;
16091
16873
  for (const entry of property.initializer.properties) {
16092
- const normalized = normalizeEntry2(entry, group, imports, dir, result.warnings);
16874
+ const normalized = normalizeEntry2(entry, name, imports, dir, result.warnings);
16093
16875
  if (normalized) {
16094
- result[group].push(normalized);
16876
+ result[name].push(normalized);
16095
16877
  }
16096
16878
  }
16097
16879
  };
@@ -16125,9 +16907,10 @@ import { parseTsconfig } from "get-tsconfig";
16125
16907
  import { dirname as dirname10, join as join22, resolve as resolve7 } from "pathe";
16126
16908
  var CONFIG_DIR_TEMPLATE = "${configDir}";
16127
16909
  var substituteConfigDir = (value, configDir) => value.startsWith(CONFIG_DIR_TEMPLATE) ? join22(configDir, value.slice(CONFIG_DIR_TEMPLATE.length)) : value;
16910
+ var isPathTarget = (target) => typeof target === "string";
16128
16911
  var toAlias = (key, value, baseDir, configDir) => {
16129
16912
  const first = Array.isArray(value) ? value[0] : value;
16130
- if (typeof first !== "string") {
16913
+ if (!isPathTarget(first)) {
16131
16914
  return null;
16132
16915
  }
16133
16916
  const find = key.endsWith("/*") ? key.slice(0, -2) : key;
@@ -16157,14 +16940,14 @@ var resolveTsconfigAliases = (root) => {
16157
16940
  }
16158
16941
  const configDir = dirname10(entry);
16159
16942
  const baseDir = resolve7(configDir, substituteConfigDir(options?.baseUrl ?? ".", configDir));
16160
- const aliases = {};
16943
+ const entries = [];
16161
16944
  for (const [key, value] of Object.entries(paths)) {
16162
16945
  const alias = toAlias(key, value, baseDir, configDir);
16163
16946
  if (alias) {
16164
- aliases[alias.find] = alias.replacement;
16947
+ entries.push([alias.find, alias.replacement]);
16165
16948
  }
16166
16949
  }
16167
- return aliases;
16950
+ return Object.fromEntries(entries);
16168
16951
  };
16169
16952
 
16170
16953
  // src/og/derive.ts
@@ -16172,8 +16955,12 @@ import { existsSync as existsSync14 } from "node:fs";
16172
16955
  import { isAbsolute as isAbsolute7, join as join23 } from "pathe";
16173
16956
  var CARD_WEIGHTS = [400, 600];
16174
16957
  var absoluteSrc = (root, src) => isAbsolute7(src) ? src : join23(root, src);
16958
+ var isNumericWeight = (weight) => typeof weight === "number";
16959
+ var isRangeWeight = (weight) => typeof weight === "string";
16960
+ var isFontName = (value) => typeof value === "string";
16961
+ var isLocalOgFont = (font) => typeof font !== "string" && ("src" in font);
16175
16962
  var googleWeights = (weights) => {
16176
- const numbers = weights.filter((weight) => typeof weight === "number");
16963
+ const numbers = weights.filter(isNumericWeight);
16177
16964
  const used = numbers.filter((weight) => CARD_WEIGHTS.includes(weight));
16178
16965
  if (used.length > 0) {
16179
16966
  return used;
@@ -16182,20 +16969,22 @@ var googleWeights = (weights) => {
16182
16969
  return numbers;
16183
16970
  }
16184
16971
  const [first] = weights;
16185
- return weights.length === 1 && typeof first === "string" ? first : undefined;
16972
+ return weights.length === 1 && isRangeWeight(first) ? first : undefined;
16186
16973
  };
16187
16974
  var googleOgFont = (name, weights) => {
16188
16975
  const weight = googleWeights(weights);
16189
16976
  return weight === undefined ? { name } : { name, weight };
16190
16977
  };
16191
- var localOgFonts = (font, root) => font.variants.map((variant) => ({
16192
- name: font.name,
16193
- src: absoluteSrc(root, variant.src),
16194
- ...typeof variant.weight === "number" ? { weight: variant.weight } : {},
16195
- ...variant.style === "normal" || variant.style === "italic" ? { style: variant.style } : {}
16196
- }));
16978
+ var localOgFonts = (font, root) => font.variants.map((variant) => {
16979
+ const entry = {
16980
+ name: font.name,
16981
+ src: absoluteSrc(root, variant.src)
16982
+ };
16983
+ const withWeight = isNumericWeight(variant.weight) ? { ...entry, weight: variant.weight } : entry;
16984
+ return variant.style === "normal" || variant.style === "italic" ? { ...withWeight, style: variant.style } : withWeight;
16985
+ });
16197
16986
  var roleFonts = (value, root) => {
16198
- if (typeof value === "string") {
16987
+ if (isFontName(value)) {
16199
16988
  if (!isFontSlug(value)) {
16200
16989
  return null;
16201
16990
  }
@@ -16205,14 +16994,13 @@ var roleFonts = (value, root) => {
16205
16994
  if ("variants" in value) {
16206
16995
  return localOgFonts(value, root);
16207
16996
  }
16208
- const remote = value;
16209
- if ((remote.provider ?? "google") !== "google") {
16997
+ if ((value.provider ?? "google") !== "google") {
16210
16998
  return null;
16211
16999
  }
16212
- return [googleOgFont(remote.name, remote.weights ?? CARD_WEIGHTS)];
17000
+ return [googleOgFont(value.name, value.weights ?? CARD_WEIGHTS)];
16213
17001
  };
16214
17002
  var roleFamily = (value) => {
16215
- if (typeof value === "string") {
17003
+ if (isFontName(value)) {
16216
17004
  return isFontSlug(value) ? GOOGLE_FONTS[value].family : null;
16217
17005
  }
16218
17006
  return value.name;
@@ -16242,12 +17030,13 @@ var deriveOgFonts = (fonts, root) => {
16242
17030
  }
16243
17031
  }
16244
17032
  }
16245
- return {
16246
- fonts: derived,
16247
- ...families.title || families.body ? { families } : {}
16248
- };
17033
+ const result = { fonts: derived };
17034
+ if (families.title || families.body) {
17035
+ result.families = families;
17036
+ }
17037
+ return result;
16249
17038
  };
16250
- var resolveOgFontSources = (fonts, root) => fonts.map((font) => typeof font !== "string" && ("src" in font) ? { ...font, src: absoluteSrc(root, font.src) } : font);
17039
+ var resolveOgFontSources = (fonts, root) => fonts.map((font) => isLocalOgFont(font) ? { ...font, src: absoluteSrc(root, font.src) } : font);
16251
17040
  var resolveOgFonts = (options, root) => {
16252
17041
  if (options.ogFonts) {
16253
17042
  return { fonts: resolveOgFontSources(options.ogFonts, root) };
@@ -16257,12 +17046,12 @@ var resolveOgFonts = (options, root) => {
16257
17046
  var missingFontFiles = (options, root) => {
16258
17047
  const sources = [];
16259
17048
  for (const value of Object.values(options.themeFonts ?? {})) {
16260
- if (typeof value !== "string" && "variants" in value) {
17049
+ if (!isFontName(value) && "variants" in value) {
16261
17050
  sources.push(...value.variants.map((variant) => absoluteSrc(root, variant.src)));
16262
17051
  }
16263
17052
  }
16264
17053
  for (const font of options.ogFonts) {
16265
- if (typeof font !== "string" && "src" in font) {
17054
+ if (isLocalOgFont(font)) {
16266
17055
  sources.push(absoluteSrc(root, font.src));
16267
17056
  }
16268
17057
  }
@@ -16299,9 +17088,10 @@ var ACCENTS = {
16299
17088
  red: "oklch(0.58 0.22 25)",
16300
17089
  teal: "oklch(0.6 0.12 195)"
16301
17090
  };
17091
+ var isAccentPreset = (value) => Object.hasOwn(ACCENTS, value);
16302
17092
  var CSS_COLOR = /^[\w\s#%.,()/+-]+$/u;
16303
17093
  var safeColor = (value, fallback) => CSS_COLOR.test(value.trim()) ? value.trim() : fallback;
16304
- var presetOrColor = (value) => Object.hasOwn(ACCENTS, value) ? ACCENTS[value] : safeColor(value, FALLBACK_ACCENT);
17094
+ var presetOrColor = (value) => isAccentPreset(value) ? ACCENTS[value] : safeColor(value, FALLBACK_ACCENT);
16305
17095
  var safeColorOrNull = (value) => value && CSS_COLOR.test(value.trim()) ? value.trim() : null;
16306
17096
  var RADII = {
16307
17097
  lg: "0.75rem",
@@ -16457,11 +17247,16 @@ var buildReferenceFiles = async (options) => {
16457
17247
  };
16458
17248
 
16459
17249
  // src/search/popular.ts
16460
- var resolveSearchPopular = (popular, basePath) => popular.map(({ href, icon, label }) => ({
16461
- ...icon ? { icon } : {},
16462
- label,
16463
- route: withBasePath(basePath, href)
16464
- }));
17250
+ var resolveSearchPopular = (popular, basePath) => popular.map(({ href, icon, label }) => {
17251
+ const page = {
17252
+ label,
17253
+ route: withBasePath(basePath, href)
17254
+ };
17255
+ if (icon) {
17256
+ page.icon = icon;
17257
+ }
17258
+ return page;
17259
+ });
16465
17260
 
16466
17261
  // src/theme/entry.ts
16467
17262
  var DARK_VARIANT = `/* Dark mode is driven by data-theme on the <html> element. */
@@ -16607,7 +17402,11 @@ ${THEME_MAPPING}
16607
17402
  scroll-padding-top: 4.5rem;
16608
17403
  text-rendering: optimizeLegibility;
16609
17404
  }
16610
- /* Headings use the display font (defaults to the body font when unset). */
17405
+ /* Headings use the display font (defaults to the body font when unset).
17406
+ The tightened tracking is part of the theme, not the font: display-tuned
17407
+ families bake it into their metrics, but a text family promoted to
17408
+ headings (including the Inter default) reads loose without it. -0.05em
17409
+ was matched visually against Inter Tight, the previous display default. */
16611
17410
  h1,
16612
17411
  h2,
16613
17412
  h3,
@@ -16615,6 +17414,7 @@ ${THEME_MAPPING}
16615
17414
  h5,
16616
17415
  h6 {
16617
17416
  font-family: var(--font-display);
17417
+ letter-spacing: -0.05em;
16618
17418
  }
16619
17419
  :focus-visible {
16620
17420
  outline: 2px solid var(--blume-accent);
@@ -16635,6 +17435,22 @@ ${THEME_MAPPING}
16635
17435
  }
16636
17436
  }
16637
17437
 
17438
+ /* Same-origin navigations are full document loads (no client router); opting
17439
+ into cross-document view transitions has the browser crossfade between the
17440
+ old and new page instead of hard-swapping, in browsers that support it.
17441
+ Pairs with the prefetch option in the generated Astro config. */
17442
+ @view-transition {
17443
+ navigation: auto;
17444
+ }
17445
+
17446
+ @media (prefers-reduced-motion: reduce) {
17447
+ ::view-transition-group(*),
17448
+ ::view-transition-old(*),
17449
+ ::view-transition-new(*) {
17450
+ animation: none !important;
17451
+ }
17452
+ }
17453
+
16638
17454
  /* Code reads left-to-right regardless of page direction; only the surrounding
16639
17455
  chrome mirrors for RTL. Inline code is isolated so LTR identifiers don't
16640
17456
  disturb the bidi flow of right-to-left prose. */
@@ -16670,9 +17486,10 @@ ${THEME_MAPPING}
16670
17486
  line-height: 1.7;
16671
17487
  }
16672
17488
 
17489
+ /* No letter-spacing here: prose headings inherit the base h1-h6 rule's
17490
+ display tracking, same as headings outside the prose column. */
16673
17491
  .prose :where(h1, h2, h3, h4) {
16674
17492
  font-weight: 500;
16675
- letter-spacing: 0;
16676
17493
  }
16677
17494
 
16678
17495
  /* A heading can carry one long unbreakable token — an OpenAPI operation's title
@@ -17423,12 +18240,13 @@ var VALID_MODES = new Set([
17423
18240
  "only",
17424
18241
  "visible"
17425
18242
  ]);
17426
- var FRAMEWORK_BY_EXT2 = {
17427
- jsx: "react",
17428
- svelte: "svelte",
17429
- tsx: "react",
17430
- vue: "vue"
17431
- };
18243
+ var isClientMode = (mode) => VALID_MODES.has(mode);
18244
+ var FRAMEWORK_BY_EXT2 = new Map([
18245
+ ["jsx", "react"],
18246
+ ["svelte", "svelte"],
18247
+ ["tsx", "react"],
18248
+ ["vue", "vue"]
18249
+ ]);
17432
18250
  var ISLAND_FILE = /\.(?<ext>jsx|svelte|tsx|vue)$/u;
17433
18251
  var CLIENT_EXPORT = /export\s+const\s+client\s*(?::[^=]+)?=\s*["'](?<mode>\w+)["']/u;
17434
18252
  var readClientMode = (source, file, warnings) => {
@@ -17436,7 +18254,7 @@ var readClientMode = (source, file, warnings) => {
17436
18254
  if (!mode) {
17437
18255
  return DEFAULT_CLIENT;
17438
18256
  }
17439
- if (!VALID_MODES.has(mode)) {
18257
+ if (!isClientMode(mode)) {
17440
18258
  warnings.push(`Island "${file}" declares an unknown client mode "${mode}"; defaulting to "${DEFAULT_CLIENT}". Use "load", "idle", "visible", or "only".`);
17441
18259
  return DEFAULT_CLIENT;
17442
18260
  }
@@ -17459,7 +18277,7 @@ var discoverIslands = async (root) => {
17459
18277
  const collectIsland = (file, source) => {
17460
18278
  const base = basename2(file);
17461
18279
  const ext = base.match(ISLAND_FILE)?.groups?.ext;
17462
- const framework = ext ? FRAMEWORK_BY_EXT2[ext] : undefined;
18280
+ const framework = ext ? FRAMEWORK_BY_EXT2.get(ext) : undefined;
17463
18281
  if (!framework) {
17464
18282
  return;
17465
18283
  }
@@ -17691,18 +18509,18 @@ var ensureDepsLink = async (outDir, pkgDir = packageRoot()) => {
17691
18509
  }
17692
18510
  return astroConflictWarning(blumeAstro, outDirHit?.pkg ?? null);
17693
18511
  };
17694
- var ISLAND_FRAMEWORK_DEPS = {
17695
- svelte: "@astrojs/svelte",
17696
- vue: "@astrojs/vue"
17697
- };
17698
- var DEPLOYMENT_ADAPTER_DEPS = {
17699
- cloudflare: "@astrojs/cloudflare",
17700
- netlify: "@astrojs/netlify"
17701
- };
18512
+ var ISLAND_FRAMEWORK_DEPS = new Map([
18513
+ ["svelte", "@astrojs/svelte"],
18514
+ ["vue", "@astrojs/vue"]
18515
+ ]);
18516
+ var DEPLOYMENT_ADAPTER_DEPS = new Map([
18517
+ ["cloudflare", "@astrojs/cloudflare"],
18518
+ ["netlify", "@astrojs/netlify"]
18519
+ ]);
17702
18520
  var islandFrameworkWarnings = (frameworks, root) => {
17703
18521
  const warnings = [];
17704
18522
  for (const framework of frameworks) {
17705
- const dep = ISLAND_FRAMEWORK_DEPS[framework];
18523
+ const dep = ISLAND_FRAMEWORK_DEPS.get(framework);
17706
18524
  if (dep && !canResolveFrom(root, dep)) {
17707
18525
  warnings.push(`Islands use ${framework}, which needs "${dep}". Install it (e.g. \`npm install ${dep} ${framework}\`).`);
17708
18526
  }
@@ -17710,7 +18528,7 @@ var islandFrameworkWarnings = (frameworks, root) => {
17710
18528
  return warnings;
17711
18529
  };
17712
18530
  var deploymentAdapterWarnings = (deployment, root) => {
17713
- const dep = deployment.output === "server" && deployment.adapter ? DEPLOYMENT_ADAPTER_DEPS[deployment.adapter] : undefined;
18531
+ const dep = deployment.output === "server" && deployment.adapter ? DEPLOYMENT_ADAPTER_DEPS.get(deployment.adapter) : undefined;
17714
18532
  if (dep && !(canResolveFrom(root, dep) || canResolveFrom(packageRoot(), dep))) {
17715
18533
  return [
17716
18534
  `Deployment adapter "${deployment.adapter}" needs "${dep}", which isn't installed. Run \`npm install ${dep}\` (or your package manager's equivalent).`
@@ -17862,14 +18680,15 @@ var readLogoSvg = (project, source) => {
17862
18680
  ].find((path) => existsSync16(path));
17863
18681
  return file ? readFileSync8(file, "utf-8") : undefined;
17864
18682
  };
18683
+ var isStringShorthand = (value) => typeof value === "string";
17865
18684
  var resolveLogo = (project) => {
17866
18685
  const { logo } = project.config;
17867
18686
  if (!logo) {
17868
18687
  return null;
17869
18688
  }
17870
- const config = typeof logo === "string" ? { image: logo } : logo;
18689
+ const config = isStringShorthand(logo) ? { image: logo } : logo;
17871
18690
  const { href, image: source, text } = config;
17872
- const image = typeof source === "string" ? { light: source } : source;
18691
+ const image = isStringShorthand(source) ? { light: source } : source;
17873
18692
  const light = image?.light ?? image?.dark;
17874
18693
  const dark = image?.dark ?? image?.light;
17875
18694
  const alt = image?.alt ?? "";
@@ -17892,16 +18711,16 @@ var FAVICON_CANDIDATES = [
17892
18711
  "favicon.ico",
17893
18712
  "icon.ico"
17894
18713
  ];
17895
- var FAVICON_TYPES = {
17896
- ico: "image/x-icon",
17897
- jpeg: "image/jpeg",
17898
- jpg: "image/jpeg",
17899
- png: "image/png",
17900
- svg: "image/svg+xml"
17901
- };
18714
+ var FAVICON_TYPES = new Map([
18715
+ ["ico", "image/x-icon"],
18716
+ ["jpeg", "image/jpeg"],
18717
+ ["jpg", "image/jpeg"],
18718
+ ["png", "image/png"],
18719
+ ["svg", "image/svg+xml"]
18720
+ ]);
17902
18721
  var faviconType = (name) => {
17903
18722
  const ext = name.split(".").pop()?.toLowerCase();
17904
- return ext ? FAVICON_TYPES[ext] : undefined;
18723
+ return ext ? FAVICON_TYPES.get(ext) : undefined;
17905
18724
  };
17906
18725
  var inlineDataUri = (file, type) => `data:${type};base64,${readFileSync8(file).toString("base64")}`;
17907
18726
  var defaultFavicon = () => ({
@@ -17937,7 +18756,7 @@ var resolveBanner = (config) => {
17937
18756
  if (!banner) {
17938
18757
  return null;
17939
18758
  }
17940
- if (typeof banner === "string") {
18759
+ if (isStringShorthand(banner)) {
17941
18760
  return { content: banner, dismissible: false, key: banner };
17942
18761
  }
17943
18762
  return {
@@ -18068,6 +18887,7 @@ var buildRuntimeData = (project) => {
18068
18887
  theme: config.theme,
18069
18888
  title: config.title,
18070
18889
  toc: config.toc,
18890
+ versions: config.versions ?? null,
18071
18891
  webmcp: {
18072
18892
  enabled: config.ai.webmcp,
18073
18893
  llms: config.ai.llmsTxt.enabled
@@ -18078,9 +18898,16 @@ var buildRuntimeData = (project) => {
18078
18898
  href: feed.path,
18079
18899
  title: feed.title
18080
18900
  })),
18081
- fontCssVars: configuredCssVars(config.theme.fonts),
18901
+ fontCssVars: configuredFonts(config.theme.fonts),
18082
18902
  navigation: withRepoUrl(graph.navigation),
18083
18903
  navigationByLocale,
18904
+ navigationByVersion: Object.fromEntries(Object.entries(graph.navigationByVersion).map(([id2, byLocale]) => [
18905
+ id2,
18906
+ Object.fromEntries(Object.entries(byLocale).map(([code, nav]) => [
18907
+ code,
18908
+ withRepoUrl(nav)
18909
+ ]))
18910
+ ])),
18084
18911
  routes: manifest.routes.map((route) => ({
18085
18912
  alternates: route.alternates,
18086
18913
  collection: route.collection,
@@ -18094,7 +18921,9 @@ var buildRuntimeData = (project) => {
18094
18921
  lastModified: route.lastModified ?? null,
18095
18922
  locale: route.locale,
18096
18923
  path: route.path,
18097
- title: route.title
18924
+ title: route.title,
18925
+ version: route.version,
18926
+ versionAlternates: route.versionAlternates
18098
18927
  })),
18099
18928
  ui: defaultUi,
18100
18929
  uiByLocale
@@ -18263,6 +19092,7 @@ var generateRuntime = async (project) => {
18263
19092
  const needsSvelte = frameworks.has("svelte");
18264
19093
  const reactCompilerPath = resolveReactCompiler(config, needsReact);
18265
19094
  const ogRoutes = customOgRoutes(pages, config.title, config.seo.og.titles);
19095
+ const changelogIndex = hasGeneratedChangelog(project, pages);
18266
19096
  const mcp = planMcp(project, srcDir, pages);
18267
19097
  pages.push(...mcp.discoveryPages);
18268
19098
  const hasStaged = staged.size > 0;
@@ -18332,9 +19162,9 @@ var generateRuntime = async (project) => {
18332
19162
  writeMcpFiles(project, mcp, write)
18333
19163
  ]);
18334
19164
  if (config.seo.og.enabled) {
18335
- await write(join28(srcDir, "pages", "og", "[...slug].png.ts"), ogEndpointTemplate(ogRoutes, projectOgFonts(project)));
19165
+ await write(join28(srcDir, "pages", "og", "[...slug].png.ts"), ogEndpointTemplate(ogRoutes, projectOgFonts(project), changelogIndex));
18336
19166
  }
18337
- if (hasGeneratedChangelog(project, pages)) {
19167
+ if (changelogIndex) {
18338
19168
  await write(join28(srcDir, "pages", "changelog.astro"), changelogIndexTemplate({
18339
19169
  exportEpub,
18340
19170
  exportPdf,
@@ -18396,7 +19226,7 @@ var generateRuntime = async (project) => {
18396
19226
  ...pages.map((page) => page.pattern),
18397
19227
  ...referenceRoutes(config)
18398
19228
  ]);
18399
- if (hasGeneratedChangelog(project, pages)) {
19229
+ if (changelogIndex) {
18400
19230
  navTargetRoutes.add("/changelog");
18401
19231
  }
18402
19232
  warnings.push(...[
@@ -18530,6 +19360,7 @@ var prepareProject = async (options) => {
18530
19360
 
18531
19361
  // src/cli/commands/build.ts
18532
19362
  var ADAPTERS = ["vercel", "node", "netlify", "cloudflare"];
19363
+ var isAdapter = (value) => ADAPTERS.some((adapter) => adapter === value);
18533
19364
  var BUDGET_JS = "budget-js";
18534
19365
  var BUDGET_CSS = "budget-css";
18535
19366
  var validateBudgetFlags = (args) => {
@@ -18626,10 +19457,13 @@ var emitVercelNegotiation = async (project, routePaths, root) => {
18626
19457
  if (!existsSync18(configPath)) {
18627
19458
  return;
18628
19459
  }
18629
- const overrides = {
18630
- ...hasApiCatalog(config2) ? { [API_CATALOG_PATH.slice(1)]: API_CATALOG_TYPE } : {},
18631
- ...config2.ai.webBotAuth.keys.length > 0 ? { [SIGNATURES_DIRECTORY_PATH.slice(1)]: SIGNATURES_DIRECTORY_TYPE } : {}
18632
- };
19460
+ const overrides = {};
19461
+ if (hasApiCatalog(config2)) {
19462
+ overrides[API_CATALOG_PATH.slice(1)] = API_CATALOG_TYPE;
19463
+ }
19464
+ if (config2.ai.webBotAuth.keys.length > 0) {
19465
+ overrides[SIGNATURES_DIRECTORY_PATH.slice(1)] = SIGNATURES_DIRECTORY_TYPE;
19466
+ }
18633
19467
  const rawMarkdown = await buildRawMarkdown(project);
18634
19468
  const home = rawMarkdown["/"];
18635
19469
  const injected = injectNegotiationRoutes(await readFile17(configPath, "utf-8"), routePaths, buildHomeLinkHeader(config2, routePaths), overrides, home ? markdownTokenCount(agentMarkdown(home)) : undefined);
@@ -18883,7 +19717,7 @@ var buildCommand = defineCommand3({
18883
19717
  logger.error(`Invalid --output "${args.output}" (use static | server).`);
18884
19718
  process.exit(1);
18885
19719
  }
18886
- if (args.adapter && !ADAPTERS.includes(args.adapter)) {
19720
+ if (args.adapter && !isAdapter(args.adapter)) {
18887
19721
  logger.error(`Invalid --adapter "${args.adapter}" (use ${ADAPTERS.join(" | ")}).`);
18888
19722
  process.exit(1);
18889
19723
  }
@@ -19295,7 +20129,7 @@ var askFiles = async (project, srcDir, genDir) => {
19295
20129
  const grounded = ask.provider !== "inkeep";
19296
20130
  const files = [
19297
20131
  {
19298
- content: askEndpointTemplate(resolveAskBackend(ask), grounded),
20132
+ content: askEndpointTemplate(resolveAskBackend(ask), grounded, ask.instructions),
19299
20133
  path: join33(srcDir, "pages", "api", "ask.ts")
19300
20134
  }
19301
20135
  ];
@@ -19794,14 +20628,15 @@ var commandsFor = (pm) => ({
19794
20628
  exec: { bun: "bunx", npm: "npx", pnpm: "pnpm exec", yarn: "yarn" }[pm],
19795
20629
  install: `${pm} install`
19796
20630
  });
20631
+ var isPackageManager = (value) => PACKAGE_MANAGERS.some((pm) => pm === value);
19797
20632
  var detectPackageManager = (userAgent) => {
19798
20633
  const name = userAgent?.split("/")[0];
19799
- return name !== undefined && PACKAGE_MANAGERS.includes(name) ? name : "npm";
20634
+ return name !== undefined && isPackageManager(name) ? name : "npm";
19800
20635
  };
19801
20636
  var detectProjectPackageManager = async (root) => {
19802
20637
  const detected = await detect({ cwd: root });
19803
20638
  const name = detected?.name;
19804
- return name !== undefined && PACKAGE_MANAGERS.includes(name) ? name : detectPackageManager(process.env.npm_config_user_agent);
20639
+ return name !== undefined && isPackageManager(name) ? name : detectPackageManager(process.env.npm_config_user_agent);
19805
20640
  };
19806
20641
  var validateContentDir = (root, dir) => isAbsolute9(dir) || relative16(root, join35(root, dir)).startsWith("..") ? "Must be a relative path inside the project." : undefined;
19807
20642
  var titleize = (raw) => {
@@ -19865,10 +20700,16 @@ export default defineConfig({
19865
20700
  description: "Documentation powered by Blume.",${STARTERS[answers.template].configExtra}${contentBlockFor(answers)}
19866
20701
  });
19867
20702
  `;
19868
- var extraDepsFor = (sources) => ({
19869
- ...sources.includes("notion") && { "@notionhq/client": "^2.2.15" },
19870
- ...sources.includes("sanity") && { "@sanity/client": "^7.25.0" }
19871
- });
20703
+ var extraDepsFor = (sources) => {
20704
+ const deps = {};
20705
+ if (sources.includes("notion")) {
20706
+ deps["@notionhq/client"] = "^2.2.15";
20707
+ }
20708
+ if (sources.includes("sanity")) {
20709
+ deps["@sanity/client"] = "^7.25.0";
20710
+ }
20711
+ return deps;
20712
+ };
19872
20713
  var buildPlan = (root, answers) => {
19873
20714
  const files = [
19874
20715
  {
@@ -20166,7 +21007,7 @@ import { join as join38 } from "pathe";
20166
21007
  import { readFile as readFile20, writeFile as writeFile12 } from "node:fs/promises";
20167
21008
  import spawn2 from "cross-spawn";
20168
21009
  import { join as join37 } from "pathe";
20169
- import { z as z3 } from "zod";
21010
+ import { z as z4 } from "zod";
20170
21011
  var KILL_GRACE_MS = 5000;
20171
21012
  var MCP_TOOL_NAMES = [
20172
21013
  "search_docs",
@@ -20292,10 +21133,10 @@ var codexArgs = (context) => {
20292
21133
  return [...base, "-"];
20293
21134
  };
20294
21135
  var agentArgs = (kind, context) => kind === "claude" ? claudeArgs(context) : codexArgs(context);
20295
- var claudeResultSchema = z3.object({
20296
- is_error: z3.boolean().default(false),
20297
- result: z3.string().default(""),
20298
- total_cost_usd: z3.number().optional()
21136
+ var claudeResultSchema = z4.object({
21137
+ is_error: z4.boolean().default(false),
21138
+ result: z4.string().default(""),
21139
+ total_cost_usd: z4.number().optional()
20299
21140
  });
20300
21141
  var tail = (value, max = 300) => {
20301
21142
  const trimmed = value.trim();
@@ -20349,11 +21190,11 @@ var readAgentOutput = async (kind, result, lastMessagePath) => {
20349
21190
  }
20350
21191
  return { isError: false, text };
20351
21192
  };
20352
- var verdictSchema = z3.object({
20353
- missing: z3.array(z3.string()).default([]),
20354
- notes: z3.string().default(""),
20355
- pass: z3.boolean(),
20356
- score: z3.number().min(0).max(1).optional()
21193
+ var verdictSchema = z4.object({
21194
+ missing: z4.array(z4.string()).default([]),
21195
+ notes: z4.string().default(""),
21196
+ pass: z4.boolean(),
21197
+ score: z4.number().min(0).max(1).optional()
20357
21198
  });
20358
21199
  var parseVerdict = (text) => {
20359
21200
  const start = text.indexOf("{");
@@ -20374,26 +21215,26 @@ var parseVerdict = (text) => {
20374
21215
  // src/eval/schema.ts
20375
21216
  import { readFile as readFile21 } from "node:fs/promises";
20376
21217
  import { load as load2 } from "js-yaml";
20377
- import { z as z4 } from "zod";
21218
+ import { z as z5 } from "zod";
20378
21219
  var ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/u;
20379
- var questionSchema = z4.strictObject({
20380
- expected: z4.array(z4.string().min(1)).min(1, "expected must list at least one fact"),
20381
- id: z4.string().regex(ID_PATTERN, "id must be a kebab-case slug (a-z, 0-9, dashes)"),
20382
- question: z4.string().min(1),
20383
- routes: z4.union([z4.string(), z4.array(z4.string())]).default([]).transform((value) => typeof value === "string" ? [value] : value),
20384
- severity: z4.enum(["error", "warning"]).default("error"),
20385
- skip: z4.boolean().default(false)
21220
+ var questionSchema = z5.strictObject({
21221
+ expected: z5.array(z5.string().min(1)).min(1, "expected must list at least one fact"),
21222
+ id: z5.string().regex(ID_PATTERN, "id must be a kebab-case slug (a-z, 0-9, dashes)"),
21223
+ question: z5.string().min(1),
21224
+ routes: z5.union([z5.string(), z5.array(z5.string())]).default([]).transform((value) => Array.isArray(value) ? value : [value]),
21225
+ severity: z5.enum(["error", "warning"]).default("error"),
21226
+ skip: z5.boolean().default(false)
20386
21227
  });
20387
- var fullSchema = z4.strictObject({
20388
- questions: z4.array(questionSchema).min(1),
20389
- version: z4.literal(1).default(1)
21228
+ var fullSchema = z5.strictObject({
21229
+ questions: z5.array(questionSchema).min(1),
21230
+ version: z5.literal(1).default(1)
20390
21231
  });
20391
21232
  var evalsFileSchema = fullSchema.superRefine((value, context) => {
20392
21233
  const seen = new Set;
20393
21234
  for (const question of value.questions) {
20394
21235
  if (seen.has(question.id)) {
20395
21236
  context.addIssue({
20396
- code: z4.ZodIssueCode.custom,
21237
+ code: z5.ZodIssueCode.custom,
20397
21238
  message: `duplicate question id "${question.id}"`,
20398
21239
  path: ["questions"]
20399
21240
  });
@@ -20986,13 +21827,13 @@ var initCommand = defineCommand9({
20986
21827
  },
20987
21828
  async run({ args }) {
20988
21829
  const cwd = process.cwd();
20989
- const template = args.template;
20990
- if (template !== undefined && !TEMPLATES.includes(template)) {
21830
+ const template = TEMPLATES.find((candidate) => candidate === args.template);
21831
+ if (args.template !== undefined && template === undefined) {
20991
21832
  logger.error(`Unknown template "${args.template}" (use ${TEMPLATES.join(" | ")}).`);
20992
21833
  process.exit(1);
20993
21834
  }
20994
- const pm = args["package-manager"];
20995
- if (pm !== undefined && !PACKAGE_MANAGERS.includes(pm)) {
21835
+ const pm = PACKAGE_MANAGERS.find((candidate) => candidate === args["package-manager"]);
21836
+ if (args["package-manager"] !== undefined && pm === undefined) {
20996
21837
  logger.error(`Unknown package manager "${args["package-manager"]}" (use ${PACKAGE_MANAGERS.join(" | ")}).`);
20997
21838
  process.exit(1);
20998
21839
  }
@@ -21065,7 +21906,7 @@ import {
21065
21906
  CallToolRequestSchema,
21066
21907
  ListToolsRequestSchema
21067
21908
  } from "@modelcontextprotocol/sdk/types.js";
21068
- import { z as z5 } from "zod";
21909
+ import { z as z6 } from "zod";
21069
21910
 
21070
21911
  // src/search/orama-index.ts
21071
21912
  import { create, insertMultiple, search } from "@orama/orama";
@@ -21076,7 +21917,8 @@ var SCHEMA = {
21076
21917
  facetTerms: "enum[]",
21077
21918
  locale: "enum",
21078
21919
  route: "string",
21079
- title: "string"
21920
+ title: "string",
21921
+ version: "enum"
21080
21922
  };
21081
21923
  var toFacetTerms = (facets) => Object.entries(facets).map(([key, value]) => `${key}:${value}`);
21082
21924
  var BOOST = { description: 2, title: 3 };
@@ -21098,12 +21940,13 @@ var addBigrams = (run, tokens) => {
21098
21940
  previous = character;
21099
21941
  }
21100
21942
  };
21943
+ var hasSegmenter = (segmenter) => typeof segmenter === "function";
21101
21944
  var segmentingTokenizer = (locale) => {
21102
21945
  const language = locale?.toLowerCase().split(/[-_]/u)[0] ?? "";
21103
21946
  if (!SEGMENTED_LANGUAGES.has(language)) {
21104
21947
  return;
21105
21948
  }
21106
- if (typeof Intl.Segmenter !== "function") {
21949
+ if (!hasSegmenter(Intl.Segmenter)) {
21107
21950
  return;
21108
21951
  }
21109
21952
  const segmenter = new Intl.Segmenter(language, { granularity: "word" });
@@ -21152,28 +21995,33 @@ var segmentingTokenizer = (locale) => {
21152
21995
  };
21153
21996
  var buildOramaIndex = async (documents, locale) => {
21154
21997
  const tokenizer = segmentingTokenizer(locale);
21155
- const db = create({
21156
- schema: SCHEMA,
21157
- ...tokenizer ? { components: { tokenizer } } : {}
21158
- });
21998
+ const db = tokenizer ? create({ components: { tokenizer }, schema: SCHEMA }) : create({ schema: SCHEMA });
21159
21999
  await insertMultiple(db, documents.map((doc) => doc.facets ? { ...doc, facetTerms: toFacetTerms(doc.facets) } : doc));
21160
22000
  return db;
21161
22001
  };
21162
22002
  var ALL_TOKENS = 0;
21163
22003
  var queryOramaIndex = async (db, term, limit, filters) => {
21164
22004
  const facetTerms = filters?.facets ? toFacetTerms(filters.facets) : [];
21165
- const where = {
21166
- ...filters?.locale ? { locale: { eq: filters.locale } } : {},
21167
- ...filters?.contentTypes && filters.contentTypes.length > 0 ? { contentType: { in: filters.contentTypes } } : {},
21168
- ...facetTerms.length > 0 ? { facetTerms: { containsAll: facetTerms } } : {}
21169
- };
21170
- const params = {
22005
+ const where = {};
22006
+ if (filters?.locale) {
22007
+ where.locale = { eq: filters.locale };
22008
+ }
22009
+ if (filters?.version !== undefined) {
22010
+ where.version = { eq: filters.version };
22011
+ }
22012
+ if (filters?.contentTypes && filters.contentTypes.length > 0) {
22013
+ where.contentType = { in: filters.contentTypes };
22014
+ }
22015
+ if (facetTerms.length > 0) {
22016
+ where.facetTerms = { containsAll: facetTerms };
22017
+ }
22018
+ const unfiltered = {
21171
22019
  boost: BOOST,
21172
22020
  limit,
21173
22021
  properties: ["title", "description", "content"],
21174
- term,
21175
- ...Object.keys(where).length > 0 ? { where } : {}
22022
+ term
21176
22023
  };
22024
+ const params = Object.keys(where).length > 0 ? { ...unfiltered, where } : unfiltered;
21177
22025
  const bigrammed = BIGRAM_LANGUAGES.has(db.tokenizer?.language ?? "");
21178
22026
  const strict = bigrammed ? await search(db, { ...params, threshold: ALL_TOKENS }) : undefined;
21179
22027
  const found = strict && strict.hits.length > 0 ? strict : await search(db, params);
@@ -21184,36 +22032,55 @@ var queryOramaIndex = async (db, term, limit, filters) => {
21184
22032
  var DEFAULT_SEARCH_LIMIT = 8;
21185
22033
  var MAX_SEARCH_LIMIT = 20;
21186
22034
  var EXCERPT_LENGTH = 200;
21187
- var contentTypesField = z5.preprocess((value) => {
22035
+ var contentTypesField = z6.preprocess((value) => {
21188
22036
  const list2 = (Array.isArray(value) ? value : [value]).filter((entry) => typeof entry === "string");
21189
22037
  return list2.length > 0 ? list2 : undefined;
21190
- }, z5.array(z5.string()).optional().describe('Only include pages of these content types (frontmatter `type`, e.g. `["doc", "rfc"]`). `list_pages` shows each page\'s type. Omit to include every type.'));
21191
- var filtersField = z5.preprocess((value) => {
21192
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
22038
+ }, z6.array(z6.string()).optional().describe('Only include pages of these content types (frontmatter `type`, e.g. `["doc", "rfc"]`). `list_pages` shows each page\'s type. Omit to include every type.'));
22039
+ var looseFacetObject = z6.record(z6.string(), z6.unknown());
22040
+ var filtersField = z6.preprocess((value) => {
22041
+ const candidate = looseFacetObject.safeParse(value);
22042
+ if (!candidate.success) {
21193
22043
  return;
21194
22044
  }
21195
- const entries = Object.entries(value).filter((entry) => typeof entry[1] === "string");
22045
+ const entries = Object.entries(candidate.data).filter((entry) => typeof entry[1] === "string");
21196
22046
  return entries.length > 0 ? Object.fromEntries(entries) : undefined;
21197
- }, z5.record(z5.string(), z5.string()).optional().describe('Only include pages matching every facet, key → required value (e.g. `{"status": "enforced"}`). Facets are metadata the site declares per content type; `list_pages` shows each page\'s facet values. Omit for no facet filtering.'));
21198
- var limitField = z5.preprocess((value) => {
21199
- const num = typeof value === "number" ? value : Number(value);
22047
+ }, z6.record(z6.string(), z6.string()).optional().describe('Only include pages matching every facet, key → required value (e.g. `{"status": "enforced"}`). Facets are metadata the site declares per content type; `list_pages` shows each page\'s facet values. Omit for no facet filtering.'));
22048
+ var limitField = z6.preprocess((value) => {
22049
+ const num = Number(value);
21200
22050
  return Number.isFinite(num) ? Math.min(Math.max(Math.trunc(num), 1), MAX_SEARCH_LIMIT) : undefined;
21201
- }, z5.int().min(1).max(MAX_SEARCH_LIMIT).optional().describe(`Maximum hits to return (default ${DEFAULT_SEARCH_LIMIT}).`));
21202
- var textField = (description) => z5.preprocess((value) => typeof value === "string" ? value : "", z5.string().describe(description));
22051
+ }, z6.int().min(1).max(MAX_SEARCH_LIMIT).optional().describe(`Maximum hits to return (default ${DEFAULT_SEARCH_LIMIT}).`));
22052
+ var textField = (description) => z6.preprocess((value) => {
22053
+ const parsed = z6.string().safeParse(value);
22054
+ return parsed.success ? parsed.data : "";
22055
+ }, z6.string().describe(description));
22056
+ var optionalTextField = (description) => z6.preprocess((value) => {
22057
+ const parsed = z6.string().safeParse(value);
22058
+ const trimmed = parsed.success ? parsed.data.trim() : "";
22059
+ return trimmed || undefined;
22060
+ }, z6.string().optional().describe(description));
22061
+ var localeField = optionalTextField("Only include pages in this locale (e.g. `fr`). Omit for every language.");
22062
+ var versionField = optionalTextField('Docs version to scope to on a versioned site: `"latest"` (the default — current docs only), `"all"` (every version), or an archived version id (e.g. `"v1.0"`). Ignored when the site is unversioned.');
21203
22063
  var TOOL_INPUTS = {
21204
- get_navigation: z5.object({}),
21205
- get_page: z5.object({
22064
+ get_navigation: z6.object({
22065
+ locale: optionalTextField("Locale whose navigation tree to return (defaults to the default locale)."),
22066
+ version: optionalTextField("Archived version id whose tree to return (defaults to the current docs).")
22067
+ }),
22068
+ get_page: z6.object({
21206
22069
  route: textField("The page route, e.g. `/guides/install`.")
21207
22070
  }),
21208
- list_pages: z5.object({
22071
+ list_pages: z6.object({
21209
22072
  contentTypes: contentTypesField,
21210
- filters: filtersField
22073
+ filters: filtersField,
22074
+ locale: localeField,
22075
+ version: versionField
21211
22076
  }),
21212
- search_docs: z5.object({
22077
+ search_docs: z6.object({
21213
22078
  contentTypes: contentTypesField,
21214
22079
  filters: filtersField,
21215
22080
  limit: limitField,
21216
- query: textField("The search query.")
22081
+ locale: localeField,
22082
+ query: textField("The search query."),
22083
+ version: versionField
21217
22084
  })
21218
22085
  };
21219
22086
  var inputSchemaFor = (schema) => {
@@ -21221,7 +22088,7 @@ var inputSchemaFor = (schema) => {
21221
22088
  $schema: _dialect,
21222
22089
  additionalProperties: _closed,
21223
22090
  ...rest
21224
- } = z5.toJSONSchema(schema);
22091
+ } = z6.toJSONSchema(schema);
21225
22092
  return rest;
21226
22093
  };
21227
22094
  var TOOL_DEFINITIONS = MCP_TOOLS.map((tool) => ({
@@ -21232,6 +22099,19 @@ var TOOL_DEFINITIONS = MCP_TOOLS.map((tool) => ({
21232
22099
  title: tool.title
21233
22100
  }));
21234
22101
  var matchesFacets = (facets, filters) => Object.entries(filters).every(([key, value]) => facets?.[key] === value);
22102
+ var asVersionScope = (value, data) => {
22103
+ if (!data.archivedVersions) {
22104
+ return;
22105
+ }
22106
+ if (value === "all") {
22107
+ return;
22108
+ }
22109
+ if (value === undefined || value === "latest" || value === "current") {
22110
+ return "";
22111
+ }
22112
+ return value;
22113
+ };
22114
+ var unknownVersionError = (versionId, data) => versionId && data.archivedVersions && !data.archivedVersions.includes(versionId) ? `Unknown version "${versionId}". Archived versions: ${data.archivedVersions.join(", ")}.` : null;
21235
22115
  var normalizeRoute2 = (input, data) => {
21236
22116
  let value = input.trim();
21237
22117
  if (/^https?:\/\//iu.test(value)) {
@@ -21256,10 +22136,10 @@ var excerptFor = (doc) => {
21256
22136
  const head = doc.content.slice(0, EXCERPT_LENGTH).trim();
21257
22137
  return doc.content.length > EXCERPT_LENGTH ? `${head}…` : head;
21258
22138
  };
21259
- var text = (value, isError = false) => ({
21260
- content: [{ text: value, type: "text" }],
21261
- ...isError ? { isError: true } : {}
21262
- });
22139
+ var text = (value, isError = false) => {
22140
+ const content = [{ text: value, type: "text" }];
22141
+ return isError ? { content, isError: true } : { content };
22142
+ };
21263
22143
  var createIndexProvider = (documents, locale) => {
21264
22144
  let dbPromise = null;
21265
22145
  return function provideIndex() {
@@ -21268,10 +22148,8 @@ var createIndexProvider = (documents, locale) => {
21268
22148
  };
21269
22149
  };
21270
22150
  var buildServer = (data, index) => {
21271
- const server = new Server({ name: data.name, version: data.version }, {
21272
- capabilities: { tools: {} },
21273
- ...data.instructions ? { instructions: data.instructions } : {}
21274
- });
22151
+ const serverOptions = data.instructions ? { capabilities: { tools: {} }, instructions: data.instructions } : { capabilities: { tools: {} } };
22152
+ const server = new Server({ name: data.name, version: data.version }, serverOptions);
21275
22153
  server.setRequestHandler(ListToolsRequestSchema, () => ({
21276
22154
  tools: TOOL_DEFINITIONS
21277
22155
  }));
@@ -21282,16 +22160,24 @@ var buildServer = (data, index) => {
21282
22160
  const db = await index();
21283
22161
  const hits = await queryOramaIndex(db, input.query, input.limit ?? DEFAULT_SEARCH_LIMIT, {
21284
22162
  contentTypes: input.contentTypes,
21285
- facets: input.filters
22163
+ facets: input.filters,
22164
+ locale: input.locale,
22165
+ version: asVersionScope(input.version, data)
22166
+ });
22167
+ const results = hits.map((doc) => {
22168
+ const hit = {
22169
+ contentType: doc.contentType,
22170
+ excerpt: excerptFor(doc),
22171
+ facets: doc.facets,
22172
+ route: doc.route,
22173
+ title: doc.title,
22174
+ url: urlFor(doc.route, data)
22175
+ };
22176
+ if (data.archivedVersions) {
22177
+ hit.version = doc.version ?? "";
22178
+ }
22179
+ return hit;
21286
22180
  });
21287
- const results = hits.map((doc) => ({
21288
- contentType: doc.contentType,
21289
- excerpt: excerptFor(doc),
21290
- facets: doc.facets,
21291
- route: doc.route,
21292
- title: doc.title,
21293
- url: urlFor(doc.route, data)
21294
- }));
21295
22181
  return text(JSON.stringify(results, null, 2));
21296
22182
  }
21297
22183
  if (name === "get_page") {
@@ -21304,20 +22190,40 @@ var buildServer = (data, index) => {
21304
22190
  return text(markdown);
21305
22191
  }
21306
22192
  if (name === "list_pages") {
21307
- const { contentTypes, filters } = TOOL_INPUTS.list_pages.parse(args);
21308
- const routes = data.routes.filter((route) => (!contentTypes || contentTypes.includes(route.contentType)) && (!filters || matchesFacets(route.facets, filters)));
21309
- return text(JSON.stringify(routes.map((route) => ({
21310
- contentType: route.contentType,
21311
- description: route.description,
21312
- facets: route.facets,
21313
- lastModified: route.lastModified,
21314
- route: route.route,
21315
- title: route.title,
21316
- url: urlFor(route.route, data)
21317
- })), null, 2));
22193
+ const input = TOOL_INPUTS.list_pages.parse(args);
22194
+ const { contentTypes, filters, locale } = input;
22195
+ const versionScope = asVersionScope(input.version, data);
22196
+ const routes = data.routes.filter((route) => (!contentTypes || contentTypes.includes(route.contentType)) && (!filters || matchesFacets(route.facets, filters)) && (!locale || route.locale === locale) && (versionScope === undefined || route.version === versionScope));
22197
+ return text(JSON.stringify(routes.map((route) => {
22198
+ const listing = {
22199
+ contentType: route.contentType,
22200
+ description: route.description,
22201
+ facets: route.facets,
22202
+ lastModified: route.lastModified,
22203
+ route: route.route,
22204
+ title: route.title,
22205
+ url: urlFor(route.route, data)
22206
+ };
22207
+ if (data.archivedVersions) {
22208
+ listing.version = route.version;
22209
+ }
22210
+ return listing;
22211
+ }), null, 2));
21318
22212
  }
21319
22213
  if (name === "get_navigation") {
21320
- return text(JSON.stringify(data.navigation, null, 2));
22214
+ const { locale, version: versionId } = TOOL_INPUTS.get_navigation.parse(args);
22215
+ const unknownVersion = unknownVersionError(versionId, data);
22216
+ if (unknownVersion) {
22217
+ return text(unknownVersion, true);
22218
+ }
22219
+ let { navigation } = data;
22220
+ const byLocale = versionId ? data.navigationByVersion?.[versionId] : undefined;
22221
+ if (byLocale) {
22222
+ navigation = (locale ? byLocale[locale] : undefined) ?? byLocale[data.defaultLocale ?? ""] ?? Object.values(byLocale)[0] ?? navigation;
22223
+ } else if (locale && data.navigationByLocale?.[locale]) {
22224
+ navigation = data.navigationByLocale[locale];
22225
+ }
22226
+ return text(JSON.stringify(navigation, null, 2));
21321
22227
  }
21322
22228
  return text(`Unknown tool: ${name}`, true);
21323
22229
  });
@@ -21469,11 +22375,11 @@ var translateAgentArgs = (kind, lastMessagePath) => kind === "claude" ? claudeAr
21469
22375
  import { createHash as createHash5 } from "node:crypto";
21470
22376
  import { readFile as readFile23 } from "node:fs/promises";
21471
22377
  import { join as join42 } from "pathe";
21472
- import { z as z6 } from "zod";
22378
+ import { z as z7 } from "zod";
21473
22379
  var LEDGER_FILE = "blume.translations.json";
21474
- var ledgerSchema = z6.object({
21475
- files: z6.record(z6.string(), z6.record(z6.string(), z6.string())),
21476
- version: z6.literal(1)
22380
+ var ledgerSchema = z7.object({
22381
+ files: z7.record(z7.string(), z7.record(z7.string(), z7.string())),
22382
+ version: z7.literal(1)
21477
22383
  });
21478
22384
  var emptyLedger = () => ({
21479
22385
  files: {},
@@ -21743,6 +22649,7 @@ import { readFile as readFile24 } from "node:fs/promises";
21743
22649
  import { dirname as dirname15, join as join43, relative as relative19 } from "pathe";
21744
22650
  import { glob as glob8 } from "tinyglobby";
21745
22651
  var META_FILES2 = ["**/meta.ts", "**/meta.js", "**/meta.mjs"];
22652
+ var isFactoryModule = (value) => typeof value === "function";
21746
22653
  var metaTargetPath = (meta, locale) => join43(meta.contentRoot, locale, meta.dir, "meta.ts");
21747
22654
  var discoverTranslatableMeta = async (project) => {
21748
22655
  const { i18n } = project.config;
@@ -21771,7 +22678,7 @@ var discoverTranslatableMeta = async (project) => {
21771
22678
  readFile24(file, "utf-8"),
21772
22679
  load3(file)
21773
22680
  ]);
21774
- if (typeof mod === "function") {
22681
+ if (isFactoryModule(mod)) {
21775
22682
  diagnostics.push({
21776
22683
  code: "BLUME_TRANSLATE_META_FACTORY",
21777
22684
  file,
@@ -21803,7 +22710,7 @@ var generateMetaModule = (meta, translatedTitle) => {
21803
22710
  ...meta,
21804
22711
  title: translatedTitle
21805
22712
  };
21806
- const lines = Object.keys(data).toSorted().filter((key) => data[key] !== undefined).map((key) => ` ${key}: ${JSON.stringify(data[key])},`);
22713
+ const lines = Object.entries(data).toSorted(([a], [b]) => a < b ? -1 : 1).filter(([, value]) => value !== undefined).map(([key, value]) => ` ${key}: ${JSON.stringify(value)},`);
21807
22714
  return [
21808
22715
  "// Generated by `blume translate` — edit the default locale's meta file",
21809
22716
  "// and rerun the translation instead of editing this copy.",
@@ -21863,10 +22770,12 @@ var stripOuterFence = (text2) => {
21863
22770
  };
21864
22771
  var countFenceLines = (text2) => text2.split(`
21865
22772
  `).filter((line) => FENCE_LINE.test(line)).length;
22773
+ var isKeyedObject = (value) => typeof value === "object" && value !== null;
22774
+ var isString7 = (value) => typeof value === "string";
21866
22775
  var getPath2 = (data, path) => {
21867
22776
  let value = data;
21868
22777
  for (const key of path) {
21869
- if (typeof value !== "object" || value === null) {
22778
+ if (!isKeyedObject(value)) {
21870
22779
  return;
21871
22780
  }
21872
22781
  value = value[key];
@@ -21921,7 +22830,7 @@ var validateTranslation = (sourceText, agentText) => {
21921
22830
  for (const path of TRANSLATABLE_KEY_PATHS) {
21922
22831
  const original = getPath2(source.data, path);
21923
22832
  const translated = getPath2(parsed.data, path);
21924
- if (typeof original === "string" && typeof translated === "string" && translated.trim() !== "") {
22833
+ if (isString7(original) && isString7(translated) && translated.trim() !== "") {
21925
22834
  setPath(data, path, translated);
21926
22835
  }
21927
22836
  }
@@ -21941,12 +22850,12 @@ var parseMetaTitles = (agentText, expectedKeys) => {
21941
22850
  parsed = undefined;
21942
22851
  }
21943
22852
  }
21944
- const record = typeof parsed === "object" && parsed !== null ? parsed : {};
22853
+ const record = isKeyedObject(parsed) ? parsed : {};
21945
22854
  const titles = {};
21946
22855
  const missing = [];
21947
22856
  for (const key of expectedKeys) {
21948
22857
  const value = record[key];
21949
- if (typeof value === "string" && value.trim() !== "") {
22858
+ if (isString7(value) && value.trim() !== "") {
21950
22859
  titles[key] = value;
21951
22860
  } else {
21952
22861
  missing.push(key);
@@ -22103,7 +23012,7 @@ var translatablePages = (project, i18n) => {
22103
23012
  const seen = new Set;
22104
23013
  const universe = [];
22105
23014
  for (const page2 of project.graph.pages) {
22106
- if (page2.locale !== i18n.defaultLocale || page2.fallback || !page2.sourcePath || seen.has(page2.sourcePath)) {
23015
+ if (page2.locale !== i18n.defaultLocale || page2.fallback || !page2.sourcePath || seen.has(page2.sourcePath) || page2.version !== "") {
22107
23016
  continue;
22108
23017
  }
22109
23018
  const ext = extname8(page2.sourcePath);
@@ -22673,8 +23582,249 @@ var validateCommand = defineCommand14({
22673
23582
  }
22674
23583
  });
22675
23584
 
23585
+ // src/cli/commands/version.ts
23586
+ import { defineCommand as defineCommand15 } from "citty";
23587
+
23588
+ // src/core/version-cut.ts
23589
+ import { existsSync as existsSync28 } from "node:fs";
23590
+ import { cp as cp3, readdir as readdir3, readFile as readFile27, rm as rm5 } from "node:fs/promises";
23591
+ import { join as join48, relative as relative21 } from "pathe";
23592
+ class CutError extends Error {
23593
+ constructor(message) {
23594
+ super(message);
23595
+ this.name = "CutError";
23596
+ }
23597
+ }
23598
+ var INLINE_CODE2 = /`[^`]*`/gu;
23599
+ var ROOT_LINK = /(?<prefix>\]\(|href="|src=")(?<target>\/[^\s"')]*)/gu;
23600
+ var buildRouteRewrites = (project, id2) => {
23601
+ const { basePath, i18n } = project.config;
23602
+ const { contentRoot: contentRoot2 } = project.context;
23603
+ const rewrites = new Map;
23604
+ for (const page2 of project.graph.pages) {
23605
+ const { sourcePath } = page2;
23606
+ if (page2.version !== "" || !sourcePath || relative21(contentRoot2, sourcePath).startsWith("..")) {
23607
+ continue;
23608
+ }
23609
+ const logical = versionizeRoute(page2.versionKey, id2);
23610
+ rewrites.set(stripBasePath(basePath, page2.route), i18n ? localizeRoute(logical, page2.locale, i18n) : logical);
23611
+ }
23612
+ return rewrites;
23613
+ };
23614
+ var rewriteLine2 = (line, rewrites) => {
23615
+ const masked = line.replaceAll(INLINE_CODE2, (span) => " ".repeat(span.length));
23616
+ let out = "";
23617
+ let cursor = 0;
23618
+ let count = 0;
23619
+ for (const match of masked.matchAll(ROOT_LINK)) {
23620
+ const target = match.groups?.target ?? "";
23621
+ const hash = target.search(/[#?]/u);
23622
+ const path = hash === -1 ? target : target.slice(0, hash);
23623
+ const suffix = hash === -1 ? "" : target.slice(hash);
23624
+ const bare = path !== "/" && path.endsWith("/") ? path.slice(0, -1) : path;
23625
+ const replacement = rewrites.get(bare);
23626
+ if (replacement === undefined) {
23627
+ continue;
23628
+ }
23629
+ const offset = (match.index ?? 0) + (match.groups?.prefix?.length ?? 0);
23630
+ out += line.slice(cursor, offset) + replacement + suffix;
23631
+ cursor = offset + target.length;
23632
+ count += 1;
23633
+ }
23634
+ return count === 0 ? line : out + line.slice(cursor);
23635
+ };
23636
+ var rewriteSnapshotLinks = (source, rewrites) => {
23637
+ let fence = null;
23638
+ let count = 0;
23639
+ const lines = source.split(`
23640
+ `).map((line) => {
23641
+ const next = nextFenceState(line, fence);
23642
+ const inFence = fence !== null || next !== null;
23643
+ fence = next;
23644
+ if (inFence) {
23645
+ return line;
23646
+ }
23647
+ const rewrittenLine = rewriteLine2(line, rewrites);
23648
+ if (rewrittenLine !== line) {
23649
+ count += 1;
23650
+ }
23651
+ return rewrittenLine;
23652
+ });
23653
+ return { count, text: lines.join(`
23654
+ `) };
23655
+ };
23656
+ var snippetFor = (id2, hasVersions) => hasVersions ? `Add to versions.archived in blume.config.ts (newest first):
23657
+
23658
+ { id: "${id2}" },
23659
+ ` : `Add to blume.config.ts:
23660
+
23661
+ versions: {
23662
+ archived: [{ id: "${id2}" }],
23663
+ current: { label: "…" },
23664
+ },
23665
+ `;
23666
+ var insertArchivedVersion = async (configPath, id2) => {
23667
+ let text2;
23668
+ try {
23669
+ text2 = await readFile27(configPath, "utf-8");
23670
+ } catch {
23671
+ return false;
23672
+ }
23673
+ const match = text2.match(/(?<lead>archived:\s*\[)/u);
23674
+ if (!match || match.index === undefined) {
23675
+ return false;
23676
+ }
23677
+ const insertAt = match.index + (match.groups?.lead?.length ?? 0);
23678
+ const lineStart = text2.lastIndexOf(`
23679
+ `, match.index) + 1;
23680
+ const indent = text2.slice(lineStart).match(/^\s*/u)?.[0] ?? "";
23681
+ const rest = text2.slice(insertAt);
23682
+ let entry;
23683
+ if (rest.trimStart().startsWith("]")) {
23684
+ entry = `{ id: "${id2}" }`;
23685
+ } else if (rest.startsWith(`
23686
+ `)) {
23687
+ entry = `
23688
+ ${indent} { id: "${id2}" },`;
23689
+ } else {
23690
+ entry = `{ id: "${id2}" }, `;
23691
+ }
23692
+ await writeTextAtomic(configPath, text2.slice(0, insertAt) + entry + rest);
23693
+ return true;
23694
+ };
23695
+ var cutVersion = async (root, id2, options = {}) => {
23696
+ if (!VERSION_ID.test(id2)) {
23697
+ throw new CutError(`Version ids must start with a letter (e.g. "v1.0") and contain only letters, digits, dots, hyphens, and underscores — got "${id2}".`);
23698
+ }
23699
+ const project = await scanProject(root);
23700
+ const errors = project.diagnostics.filter((diagnostic) => diagnostic.severity === "error");
23701
+ if (errors.length > 0) {
23702
+ throw new CutError(`The project has ${errors.length} error diagnostic(s) — fix them before cutting a version (run \`blume validate\`).`);
23703
+ }
23704
+ const { contentRoot: contentRoot2 } = project.context;
23705
+ if (project.config.versions?.archived.some((version) => version.id === id2)) {
23706
+ throw new CutError(`Version "${id2}" is already registered in versions.archived.`);
23707
+ }
23708
+ const dir = join48(contentRoot2, id2);
23709
+ if (existsSync28(dir)) {
23710
+ if (!options.force) {
23711
+ throw new CutError(`${dir} already exists — pass --force to overwrite the snapshot.`);
23712
+ }
23713
+ await rm5(dir, { force: true, recursive: true });
23714
+ }
23715
+ const excluded = new Set([
23716
+ id2,
23717
+ "node_modules",
23718
+ ...project.config.versions?.archived.map((version) => version.id) ?? []
23719
+ ]);
23720
+ const entries = await readdir3(contentRoot2, { withFileTypes: true });
23721
+ let copied = 0;
23722
+ await Promise.all(entries.map(async (entry) => {
23723
+ if (excluded.has(entry.name) || entry.name.startsWith(".") || entry.isDirectory() && VERSION_LIKE.test(entry.name)) {
23724
+ return;
23725
+ }
23726
+ await cp3(join48(contentRoot2, entry.name), join48(dir, entry.name), {
23727
+ recursive: true
23728
+ });
23729
+ }));
23730
+ const rewrites = buildRouteRewrites(project, id2);
23731
+ const rewritten = [];
23732
+ const walk = async (current) => {
23733
+ const children = await readdir3(current, { withFileTypes: true });
23734
+ await Promise.all(children.map(async (child) => {
23735
+ const abs = join48(current, child.name);
23736
+ if (child.isDirectory()) {
23737
+ await walk(abs);
23738
+ return;
23739
+ }
23740
+ copied += 1;
23741
+ if (!/\.mdx?$/u.test(child.name)) {
23742
+ return;
23743
+ }
23744
+ const source = await readFile27(abs, "utf-8");
23745
+ const { text: text2, count } = rewriteSnapshotLinks(source, rewrites);
23746
+ if (count > 0) {
23747
+ await writeTextAtomic(abs, text2);
23748
+ rewritten.push({ count, file: relative21(dir, abs) });
23749
+ }
23750
+ }));
23751
+ };
23752
+ await walk(dir);
23753
+ const configPath = join48(root, "blume.config.ts");
23754
+ const configUpdated = project.config.versions !== undefined && await insertArchivedVersion(configPath, id2);
23755
+ return {
23756
+ configSnippet: configUpdated ? null : snippetFor(id2, project.config.versions !== undefined),
23757
+ configUpdated,
23758
+ copied,
23759
+ dir,
23760
+ rewritten
23761
+ };
23762
+ };
23763
+
23764
+ // src/cli/commands/version.ts
23765
+ var versionCommand = defineCommand15({
23766
+ args: {
23767
+ force: {
23768
+ description: "Overwrite an existing snapshot directory.",
23769
+ type: "boolean"
23770
+ },
23771
+ id: {
23772
+ description: 'Version id to cut (e.g. "v1.0").',
23773
+ required: false,
23774
+ type: "positional"
23775
+ }
23776
+ },
23777
+ meta: {
23778
+ description: "Freeze the current docs as an archived version.",
23779
+ name: "version"
23780
+ },
23781
+ async run({ args }) {
23782
+ const root = process.cwd();
23783
+ if (!args.id) {
23784
+ const { config: config2 } = await loadConfig(root);
23785
+ if (!config2.versions) {
23786
+ logger.info("Versioning is not configured. Cut the first version with `blume version <id>` (e.g. `blume version v1.0`).");
23787
+ return;
23788
+ }
23789
+ const { current, archived } = config2.versions;
23790
+ process.stdout.write(` ${current.label} (current)${current.badge ? ` — ${current.badge}` : ""}
23791
+ `);
23792
+ for (const version of archived) {
23793
+ process.stdout.write(` ${version.label ?? version.id} — ${version.id}/
23794
+ `);
23795
+ }
23796
+ return;
23797
+ }
23798
+ try {
23799
+ const result = await cutVersion(root, args.id, { force: args.force });
23800
+ logger.success(`Snapshot ${result.dir} (${result.copied} file(s) copied)`);
23801
+ const totalRewrites = result.rewritten.reduce((sum, entry) => sum + entry.count, 0);
23802
+ if (totalRewrites > 0) {
23803
+ logger.info(`Rewrote root-absolute links in ${result.rewritten.length} page(s) (${totalRewrites} line(s)).`);
23804
+ }
23805
+ if (result.configUpdated) {
23806
+ logger.success(`Added "${args.id}" to versions.archived in blume.config.ts`);
23807
+ } else if (result.configSnippet) {
23808
+ logger.info(result.configSnippet);
23809
+ }
23810
+ logger.info("Archived versions are frozen — future edits belong in the live tree. Restart `blume dev` to pick up the snapshot.");
23811
+ } catch (error) {
23812
+ if (error instanceof CutError) {
23813
+ logger.error(error.message);
23814
+ process.exit(1);
23815
+ }
23816
+ if (error instanceof BlumeError) {
23817
+ logger.error(error.diagnostic.message);
23818
+ process.exit(1);
23819
+ }
23820
+ reportInternalError(error);
23821
+ process.exit(1);
23822
+ }
23823
+ }
23824
+ });
23825
+
22676
23826
  // src/cli/index.ts
22677
- var main = defineCommand15({
23827
+ var main = defineCommand16({
22678
23828
  meta: {
22679
23829
  description: "Markdown-first documentation powered by Astro and Vite.",
22680
23830
  name: "blume",
@@ -22694,7 +23844,8 @@ var main = defineCommand15({
22694
23844
  preview: previewCommand,
22695
23845
  sync: syncCommand,
22696
23846
  translate: translateCommand,
22697
- validate: validateCommand
23847
+ validate: validateCommand,
23848
+ version: versionCommand
22698
23849
  }
22699
23850
  });
22700
23851
  loadEnvFiles(process.cwd());
@@ -22708,5 +23859,5 @@ process.on("unhandledRejection", (error) => {
22708
23859
  });
22709
23860
  runMain(main);
22710
23861
 
22711
- //# debugId=07BF7082AA0A5EEC64756E2164756E21
23862
+ //# debugId=A01DCE54DD00393164756E2164756E21
22712
23863
  //# sourceMappingURL=index.js.map