blume 1.4.3 → 1.5.0

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 (194) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/cli/index.js +1621 -576
  3. package/dist/cli/index.js.map +109 -104
  4. package/dist/types/ai/component-markdown.d.ts +14 -4
  5. package/dist/types/core/config-input.d.ts +79 -27
  6. package/dist/types/core/config.d.ts +2 -1
  7. package/dist/types/core/data.d.ts +16 -1
  8. package/dist/types/core/diagnostics.d.ts +5 -1
  9. package/dist/types/core/i18n-ui.d.ts +12 -0
  10. package/dist/types/core/schema.d.ts +112 -15
  11. package/dist/types/core/sources/types.d.ts +3 -1
  12. package/dist/types/core/standard-schema.d.ts +7 -3
  13. package/dist/types/core/types.d.ts +43 -2
  14. package/dist/types/core/ui-packs/index.d.ts +9 -1
  15. package/dist/types/openapi/references.d.ts +6 -5
  16. package/dist/types/seo/x-handle.d.ts +3 -2
  17. package/docs/advanced/api-reference.mdx +8 -6
  18. package/docs/configuration/search.mdx +2 -0
  19. package/docs/configuration/seo.mdx +1 -1
  20. package/docs/content/i18n.mdx +1 -1
  21. package/docs/content/meta.mdx +2 -1
  22. package/docs/content/meta.ts +1 -0
  23. package/docs/content/navigation.mdx +35 -1
  24. package/docs/content/versioning.mdx +106 -0
  25. package/docs/reference/cli.mdx +1 -0
  26. package/docs/reference/frontmatter.mdx +3 -0
  27. package/package.json +3 -1
  28. package/skills/blume-migrate/SKILL.md +2 -2
  29. package/skills/blume-migrate/references/docusaurus.md +1 -1
  30. package/skills/blume-migrate/references/fumadocs.md +1 -1
  31. package/skills/blume-migrate/references/mintlify.md +1 -1
  32. package/src/ai/agent-readability.ts +37 -10
  33. package/src/ai/ask-context.ts +5 -1
  34. package/src/ai/ask.ts +10 -1
  35. package/src/ai/component-markdown.ts +80 -43
  36. package/src/ai/llms.ts +40 -16
  37. package/src/ai/mcp/data.ts +48 -12
  38. package/src/ai/mcp/discovery.ts +28 -11
  39. package/src/ai/mcp/server.ts +183 -38
  40. package/src/ai/mcp/tools.ts +3 -3
  41. package/src/ai/skills.ts +32 -9
  42. package/src/ai/visibility.ts +2 -2
  43. package/src/astro/component-slots.ts +2 -0
  44. package/src/astro/examples.ts +6 -2
  45. package/src/astro/generate.ts +54 -29
  46. package/src/astro/integration.ts +13 -2
  47. package/src/astro/islands.ts +16 -9
  48. package/src/astro/templates.ts +152 -33
  49. package/src/audit/agent.ts +2 -2
  50. package/src/audit/checks/content.ts +26 -11
  51. package/src/audit/checks/dns-aid.ts +3 -0
  52. package/src/audit/checks/indexability.ts +24 -6
  53. package/src/audit/checks/llms.ts +9 -4
  54. package/src/audit/checks/network.ts +2 -0
  55. package/src/audit/checks/social.ts +18 -10
  56. package/src/audit/crawl.ts +37 -9
  57. package/src/audit/report.ts +20 -19
  58. package/src/audit/run.ts +5 -2
  59. package/src/audit/snapshot.ts +2 -4
  60. package/src/audit/types.ts +25 -3
  61. package/src/blume-modules.d.ts +5 -1
  62. package/src/cli/commands/audit.ts +9 -4
  63. package/src/cli/commands/build.ts +15 -9
  64. package/src/cli/commands/dev.ts +2 -0
  65. package/src/cli/commands/doctor.ts +2 -0
  66. package/src/cli/commands/eval.ts +7 -3
  67. package/src/cli/commands/init.ts +9 -9
  68. package/src/cli/commands/mcp-stdio.ts +3 -0
  69. package/src/cli/commands/translate.ts +14 -3
  70. package/src/cli/commands/version.ts +85 -0
  71. package/src/cli/dev-lock.ts +31 -10
  72. package/src/cli/eject-scripts.ts +17 -2
  73. package/src/cli/index.ts +2 -0
  74. package/src/cli/init/questions.ts +1 -1
  75. package/src/cli/init/scaffold.ts +22 -15
  76. package/src/cli/internal-error.ts +1 -0
  77. package/src/components/content/auto-type-table.ts +3 -0
  78. package/src/components/content/diff.ts +9 -5
  79. package/src/components/content/github-info.ts +2 -0
  80. package/src/components/islands/ask-ai.tsx +33 -25
  81. package/src/components/islands/hooks.ts +5 -1
  82. package/src/components/islands/webmcp.ts +49 -12
  83. package/src/components/layout/Header.astro +25 -1
  84. package/src/components/layout/NavSelector.astro +11 -2
  85. package/src/components/layout/NavTree.astro +4 -2
  86. package/src/components/layout/RootLayout.astro +18 -0
  87. package/src/components/layout/Search.astro +77 -13
  88. package/src/components/layout/VersionBanner.astro +39 -0
  89. package/src/components/layout/analytics-client.ts +8 -5
  90. package/src/components/layout/hydration-hint.ts +1 -1
  91. package/src/components/layout/nav-utils.ts +1 -4
  92. package/src/components/layout/overrides.ts +25 -12
  93. package/src/components/layout/search/algolia.ts +18 -5
  94. package/src/components/layout/search/endpoint.ts +3 -0
  95. package/src/components/layout/search/flexsearch.ts +23 -7
  96. package/src/components/layout/search/orama-cloud.ts +1 -1
  97. package/src/components/layout/search/orama.ts +4 -1
  98. package/src/components/layout/search/pagefind.ts +2 -0
  99. package/src/components/layout/search/types.ts +13 -1
  100. package/src/components/layout/search/typesense.ts +19 -3
  101. package/src/components/openapi/ApiOverview.astro +32 -6
  102. package/src/components/openapi/AsyncApiOperation.astro +237 -0
  103. package/src/components/openapi/Bindings.astro +89 -0
  104. package/src/components/openapi/MethodBadge.astro +3 -0
  105. package/src/components/openapi/Operation.astro +7 -2
  106. package/src/components/openapi/PanelTabs.astro +131 -0
  107. package/src/components/openapi/ParametersTable.astro +2 -0
  108. package/src/components/openapi/RequestPanel.astro +12 -119
  109. package/src/components/openapi/async-snippets.ts +174 -0
  110. package/src/components/openapi/async.ts +348 -0
  111. package/src/components/openapi/helpers.ts +52 -20
  112. package/src/components/openapi/security.ts +102 -29
  113. package/src/components/openapi/snippets.ts +11 -11
  114. package/src/core/component-overrides.ts +28 -23
  115. package/src/core/config-input.ts +88 -27
  116. package/src/core/config.ts +20 -7
  117. package/src/core/content.ts +3 -1
  118. package/src/core/data.ts +16 -1
  119. package/src/core/define-components.ts +5 -0
  120. package/src/core/diagnostics.ts +46 -38
  121. package/src/core/frontmatter.ts +33 -7
  122. package/src/core/graph.ts +137 -53
  123. package/src/core/i18n-ui.ts +15 -0
  124. package/src/core/i18n.ts +16 -8
  125. package/src/core/load-module.ts +1 -0
  126. package/src/core/manifest.ts +92 -3
  127. package/src/core/meta.ts +44 -14
  128. package/src/core/nav-diagnostics.ts +3 -3
  129. package/src/core/navigation.ts +247 -67
  130. package/src/core/project-graph.ts +15 -3
  131. package/src/core/schema.ts +213 -67
  132. package/src/core/sources/assets.ts +2 -0
  133. package/src/core/sources/cache.ts +6 -0
  134. package/src/core/sources/github-releases.ts +39 -31
  135. package/src/core/sources/mdx-remote.ts +4 -0
  136. package/src/core/sources/normalize.ts +67 -20
  137. package/src/core/sources/notion.ts +49 -17
  138. package/src/core/sources/portable-text.ts +32 -11
  139. package/src/core/sources/sanity.ts +68 -14
  140. package/src/core/sources/types.ts +4 -0
  141. package/src/core/sources/watch.ts +1 -1
  142. package/src/core/standard-schema.ts +9 -3
  143. package/src/core/text-width.ts +26 -0
  144. package/src/core/tsconfig-aliases.ts +9 -5
  145. package/src/core/types.ts +45 -2
  146. package/src/core/ui-packs/index.ts +9 -1
  147. package/src/core/version-cut.ts +301 -0
  148. package/src/core/version.ts +2 -0
  149. package/src/core/versions.ts +170 -0
  150. package/src/deploy/adapter-output.ts +5 -2
  151. package/src/deploy/cloudflare-negotiation.ts +25 -10
  152. package/src/deploy/sitemap.ts +33 -1
  153. package/src/deploy/vercel-negotiation.ts +11 -4
  154. package/src/eval/report.ts +4 -4
  155. package/src/eval/run.ts +2 -2
  156. package/src/eval/schema.ts +1 -1
  157. package/src/markdown/base-links.ts +6 -6
  158. package/src/markdown/directives.ts +7 -1
  159. package/src/markdown/heading-anchors.ts +17 -6
  160. package/src/markdown/index.ts +73 -24
  161. package/src/markdown/inline-code.ts +14 -2
  162. package/src/markdown/language-icon.ts +6 -2
  163. package/src/markdown/mdast.ts +18 -4
  164. package/src/markdown/package-commands.ts +6 -8
  165. package/src/markdown/table-wrap.ts +4 -1
  166. package/src/markdown/twoslash.ts +2 -0
  167. package/src/og/card.ts +30 -11
  168. package/src/og/derive.ts +43 -27
  169. package/src/openapi/asyncapi.ts +366 -0
  170. package/src/openapi/model.ts +126 -57
  171. package/src/openapi/parse.ts +97 -5
  172. package/src/openapi/references.ts +12 -10
  173. package/src/openapi/render-mdx.ts +73 -34
  174. package/src/openapi/scalar.ts +6 -8
  175. package/src/openapi/source.ts +98 -28
  176. package/src/registry/eject.ts +7 -2
  177. package/src/search/documents.ts +25 -5
  178. package/src/search/facets.ts +7 -5
  179. package/src/search/orama-index.ts +66 -20
  180. package/src/search/popular.ts +10 -5
  181. package/src/search/providers.ts +2 -2
  182. package/src/search/sync/index.ts +2 -0
  183. package/src/search/sync/typesense.ts +4 -2
  184. package/src/seo/jsonld.ts +24 -6
  185. package/src/seo/x-handle.ts +8 -3
  186. package/src/theme/chrome-icons.ts +7 -2
  187. package/src/theme/fonts.ts +8 -4
  188. package/src/theme/icons.ts +4 -2
  189. package/src/theme/palette.ts +22 -14
  190. package/src/translate/meta.ts +15 -6
  191. package/src/translate/report.ts +9 -5
  192. package/src/translate/run.ts +10 -4
  193. package/src/translate/validate.ts +52 -17
  194. 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);
@@ -3626,9 +3650,9 @@ var ADAPTER_IMPORTS = {
3626
3650
  node: "@astrojs/node",
3627
3651
  vercel: "@astrojs/vercel"
3628
3652
  };
3629
- var ADAPTER_OPTIONS = {
3630
- node: '{ mode: "standalone" }'
3631
- };
3653
+ var ADAPTER_OPTIONS = new Map([
3654
+ ["node", '{ mode: "standalone" }']
3655
+ ]);
3632
3656
  var WRANGLER_CONFIG_FILES = [
3633
3657
  "wrangler.jsonc",
3634
3658
  "wrangler.json",
@@ -3790,7 +3814,7 @@ var astroConfigTemplate = (options) => {
3790
3814
  if (deployment.adapter === "cloudflare") {
3791
3815
  return resolveCloudflareAdapterArgs(context);
3792
3816
  }
3793
- return ADAPTER_OPTIONS[deployment.adapter] ?? "";
3817
+ return ADAPTER_OPTIONS.get(deployment.adapter) ?? "";
3794
3818
  })();
3795
3819
  const adapterExpr = deployment.adapter === "vercel" ? `withAdapterRoot(adapter(${adapterArgs}), ${JSON.stringify(adapterRoot(context))})` : `adapter(${adapterArgs})`;
3796
3820
  const adapterOption = server && deployment.adapter ? `
@@ -3817,13 +3841,21 @@ var astroConfigTemplate = (options) => {
3817
3841
  ])))},` : "";
3818
3842
  const fontEntries = buildFontEntries(config.theme.fonts);
3819
3843
  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(", ")}],` : "";
3844
+ 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) => {
3845
+ const face = {};
3846
+ if (variant.weight !== undefined) {
3847
+ face.weight = variant.weight;
3848
+ }
3849
+ if (variant.style !== undefined) {
3850
+ face.style = variant.style;
3851
+ }
3852
+ return {
3853
+ ...face,
3854
+ src: [
3855
+ isAbsolute(variant.src) ? variant.src : join8(context.root, variant.src)
3856
+ ]
3857
+ };
3858
+ }))} } }` : `{ 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
3859
  const defineConfigImport = astroConfigImportLine({
3828
3860
  hasFonts: fontEntries.length > 0,
3829
3861
  hasSession: sessionOption.length > 0
@@ -4638,11 +4670,13 @@ export function getStaticPaths() {
4638
4670
  locale: route.locale,
4639
4671
  route: route.path,
4640
4672
  title: route.title,
4673
+ version: route.version,
4674
+ versionAlternates: route.versionAlternates,
4641
4675
  },
4642
4676
  }));
4643
4677
  }
4644
4678
 
4645
- const { entryId, collection, route, title, indexable, editUrl, lastModified, locale, alternates, fallback } = Astro.props;
4679
+ const { entryId, collection, route, title, indexable, editUrl, lastModified, locale, alternates, fallback, version, versionAlternates } = Astro.props;
4646
4680
  const entry = await getEntry(collection as CollectionKey, entryId);
4647
4681
  if (!entry) {
4648
4682
  return new Response(null, { status: 404 });
@@ -4674,9 +4708,6 @@ const ogGenerated = !seo.image && Boolean(ogPath);
4674
4708
  const x = { ...data.config.x, ...(seo.x?.creator ? { creator: seo.x.creator } : {}) };
4675
4709
 
4676
4710
  const basedRoute = withBase(route);
4677
- const canonical =
4678
- seo.canonical ??
4679
- (base ? \`\${base}\${basedRoute === "/" ? "" : encodeURI(basedRoute)}\` : null);
4680
4711
 
4681
4712
  // Locale resolution. With i18n on, pick the active locale's nav + dictionary,
4682
4713
  // build hreflang alternates, and derive the language-switcher targets.
@@ -4697,7 +4728,20 @@ const stripLocale = (path: string, codeArg: string) => {
4697
4728
  return prefix && path.startsWith(prefix) ? path.slice(prefix.length) || "/" : path;
4698
4729
  };
4699
4730
 
4700
- const navigation = i18n ? (data.navigationByLocale[locale] ?? data.navigation) : data.navigation;
4731
+ // Version resolution. An archived page renders its snapshot's navigation tree,
4732
+ // points its canonical at the latest equivalent (unless configured otherwise),
4733
+ // and shows the old-version notice.
4734
+ const versionsConfig = data.config.versions;
4735
+ const archived = versionsConfig && version
4736
+ ? (versionsConfig.archived.find((v) => v.id === version) ?? null)
4737
+ : null;
4738
+ const latestVersionAlt = (versionAlternates ?? []).find((alt) => alt.version === "");
4739
+
4740
+ const navigation = version
4741
+ ? (data.navigationByVersion[version]?.[i18n ? locale : ""] ?? data.navigation)
4742
+ : i18n
4743
+ ? (data.navigationByLocale[locale] ?? data.navigation)
4744
+ : data.navigation;
4701
4745
  const ui = i18n ? (data.uiByLocale[locale] ?? data.ui) : data.ui;
4702
4746
  const localeMeta = i18n ? i18n.locales.find((l) => l.code === locale) : null;
4703
4747
  const dir = localeMeta?.dir ?? "ltr";
@@ -4714,6 +4758,19 @@ const absolute = (path: string) => {
4714
4758
  return base + (p === "/" ? "" : p);
4715
4759
  };
4716
4760
 
4761
+ // An archived page defaults its canonical to the same page in the latest docs
4762
+ // when that page still exists — search engines treat the live page as
4763
+ // authoritative without deindexing version-only content. A page's own
4764
+ // \`seo.canonical\` always wins, and \`canonical: "self"\` keeps the default.
4765
+ const canonical =
4766
+ seo.canonical ??
4767
+ (archived && archived.canonical === "latest" && latestVersionAlt && base
4768
+ ? absolute(latestVersionAlt.path)
4769
+ : base
4770
+ ? \`\${base}\${basedRoute === "/" ? "" : encodeURI(basedRoute)}\`
4771
+ : null);
4772
+ const effectiveNoindex = Boolean(seo.noindex) || (archived?.noindex ?? false);
4773
+
4717
4774
  const localeAlternates =
4718
4775
  i18n && base
4719
4776
  ? (alternates ?? []).map((alt) => ({ hreflang: alt.locale, href: absolute(alt.path) }))
@@ -4736,6 +4793,72 @@ const localeSwitch = i18n
4736
4793
  })
4737
4794
  : [];
4738
4795
 
4796
+ // Version switcher + old-version notice. The switcher auto-populates from the
4797
+ // versions config as a \`kind: "version"\` selector; a user-declared version
4798
+ // selector in \`navigation.selectors\` suppresses it (theirs renders instead).
4799
+ // Fallback version roots compose like real routes — \`{basePath}/{locale?}/{id}\`
4800
+ // (manifest \`versionAlternates\` paths arrive with the base already applied).
4801
+ const versionRootFor = (id: string) => {
4802
+ const logical = id ? \`/\${id}\` : "/";
4803
+ const localized = i18n ? localizeRoute(logical, locale) : logical;
4804
+ const mount = data.config.basePath;
4805
+ if (!mount) {
4806
+ return localized;
4807
+ }
4808
+ return localized === "/" ? mount : \`\${mount}\${localized}\`;
4809
+ };
4810
+ const samePageSwitch = versionsConfig
4811
+ ? versionsConfig.switcher.redirect === "same-page"
4812
+ : true;
4813
+ const userHasVersionSelector = navigation.selectors.some(
4814
+ (selector) => selector.kind === "version"
4815
+ );
4816
+ const versionSelector =
4817
+ versionsConfig && !userHasVersionSelector
4818
+ ? {
4819
+ items: [
4820
+ {
4821
+ id: "",
4822
+ label: versionsConfig.current.label,
4823
+ tag: versionsConfig.current.badge,
4824
+ },
4825
+ ...versionsConfig.archived.map((v) => ({
4826
+ id: v.id,
4827
+ label: v.label ?? v.id,
4828
+ tag: undefined,
4829
+ })),
4830
+ ].map((entry) => {
4831
+ const alt = (versionAlternates ?? []).find(
4832
+ (a) => a.version === entry.id
4833
+ );
4834
+ return {
4835
+ label: entry.label,
4836
+ path: samePageSwitch && alt ? alt.path : versionRootFor(entry.id),
4837
+ ...(entry.tag ? { tag: entry.tag } : {}),
4838
+ };
4839
+ }),
4840
+ kind: "version" as const,
4841
+ label: ui.versions.switcher,
4842
+ }
4843
+ : null;
4844
+
4845
+ const versionNotice =
4846
+ archived && archived.banner !== false
4847
+ ? {
4848
+ latestHref: latestVersionAlt
4849
+ ? latestVersionAlt.path
4850
+ : versionRootFor(""),
4851
+ latestLabel: ui.versions.latest,
4852
+ message:
4853
+ typeof archived.banner === "string"
4854
+ ? archived.banner
4855
+ : ui.versions.notice.replace(
4856
+ "{version}",
4857
+ archived.label ?? archived.id
4858
+ ),
4859
+ }
4860
+ : null;
4861
+
4739
4862
  // The whole page shell is overridable via \`layout.Layout\`; it receives the same
4740
4863
  // props as the built-in RootLayout, plus the \`layout\` map for its inner slots.
4741
4864
  const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
@@ -4760,6 +4883,9 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
4760
4883
  localeAlternates={localeAlternates}
4761
4884
  xDefault={xDefault}
4762
4885
  localeSwitch={localeSwitch}
4886
+ versionSelector={versionSelector}
4887
+ versionNotice={versionNotice}
4888
+ searchVersion={versionsConfig ? version : null}
4763
4889
  page={{ title: seo.title ?? title, description: seo.description ?? frontmatter.description, route }}
4764
4890
  headings={headings}
4765
4891
  toc={data.config.toc}
@@ -4782,7 +4908,7 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
4782
4908
  pageType={frontmatter.type}
4783
4909
  published={frontmatter.date ?? frontmatter.changelog?.date ?? null}
4784
4910
  lastModified={lastModified}
4785
- noindex={seo.noindex}
4911
+ noindex={effectiveNoindex}
4786
4912
  structuredDataEnabled={data.config.structuredData}
4787
4913
  >
4788
4914
  <h1>{title}</h1>
@@ -5467,6 +5593,9 @@ var sitemapParser = new XMLParser({
5467
5593
  parseTagValue: false,
5468
5594
  removeNSPrefix: true
5469
5595
  });
5596
+ var isUrlsetElement = (value) => typeof value === "object" && value !== null;
5597
+ var isUrlEntry = (value) => typeof value === "object" && value !== null;
5598
+ var isText = (value) => typeof value === "string";
5470
5599
  var parseSitemap = (file, xml, bytes) => {
5471
5600
  const doc = { bytes, file, lastmod: new Map, urls: [] };
5472
5601
  let parsed;
@@ -5480,19 +5609,19 @@ var parseSitemap = (file, xml, bytes) => {
5480
5609
  doc.error = Object.hasOwn(parsed, "sitemapindex") ? "sitemap is an index, not a urlset" : "no <urlset> element";
5481
5610
  return doc;
5482
5611
  }
5483
- const urlset = parsed.urlset;
5484
- const entries = typeof urlset === "object" && urlset !== null ? [urlset.url].flat() : [];
5612
+ const { urlset } = parsed;
5613
+ const entries = isUrlsetElement(urlset) ? [urlset.url].flat() : [];
5485
5614
  for (const entry of entries) {
5486
- if (typeof entry !== "object" || entry === null) {
5615
+ if (!isUrlEntry(entry)) {
5487
5616
  continue;
5488
5617
  }
5489
5618
  const { loc, lastmod } = entry;
5490
- const locText = typeof loc === "string" ? loc.trim() : "";
5619
+ const locText = isText(loc) ? loc.trim() : "";
5491
5620
  if (!locText) {
5492
5621
  continue;
5493
5622
  }
5494
5623
  doc.urls.push(locText);
5495
- if (typeof lastmod === "string" && lastmod.trim() !== "") {
5624
+ if (isText(lastmod) && lastmod.trim() !== "") {
5496
5625
  doc.lastmod?.set(locText, lastmod.trim());
5497
5626
  }
5498
5627
  }
@@ -5740,6 +5869,7 @@ import { relative as relative10 } from "pathe";
5740
5869
 
5741
5870
  // src/core/config.ts
5742
5871
  import { existsSync as existsSync6, readFileSync as readFileSync3 } from "node:fs";
5872
+ import { z as z3 } from "zod";
5743
5873
 
5744
5874
  // src/core/load-module.ts
5745
5875
  import { createJiti } from "jiti";
@@ -5795,8 +5925,9 @@ var resolveProjectContext = (root, config, options) => {
5795
5925
  import { z as z2 } from "zod";
5796
5926
 
5797
5927
  // src/seo/x-handle.ts
5928
+ var isString = (value) => typeof value === "string";
5798
5929
  var normalizeXHandle = (value) => {
5799
- if (typeof value !== "string") {
5930
+ if (!isString(value)) {
5800
5931
  return;
5801
5932
  }
5802
5933
  const handle = value.trim().replace(/^@+/u, "");
@@ -9377,6 +9508,7 @@ var uiStringsObject = z.object({
9377
9508
  search: z.object({
9378
9509
  all: z.string().default("All"),
9379
9510
  allLanguages: z.string().default("All languages"),
9511
+ allVersions: z.string().default("All versions"),
9380
9512
  askAi: z.string().default("Ask AI"),
9381
9513
  askAiHint: z.string().default("Get an instant answer from AI"),
9382
9514
  button: z.string().default("Search"),
@@ -9393,6 +9525,11 @@ var uiStringsObject = z.object({
9393
9525
  }).prefault({}),
9394
9526
  toc: z.object({
9395
9527
  title: z.string().default("On this page")
9528
+ }).prefault({}),
9529
+ versions: z.object({
9530
+ latest: z.string().default("Go to latest"),
9531
+ notice: z.string().default("You're viewing documentation for {version}. It may be out of date."),
9532
+ switcher: z.string().default("Version")
9396
9533
  }).prefault({})
9397
9534
  });
9398
9535
  var uiStringsSchema = uiStringsObject.prefault({});
@@ -9433,12 +9570,16 @@ var resolveUIStrings = (locale, options) => {
9433
9570
  var isStandardSchema = (value) => typeof value === "object" && value !== null && typeof value["~standard"]?.validate === "function";
9434
9571
 
9435
9572
  // src/core/schema.ts
9573
+ var isString2 = (value) => typeof value === "string";
9574
+ var isBoolean = (value) => typeof value === "boolean";
9436
9575
  var iconName = z2.string().min(1);
9437
9576
  var DEFAULT_CONTENT_GLOB = "**/*.{md,mdx}";
9438
9577
  var hydrationMode = z2.enum(["load", "idle", "visible", "media", "only"]);
9439
9578
  var dateSchema = z2.union([z2.string(), z2.date()]).transform((value) => value instanceof Date ? value.toISOString() : value);
9579
+ var sidebarDisplaySchema = z2.enum(["flat", "group", "page"]);
9440
9580
  var sidebarMetaSchema = z2.strictObject({
9441
9581
  badge: z2.string().optional(),
9582
+ display: sidebarDisplaySchema.optional(),
9442
9583
  hidden: z2.boolean().default(false),
9443
9584
  icon: iconName.optional(),
9444
9585
  label: z2.string().optional(),
@@ -9495,11 +9636,12 @@ var pageMetaBaseSchema = z2.strictObject({
9495
9636
  type: z2.string().optional()
9496
9637
  });
9497
9638
  var pageMetaSchema = pageMetaBaseSchema;
9639
+ var BUILT_IN_PAGE_META_KEYS = new Set(pageMetaBaseSchema.keyof().options);
9498
9640
  var customKeySchemaRecord = (where) => z2.record(z2.string(), z2.custom(isStandardSchema, {
9499
9641
  message: "Expected a Standard Schema (e.g. a Zod schema — any Zod version works)."
9500
9642
  })).default({}).superRefine((value, ctx) => {
9501
9643
  for (const key of Object.keys(value)) {
9502
- if (Object.hasOwn(pageMetaBaseSchema.shape, key)) {
9644
+ if (BUILT_IN_PAGE_META_KEYS.has(key)) {
9503
9645
  ctx.addIssue({
9504
9646
  code: z2.ZodIssueCode.custom,
9505
9647
  message: `"${key}" is a built-in frontmatter field and cannot be redeclared via ${where}.`,
@@ -9508,9 +9650,9 @@ var customKeySchemaRecord = (where) => z2.record(z2.string(), z2.custom(isStanda
9508
9650
  }
9509
9651
  }
9510
9652
  });
9511
- var sidebarDisplaySchema = z2.enum(["flat", "group", "page"]);
9512
9653
  var folderMetaSchema = z2.strictObject({
9513
9654
  collapsed: z2.boolean().optional(),
9655
+ display: sidebarDisplaySchema.optional(),
9514
9656
  icon: iconName.optional(),
9515
9657
  order: z2.number().optional(),
9516
9658
  pages: z2.array(z2.string()).optional(),
@@ -9604,7 +9746,7 @@ var githubReleasesSourceSchema = z2.strictObject({
9604
9746
  type: z2.literal("github-releases")
9605
9747
  });
9606
9748
  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)" }),
9749
+ 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
9750
  type: z2.literal("custom")
9609
9751
  });
9610
9752
  var contentSourceSchema = z2.discriminatedUnion("type", [
@@ -9695,7 +9837,7 @@ var localFontSchema = z2.strictObject({
9695
9837
  variants: z2.array(localFontVariantSchema).nonempty()
9696
9838
  });
9697
9839
  var fontValueSchema = z2.union([z2.string(), remoteFontSchema, localFontSchema]).superRefine((value, ctx) => {
9698
- if (typeof value === "string" && !isFontSlug(value)) {
9840
+ if (isString2(value) && !isFontSlug(value)) {
9699
9841
  ctx.addIssue({
9700
9842
  code: z2.ZodIssueCode.custom,
9701
9843
  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,12 +9850,12 @@ var perModeValueSchema = z2.union([
9708
9850
  dark: z2.string().optional(),
9709
9851
  light: z2.string().optional()
9710
9852
  })
9711
- ]).optional().transform((value) => typeof value === "string" ? { dark: value, light: value } : value);
9853
+ ]).optional().transform((value) => isString2(value) ? { dark: value, light: value } : value);
9712
9854
  var themeConfigSchema = z2.strictObject({
9713
9855
  accent: z2.union([
9714
9856
  z2.string(),
9715
9857
  z2.strictObject({ dark: z2.string(), light: z2.string() })
9716
- ]).default("blue").transform((value) => typeof value === "string" ? { dark: value, light: value } : value),
9858
+ ]).default("blue").transform((value) => isString2(value) ? { dark: value, light: value } : value),
9717
9859
  action: z2.string().optional(),
9718
9860
  background: perModeValueSchema,
9719
9861
  backgroundImage: perModeValueSchema,
@@ -9796,7 +9938,7 @@ var askAiProviders = [
9796
9938
  ];
9797
9939
  var PRIVATE_JWK_PARAMS = ["d", "p", "q", "dp", "dq", "qi", "oth", "k"];
9798
9940
  var publicJwkSchema = z2.record(z2.string(), z2.unknown()).superRefine((jwk, ctx) => {
9799
- if (typeof jwk.kty !== "string" || jwk.kty.length === 0) {
9941
+ if (!isString2(jwk.kty) || jwk.kty.length === 0) {
9800
9942
  ctx.addIssue({
9801
9943
  code: z2.ZodIssueCode.custom,
9802
9944
  message: 'A JWK must declare its key type ("kty").'
@@ -9857,7 +9999,7 @@ var aiConfigSchema = z2.strictObject({
9857
9999
  enabled: z2.boolean().default(true),
9858
10000
  openapi: z2.boolean().default(true)
9859
10001
  })
9860
- ]).default(true).transform((value) => typeof value === "boolean" ? { enabled: value, openapi: true } : value),
10002
+ ]).default(true).transform((value) => isBoolean(value) ? { enabled: value, openapi: true } : value),
9861
10003
  markdownComponents: z2.record(z2.string(), z2.custom((value) => typeof value === "function", {
9862
10004
  message: "Expected a serializer function."
9863
10005
  })).default({}),
@@ -9892,7 +10034,7 @@ var exportConfigSchema = z2.union([
9892
10034
  epub: z2.boolean().default(false),
9893
10035
  pdf: z2.boolean().default(false)
9894
10036
  })
9895
- ]).transform((value) => typeof value === "boolean" ? { epub: value, pdf: value } : value);
10037
+ ]).transform((value) => isBoolean(value) ? { epub: value, pdf: value } : value);
9896
10038
  var localeSchema = z2.strictObject({
9897
10039
  code: z2.string().min(1),
9898
10040
  dir: z2.enum(["ltr", "rtl"]).default("ltr"),
@@ -9923,6 +10065,36 @@ var i18nConfigSchema = z2.strictObject({
9923
10065
  });
9924
10066
  }
9925
10067
  });
10068
+ var VERSION_ID = /^[A-Za-z][\w.-]*$/u;
10069
+ var archivedVersionSchema = z2.strictObject({
10070
+ banner: z2.union([z2.boolean(), z2.string()]).default(true),
10071
+ canonical: z2.enum(["latest", "self"]).default("latest"),
10072
+ 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.'),
10073
+ label: z2.string().optional(),
10074
+ noindex: z2.boolean().default(false)
10075
+ });
10076
+ var versionsConfigSchema = z2.strictObject({
10077
+ archived: z2.array(archivedVersionSchema).default([]),
10078
+ current: z2.strictObject({
10079
+ badge: z2.string().optional(),
10080
+ label: z2.string()
10081
+ }),
10082
+ switcher: z2.strictObject({
10083
+ redirect: z2.enum(["same-page", "root"]).default("same-page")
10084
+ }).prefault({})
10085
+ }).superRefine((value, ctx) => {
10086
+ const seen = new Set;
10087
+ for (const [position, version] of value.archived.entries()) {
10088
+ if (seen.has(version.id)) {
10089
+ ctx.addIssue({
10090
+ code: z2.ZodIssueCode.custom,
10091
+ message: `versions.archived declares "${version.id}" more than once.`,
10092
+ path: ["archived", position, "id"]
10093
+ });
10094
+ }
10095
+ seen.add(version.id);
10096
+ }
10097
+ });
9926
10098
  var analyticsScriptSchema = z2.strictObject({
9927
10099
  attributes: z2.record(z2.string(), z2.string()).optional(),
9928
10100
  content: z2.string().optional(),
@@ -10036,18 +10208,18 @@ var githubConfigSchema = z2.strictObject({
10036
10208
  owner: z2.string(),
10037
10209
  repo: z2.string()
10038
10210
  });
10211
+ var isThemeObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
10039
10212
  var codeThemeSchema = z2.custom((value) => {
10040
- if (typeof value === "string") {
10213
+ if (isString2(value)) {
10041
10214
  return true;
10042
10215
  }
10043
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
10216
+ if (!isThemeObject(value)) {
10044
10217
  return false;
10045
10218
  }
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;
10219
+ const settingsValid = value.settings === undefined || Array.isArray(value.settings);
10220
+ const tokenColorsValid = value.tokenColors === undefined || Array.isArray(value.tokenColors);
10221
+ const colorsValid = value.colors === undefined || isThemeObject(value.colors);
10222
+ const hasContent = value.settings !== undefined || value.tokenColors !== undefined || value.colors !== undefined;
10051
10223
  return settingsValid && tokenColorsValid && colorsValid && hasContent;
10052
10224
  }, "Expected a Shiki theme name or custom theme object");
10053
10225
  var codeBlockThemeSchema = z2.strictObject({
@@ -10063,7 +10235,7 @@ var examplesConfigSchema = z2.union([
10063
10235
  css: z2.string().optional(),
10064
10236
  source: z2.string().default("examples")
10065
10237
  })
10066
- ]).transform((value) => typeof value === "string" ? { source: value } : value);
10238
+ ]).transform((value) => isString2(value) ? { source: value } : value);
10067
10239
  var lastModifiedConfigSchema = z2.union([
10068
10240
  z2.boolean(),
10069
10241
  z2.strictObject({ type: z2.enum(["git", "frontmatter"]).default("git") })
@@ -10103,24 +10275,24 @@ var openapiSourceSchema = z2.strictObject({
10103
10275
  spec: z2.string()
10104
10276
  });
10105
10277
  var scalarConfigSchema = z2.record(z2.string(), z2.unknown()).optional();
10106
- var openapiConfigSchema = z2.strictObject({
10107
- codeSamples: z2.array(z2.string()).default(["curl", "js", "python"]),
10278
+ var referenceConfigSchema = (defaults) => z2.strictObject({
10279
+ codeSamples: z2.array(z2.string()).default(defaults.codeSamples),
10108
10280
  enabled: z2.boolean().default(false),
10109
10281
  expandSchemas: z2.boolean().default(false),
10110
10282
  renderer: z2.enum(["blume", "scalar"]).default("blume"),
10111
- route: z2.string().default("/reference"),
10283
+ route: z2.string().default(defaults.route),
10112
10284
  scalar: scalarConfigSchema,
10113
10285
  sources: z2.array(openapiSourceSchema).default([]),
10114
10286
  spec: z2.string().optional(),
10115
10287
  theme: z2.string().optional()
10116
10288
  });
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()
10289
+ var openapiConfigSchema = referenceConfigSchema({
10290
+ codeSamples: ["curl", "js", "python"],
10291
+ route: "/reference"
10292
+ });
10293
+ var asyncapiConfigSchema = referenceConfigSchema({
10294
+ codeSamples: [],
10295
+ route: "/events"
10124
10296
  });
10125
10297
  var frontmatterConfigSchema = z2.strictObject({
10126
10298
  extend: customKeySchemaRecord("frontmatter.extend")
@@ -10132,7 +10304,7 @@ var tocConfigSchema = z2.union([
10132
10304
  minHeadingLevel: z2.number().int().min(1).max(6).optional()
10133
10305
  })
10134
10306
  ]).default(true).transform((value) => {
10135
- if (typeof value === "boolean") {
10307
+ if (isBoolean(value)) {
10136
10308
  return { enabled: value, maxLevel: 3, minLevel: 2 };
10137
10309
  }
10138
10310
  return {
@@ -10172,8 +10344,21 @@ var blumeConfigSchema = z2.strictObject({
10172
10344
  seo: seoConfigSchema.prefault({}),
10173
10345
  theme: themeConfigSchema.prefault({}),
10174
10346
  title: z2.string().default("Documentation"),
10175
- toc: tocConfigSchema
10347
+ toc: tocConfigSchema,
10348
+ versions: versionsConfigSchema.optional()
10176
10349
  }).superRefine((config, ctx) => {
10350
+ if (config.versions && config.i18n) {
10351
+ const localeCodes = new Set(config.i18n.locales.map((locale) => locale.code.toLowerCase()));
10352
+ for (const [position, version] of config.versions.archived.entries()) {
10353
+ if (localeCodes.has(version.id.toLowerCase())) {
10354
+ ctx.addIssue({
10355
+ code: z2.ZodIssueCode.custom,
10356
+ message: `Version id "${version.id}" is also a configured locale code — rename the version (e.g. "v${version.id}").`,
10357
+ path: ["versions", "archived", position, "id"]
10358
+ });
10359
+ }
10360
+ }
10361
+ }
10177
10362
  for (const [typeName, typeConfig] of Object.entries(config.content.types)) {
10178
10363
  for (const key of Object.keys(typeConfig.frontmatter)) {
10179
10364
  if (Object.hasOwn(config.frontmatter.extend, key)) {
@@ -10198,9 +10383,12 @@ var blumeConfigSchema = z2.strictObject({
10198
10383
 
10199
10384
  // src/core/config.ts
10200
10385
  var importConfigModule = createModuleLoader();
10386
+ var themeFontsProbeSchema = z3.looseObject({
10387
+ theme: z3.looseObject({ fonts: z3.unknown() }).optional()
10388
+ });
10201
10389
  var loadConfig = async (root, options = {}) => {
10202
10390
  const configFile = findConfigFile(root);
10203
- let raw = {};
10391
+ let raw;
10204
10392
  if (configFile) {
10205
10393
  try {
10206
10394
  raw = await importConfigModule(configFile);
@@ -10213,7 +10401,8 @@ var loadConfig = async (root, options = {}) => {
10213
10401
  });
10214
10402
  }
10215
10403
  }
10216
- const themeFontsConfigured = Boolean(raw && typeof raw === "object" && raw.theme?.fonts !== undefined);
10404
+ const probe2 = themeFontsProbeSchema.safeParse(raw);
10405
+ const themeFontsConfigured = probe2.success && probe2.data.theme?.fonts !== undefined;
10217
10406
  const parsed = blumeConfigSchema.safeParse(raw ?? {});
10218
10407
  if (!parsed.success) {
10219
10408
  const source = configFile && existsSync6(configFile) ? readFileSync3(configFile, "utf-8") : undefined;
@@ -10320,12 +10509,14 @@ var localeTargetPath = (rel, ext, locale, i18n) => {
10320
10509
  }
10321
10510
  return `${locale}/${rel}`;
10322
10511
  };
10323
- var i18nDiagnostics = (pages, i18n) => {
10512
+ var i18nDiagnostics = (pages, i18n, versions) => {
10324
10513
  const configured = new Set(i18n.locales.map((locale) => locale.code.toLowerCase()));
10514
+ const versionDirs = new Set(versions?.archived.map((version) => version.id));
10325
10515
  const seen = new Set;
10326
10516
  const diagnostics = [];
10327
10517
  for (const page of pages) {
10328
- const first = page.source.ref.split("/")[0]?.toLowerCase();
10518
+ const parts = page.source.ref.split("/");
10519
+ const first = (parts[0] && versionDirs.has(parts[0]) ? parts[1] : parts[0])?.toLowerCase();
10329
10520
  if (first && !seen.has(first) && KNOWN_LOCALES.has(first) && !configured.has(first)) {
10330
10521
  seen.add(first);
10331
10522
  diagnostics.push({
@@ -10381,7 +10572,7 @@ var resolveIcon = (name) => {
10381
10572
  var hasIcon = (name) => resolveIcon(name) !== null;
10382
10573
 
10383
10574
  // src/core/nav-diagnostics.ts
10384
- var ICON_SHAPE_HINT = "Use a built-in icon name, an image path/URL, or inline SVG markup.";
10575
+ var ICON_FORMAT_HINT = "Use a built-in icon name, an image path/URL, or inline SVG markup.";
10385
10576
  var flattenNodes = (nodes) => nodes.flatMap((node) => node.kind === "group" ? [node, ...flattenNodes(node.children)] : [node]);
10386
10577
  var collectIcons = (navigation) => {
10387
10578
  const icons = [];
@@ -10430,10 +10621,10 @@ var unknownIconDiagnostics = (icons, suggestion) => {
10430
10621
  }
10431
10622
  return diagnostics;
10432
10623
  };
10433
- var validateNavIcons = (navigation) => unknownIconDiagnostics(collectIcons(navigation), ICON_SHAPE_HINT);
10624
+ var validateNavIcons = (navigation) => unknownIconDiagnostics(collectIcons(navigation), ICON_FORMAT_HINT);
10434
10625
  var validateSearchPopularIcons = (popular) => {
10435
10626
  const icons = popular.flatMap((link) => link.icon ? [{ icon: link.icon, where: `popular link "${link.label}"` }] : []);
10436
- return unknownIconDiagnostics(icons, ICON_SHAPE_HINT);
10627
+ return unknownIconDiagnostics(icons, ICON_FORMAT_HINT);
10437
10628
  };
10438
10629
  var resolvesToPages = (routes, path) => routes.has(path) || [...routes].some((route) => route.startsWith(`${path}/`));
10439
10630
  var validateNavTargets = (navigation, routes) => {
@@ -10537,6 +10728,8 @@ var segmentKey = (raw) => {
10537
10728
  return (group ?? raw).replace(NUMERIC_PREFIX, "");
10538
10729
  };
10539
10730
  var isIndexStem = (stem) => stem.replace(NUMERIC_PREFIX, "") === "index";
10731
+ var stemOf = (filename) => filename.replace(extname(filename), "");
10732
+ var navStem = (navPath) => stemOf(navPath.split("/").at(-1) ?? navPath);
10540
10733
  var createGroup = (key, path, label, order) => ({
10541
10734
  children: [],
10542
10735
  index: new Map,
@@ -10561,7 +10754,7 @@ var pageOrder = (page, filename) => {
10561
10754
  if (page.meta.sidebar.order !== undefined) {
10562
10755
  return { order: page.meta.sidebar.order, orderIsAuthored: true };
10563
10756
  }
10564
- if (isIndexStem(filename.replace(extname(filename), ""))) {
10757
+ if (isIndexStem(stemOf(filename))) {
10565
10758
  return { order: Number.NEGATIVE_INFINITY, orderIsAuthored: false };
10566
10759
  }
10567
10760
  if (page.contentType === "changelog") {
@@ -10583,8 +10776,9 @@ var metaKey = (path, metaPrefix) => {
10583
10776
  }
10584
10777
  return path ? `${metaPrefix}/${path}` : metaPrefix;
10585
10778
  };
10586
- var applyFolderMeta = (group, folderMeta, sharedMeta, metaPrefix) => {
10587
- const meta = folderMeta.get(metaKey(group.path, metaPrefix)) ?? sharedMeta.get(group.path);
10779
+ var applyFolderMeta = (group, folderMeta, sharedMeta, metaPrefix, sharedMetaPrefix, indexDisplay) => {
10780
+ const meta = folderMeta.get(metaKey(group.path, metaPrefix)) ?? sharedMeta.get(metaKey(group.path, sharedMetaPrefix));
10781
+ group.display = indexDisplay.get(group.path) ?? meta?.display;
10588
10782
  if (meta) {
10589
10783
  group.label = meta.title ?? group.label;
10590
10784
  group.icon = meta.icon ?? group.icon;
@@ -10605,15 +10799,15 @@ var applyFolderMeta = (group, folderMeta, sharedMeta, metaPrefix) => {
10605
10799
  }
10606
10800
  for (const child of group.children) {
10607
10801
  if (child.kind === "group") {
10608
- applyFolderMeta(child, folderMeta, sharedMeta, metaPrefix);
10802
+ applyFolderMeta(child, folderMeta, sharedMeta, metaPrefix, sharedMetaPrefix, indexDisplay);
10609
10803
  }
10610
10804
  }
10611
10805
  };
10612
- var indexTitleMismatchDiagnostic = (page, folderPath, folderMeta, sharedMeta, metaPrefix) => {
10806
+ var indexTitleMismatchDiagnostic = (page, folderPath, folderMeta, sharedMeta, metaPrefix, sharedMetaPrefix) => {
10613
10807
  if (!page.meta.title || page.fallback || folderPath === "") {
10614
10808
  return;
10615
10809
  }
10616
- const meta = folderMeta.get(metaKey(folderPath, metaPrefix)) ?? sharedMeta.get(folderPath);
10810
+ const meta = folderMeta.get(metaKey(folderPath, metaPrefix)) ?? sharedMeta.get(metaKey(folderPath, sharedMetaPrefix));
10617
10811
  if (!meta?.title || meta.title === page.title) {
10618
10812
  return;
10619
10813
  }
@@ -10625,6 +10819,46 @@ var indexTitleMismatchDiagnostic = (page, folderPath, folderMeta, sharedMeta, me
10625
10819
  suggestion: `Update the page's frontmatter title to match ("${meta.title}"), or leave it if the divergence is intentional.`
10626
10820
  };
10627
10821
  };
10822
+ var sidebarDisplayIgnoredDiagnostics = (pages, explicitSidebar) => {
10823
+ const diagnostics = [];
10824
+ for (const page of pages) {
10825
+ if (!page.meta.sidebar.display) {
10826
+ continue;
10827
+ }
10828
+ const isIndex = isIndexStem(navStem(page.navPath));
10829
+ if (explicitSidebar) {
10830
+ diagnostics.push({
10831
+ code: "BLUME_SIDEBAR_DISPLAY_IGNORED",
10832
+ file: page.sourcePath ?? page.id,
10833
+ message: `"${page.navPath}" sets sidebar.display, but the explicit navigation.sidebar config owns each group's display mode — the value is ignored.`,
10834
+ severity: "warning",
10835
+ suggestion: "Set display on the matching group in navigation.sidebar, or remove the frontmatter key."
10836
+ });
10837
+ continue;
10838
+ }
10839
+ if (isIndex && !page.navPath.includes("/")) {
10840
+ diagnostics.push({
10841
+ code: "BLUME_SIDEBAR_DISPLAY_IGNORED",
10842
+ file: page.sourcePath ?? page.id,
10843
+ message: `"${page.navPath}" sets sidebar.display, but the content root is not a sidebar group — the value is ignored.`,
10844
+ severity: "warning",
10845
+ suggestion: "Set navigation.sidebar.display in blume.config to change the sidebar-wide mode."
10846
+ });
10847
+ continue;
10848
+ }
10849
+ if (isIndex) {
10850
+ continue;
10851
+ }
10852
+ diagnostics.push({
10853
+ code: "BLUME_SIDEBAR_DISPLAY_IGNORED",
10854
+ file: page.sourcePath ?? page.id,
10855
+ message: `"${page.navPath}" sets sidebar.display, but only a folder's index page can set its group's display mode — the value is ignored.`,
10856
+ severity: "warning",
10857
+ suggestion: "Move display to the folder's index page frontmatter, or set it in the folder's meta.ts."
10858
+ });
10859
+ }
10860
+ return diagnostics;
10861
+ };
10628
10862
  var isAuthoredOrder = (node) => node.kind === "group" || node.orderIsAuthored;
10629
10863
  var duplicateOrderDiagnostics = (nodes) => {
10630
10864
  const byOrder = new Map;
@@ -10671,25 +10905,25 @@ var sortNodes = (nodes, diagnostics) => {
10671
10905
  }
10672
10906
  }
10673
10907
  };
10674
- var hoistPages = (nodes, recurse) => {
10675
- const pages = nodes.filter((node) => node.kind === "page");
10908
+ var hoistPages = (nodes, display, hoist) => {
10676
10909
  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
- }
10910
+ if (hoist || groups.some((group) => (group.display ?? display) === "flat")) {
10911
+ const pages = nodes.filter((node) => node.kind === "page");
10912
+ nodes.splice(0, nodes.length, ...pages, ...groups);
10913
+ }
10914
+ for (const group of groups) {
10915
+ hoistPages(group.children, display, false);
10682
10916
  }
10683
10917
  };
10684
- var hoistTabSections = (nodes, tabPaths, recurse) => {
10918
+ var hoistTabSections = (nodes, tabPaths, display) => {
10685
10919
  for (const node of nodes) {
10686
10920
  if (node.kind !== "group") {
10687
10921
  continue;
10688
10922
  }
10689
10923
  if (node.routePath !== undefined && tabPaths.has(node.routePath)) {
10690
- hoistPages(node.children, recurse);
10924
+ hoistPages(node.children, display, true);
10691
10925
  }
10692
- hoistTabSections(node.children, tabPaths, recurse);
10926
+ hoistTabSections(node.children, tabPaths, display);
10693
10927
  }
10694
10928
  };
10695
10929
  var toNavNode = (node, display) => {
@@ -10708,25 +10942,29 @@ var toNavNode = (node, display) => {
10708
10942
  return {
10709
10943
  children: node.children.map((child) => toNavNode(child, display)),
10710
10944
  collapsed: node.collapsed,
10711
- display,
10945
+ display: node.display ?? display,
10712
10946
  icon: node.icon,
10713
10947
  kind: "group",
10714
10948
  label: node.label,
10715
10949
  path: node.routePath
10716
10950
  };
10717
10951
  };
10718
- var buildFileSystemSidebar = (pages, folderMeta, sharedMeta, metaPrefix, display, tabPaths, diagnostics = []) => {
10952
+ var buildFileSystemSidebar = (pages, folderMeta, sharedMeta, metaPrefix, sharedMetaPrefix, display, tabPaths, diagnostics = []) => {
10719
10953
  const root = createGroup("", "", "", 0);
10954
+ const indexDisplay = new Map;
10720
10955
  for (const page of pages) {
10721
10956
  const parts = page.navPath.split("/");
10722
10957
  const filename = parts.at(-1) ?? page.navPath;
10723
- const stem = filename.replace(extname(filename), "");
10958
+ const stem = stemOf(filename);
10724
10959
  const dirs = parts.slice(0, -1);
10725
10960
  if (isIndexStem(stem)) {
10726
- const diagnostic = indexTitleMismatchDiagnostic(page, dirs.join("/"), folderMeta, sharedMeta, metaPrefix);
10961
+ const diagnostic = indexTitleMismatchDiagnostic(page, dirs.join("/"), folderMeta, sharedMeta, metaPrefix, sharedMetaPrefix);
10727
10962
  if (diagnostic) {
10728
10963
  diagnostics.push(diagnostic);
10729
10964
  }
10965
+ if (page.meta.sidebar.display && !page.fallback) {
10966
+ indexDisplay.set(dirs.join("/"), page.meta.sidebar.display);
10967
+ }
10730
10968
  }
10731
10969
  if (page.meta.sidebar.hidden) {
10732
10970
  continue;
@@ -10760,10 +10998,10 @@ var buildFileSystemSidebar = (pages, folderMeta, sharedMeta, metaPrefix, display
10760
10998
  route: page.route
10761
10999
  });
10762
11000
  }
10763
- applyFolderMeta(root, folderMeta, sharedMeta, metaPrefix);
11001
+ applyFolderMeta(root, folderMeta, sharedMeta, metaPrefix, sharedMetaPrefix, indexDisplay);
10764
11002
  sortNodes(root.children, diagnostics);
10765
- hoistPages(root.children, display === "flat");
10766
- hoistTabSections(root.children, tabPaths, display === "flat");
11003
+ hoistPages(root.children, display, true);
11004
+ hoistTabSections(root.children, tabPaths, display);
10767
11005
  return root.children.map((child) => toNavNode(child, display));
10768
11006
  };
10769
11007
  var normalizeRef = (ref) => {
@@ -10782,8 +11020,9 @@ var routeForRef = (ref, byRoute, basePath) => {
10782
11020
  const normalized = normalizeRef(ref);
10783
11021
  return byRoute.get(normalized)?.route ?? withBasePath(basePath, normalized);
10784
11022
  };
11023
+ var isPageRef = (item) => typeof item === "string";
10785
11024
  var configItemToNode = (item, byRoute, basePath) => {
10786
- if (typeof item === "string") {
11025
+ if (isPageRef(item)) {
10787
11026
  const page = byRoute.get(normalizeRef(item));
10788
11027
  if (!page) {
10789
11028
  return null;
@@ -10826,7 +11065,7 @@ var configItemToNode = (item, byRoute, basePath) => {
10826
11065
  var buildConfigSidebar = (items, byRoute, display, basePath) => {
10827
11066
  const nodes = [];
10828
11067
  for (const item of items) {
10829
- if (typeof item !== "string" && item.items) {
11068
+ if (!isPageRef(item) && item.items) {
10830
11069
  nodes.push({
10831
11070
  badge: item.badge,
10832
11071
  children: buildConfigSidebar(item.items, byRoute, display, basePath),
@@ -10870,29 +11109,46 @@ var withTabHrefs = (tabs, sidebar) => tabs.map((tab) => {
10870
11109
  const href = tab.href ?? resolveTabHref(sidebar, tab.path);
10871
11110
  return href === tab.path ? tab : { ...tab, href };
10872
11111
  });
11112
+ var rebaseNavChrome = (basePath, options) => {
11113
+ const featured = options.featured ?? [];
11114
+ const selectors = options.selectors ?? [];
11115
+ const tabs = options.tabs ?? [];
11116
+ if (!basePath) {
11117
+ return { featured, selectors, tabs };
11118
+ }
11119
+ const rebasePath = (item) => ({
11120
+ ...item,
11121
+ path: withBasePath(basePath, item.path)
11122
+ });
11123
+ return {
11124
+ featured: featured.map((link) => ({
11125
+ ...link,
11126
+ href: withBasePath(basePath, link.href)
11127
+ })),
11128
+ selectors: selectors.map((selector) => ({
11129
+ ...selector,
11130
+ items: selector.items.map(rebasePath)
11131
+ })),
11132
+ tabs: tabs.map((tab) => {
11133
+ const rebased = {
11134
+ ...tab,
11135
+ items: tab.items?.map(rebasePath),
11136
+ path: withBasePath(basePath, tab.path)
11137
+ };
11138
+ if (tab.href) {
11139
+ rebased.href = withBasePath(basePath, tab.href);
11140
+ }
11141
+ return rebased;
11142
+ })
11143
+ };
11144
+ };
10873
11145
  var buildNavigation = (pages, options) => {
10874
11146
  const basePath = options.basePath ?? "";
10875
11147
  const display = options.display ?? "flat";
10876
11148
  const metaPrefix = options.metaPrefix ?? "";
11149
+ const sharedMetaPrefix = options.sharedMetaPrefix ?? "";
10877
11150
  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 ?? [];
11151
+ const { featured, selectors, tabs } = rebaseNavChrome(basePath, options);
10896
11152
  const byRoute = new Map(pages.map((page) => [
10897
11153
  options.refByLogical ? page.translationKey : page.route,
10898
11154
  page
@@ -10906,6 +11162,8 @@ var buildNavigation = (pages, options) => {
10906
11162
  }
10907
11163
  }
10908
11164
  const rootTabPath = withBasePath(basePath, options.localizedRoot ?? "/");
11165
+ const diagnostics = options.diagnostics ?? [];
11166
+ diagnostics.push(...sidebarDisplayIgnoredDiagnostics(pages, Boolean(options.sidebar)));
10909
11167
  if (options.sidebar) {
10910
11168
  const sidebar2 = buildConfigSidebar(options.sidebar, byRoute, display, basePath);
10911
11169
  return {
@@ -10916,7 +11174,7 @@ var buildNavigation = (pages, options) => {
10916
11174
  tabs: withTabHrefs(tabs, sidebar2)
10917
11175
  };
10918
11176
  }
10919
- const sidebar = buildFileSystemSidebar(pages, options.folderMeta, sharedFolderMeta, metaPrefix, display, new Set(tabs.flatMap((tab) => tab.path === rootTabPath ? [] : [tab.path])), options.diagnostics);
11177
+ const sidebar = buildFileSystemSidebar(pages, options.folderMeta, sharedFolderMeta, metaPrefix, sharedMetaPrefix, display, new Set(tabs.flatMap((tab) => tab.path === rootTabPath ? [] : [tab.path])), diagnostics);
10920
11178
  return {
10921
11179
  featured,
10922
11180
  root: rootTabPath,
@@ -10926,6 +11184,44 @@ var buildNavigation = (pages, options) => {
10926
11184
  };
10927
11185
  };
10928
11186
 
11187
+ // src/core/versions.ts
11188
+ var VERSION_LIKE = /^v\d/iu;
11189
+ var detectVersion = (parts, versions) => {
11190
+ const [first] = parts;
11191
+ if (first && versions.archived.some((version) => version.id === first)) {
11192
+ return { rest: parts.slice(1), version: first };
11193
+ }
11194
+ return { rest: parts, version: "" };
11195
+ };
11196
+ var detectVersionRef = (ref, versions) => {
11197
+ const { version, rest } = detectVersion(ref.split("/"), versions);
11198
+ return { rest: rest.join("/"), version };
11199
+ };
11200
+ var versionizeRoute = (logicalRoute, version) => {
11201
+ if (!version) {
11202
+ return logicalRoute;
11203
+ }
11204
+ return logicalRoute === "/" ? `/${version}` : `/${version}${logicalRoute}`;
11205
+ };
11206
+ var versionsDiagnostics = (pages, versions) => {
11207
+ const configured = new Set(versions.archived.map((version) => version.id));
11208
+ const seen = new Set;
11209
+ const diagnostics = [];
11210
+ for (const page of pages) {
11211
+ const [first] = page.source.ref.split("/");
11212
+ if (first && !seen.has(first) && VERSION_LIKE.test(first) && !configured.has(first)) {
11213
+ seen.add(first);
11214
+ diagnostics.push({
11215
+ code: "BLUME_VERSIONS_UNCONFIGURED_VERSION",
11216
+ message: `Folder "${first}/" looks like a version, but "${first}" is not in versions.archived — its pages are treated as current-version content at /${first}/….`,
11217
+ severity: "warning",
11218
+ suggestion: `Add { id: "${first}" } to versions.archived, or rename the folder if it isn't a snapshot.`
11219
+ });
11220
+ }
11221
+ }
11222
+ return diagnostics;
11223
+ };
11224
+
10929
11225
  // src/core/graph.ts
10930
11226
  var collectRoutes = (pages) => {
10931
11227
  const routes = new Map;
@@ -10964,8 +11260,9 @@ var localePagesFor = (code, real, fallback, fallbackByKey, i18n, basePath) => {
10964
11260
  }
10965
11261
  return [...real, ...filled];
10966
11262
  };
11263
+ var isSingleLabel = (label) => typeof label === "string";
10967
11264
  var resolveLabel = (label, locale, defaultLocale) => {
10968
- if (typeof label === "string") {
11265
+ if (isSingleLabel(label)) {
10969
11266
  return label;
10970
11267
  }
10971
11268
  return label[locale] ?? (defaultLocale === undefined ? undefined : label[defaultLocale]) ?? Object.values(label)[0] ?? "";
@@ -10978,19 +11275,25 @@ var resolveTabLabels = (tabs, locale, defaultLocale) => (tabs ?? []).map((tab) =
10978
11275
  })),
10979
11276
  label: resolveLabel(tab.label, locale, defaultLocale)
10980
11277
  }));
10981
- var buildLocaleNavigation = (code, pages, fallback, fallbackByKey, options, i18n, diagnostics) => {
11278
+ var buildLocaleNavigation = (code, pages, fallback, fallbackByKey, options, i18n, diagnostics, version = "") => {
10982
11279
  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
- }));
11280
+ const tabs = resolveTabLabels(options.navigation.tabs, code, i18n.defaultLocale).map((tab) => {
11281
+ const localized = {
11282
+ ...tab,
11283
+ items: tab.items?.map((item) => ({
11284
+ ...item,
11285
+ path: localizePath(item.path)
11286
+ })),
11287
+ path: localizePath(tab.path)
11288
+ };
11289
+ if (localized.href) {
11290
+ localized.href = localizePath(localized.href);
11291
+ }
11292
+ return localized;
11293
+ });
10992
11294
  const real = pages.filter((page) => page.locale === code);
10993
11295
  const localePages = localePagesFor(code, real, fallback, fallbackByKey, i18n, options.basePath ?? "");
11296
+ const localeDir = i18n.parser === "dir" && code !== i18n.defaultLocale ? code : "";
10994
11297
  return buildNavigation(localePages, {
10995
11298
  basePath: options.basePath ?? "",
10996
11299
  diagnostics,
@@ -11000,16 +11303,17 @@ var buildLocaleNavigation = (code, pages, fallback, fallbackByKey, options, i18n
11000
11303
  href: localizePath(link.href)
11001
11304
  })),
11002
11305
  folderMeta: options.folderMeta,
11003
- localizedRoot: localizeRoute("/", code, i18n),
11004
- metaPrefix: i18n.parser === "dir" && code !== i18n.defaultLocale ? code : "",
11306
+ localizedRoot: localizeRoute(versionizeRoute("/", version), code, i18n),
11307
+ metaPrefix: [version, localeDir].filter(Boolean).join("/"),
11005
11308
  refByLogical: true,
11006
11309
  selectors: options.navigation.selectors,
11007
11310
  sharedFolderMeta: options.sharedFolderMeta,
11008
- sidebar: options.navigation.sidebar.items,
11311
+ sharedMetaPrefix: version,
11312
+ sidebar: version ? undefined : options.navigation.sidebar.items,
11009
11313
  tabs
11010
11314
  });
11011
11315
  };
11012
- var buildI18nNavigation = (pages, options, i18n, diagnostics) => {
11316
+ var buildI18nNavigation = (pages, options, i18n, diagnostics, version = "") => {
11013
11317
  const fallback = resolveFallbackLocale(i18n);
11014
11318
  const fallbackByKey = new Map;
11015
11319
  if (fallback) {
@@ -11023,7 +11327,7 @@ var buildI18nNavigation = (pages, options, i18n, diagnostics) => {
11023
11327
  const seen = new Set;
11024
11328
  for (const { code } of i18n.locales) {
11025
11329
  const localeDiagnostics = [];
11026
- navigationByLocale[code] = buildLocaleNavigation(code, pages, fallback, fallbackByKey, options, i18n, localeDiagnostics);
11330
+ navigationByLocale[code] = buildLocaleNavigation(code, pages, fallback, fallbackByKey, options, i18n, localeDiagnostics, version);
11027
11331
  for (const diagnostic of localeDiagnostics) {
11028
11332
  const key = `${diagnostic.code}
11029
11333
  ${diagnostic.file ?? ""}
@@ -11042,11 +11346,37 @@ ${diagnostic.message}`;
11042
11346
  };
11043
11347
  return { navigation, navigationByLocale };
11044
11348
  };
11349
+ var buildVersionNavigation = (id2, versionPages, options, diagnostics) => {
11350
+ const { i18n } = options;
11351
+ if (i18n) {
11352
+ return buildI18nNavigation(versionPages, options, i18n, diagnostics, id2).navigationByLocale;
11353
+ }
11354
+ return {
11355
+ "": buildNavigation(versionPages, {
11356
+ basePath: options.basePath ?? "",
11357
+ diagnostics,
11358
+ display: options.navigation.sidebar.display,
11359
+ featured: options.navigation.featured,
11360
+ folderMeta: options.folderMeta,
11361
+ localizedRoot: versionizeRoute("/", id2),
11362
+ metaPrefix: id2,
11363
+ selectors: options.navigation.selectors,
11364
+ sharedFolderMeta: options.sharedFolderMeta,
11365
+ sharedMetaPrefix: id2,
11366
+ tabs: resolveTabLabels(options.navigation.tabs, "")
11367
+ })
11368
+ };
11369
+ };
11045
11370
  var buildContentGraph = (pages, options) => {
11046
11371
  const { diagnostics, routes } = collectRoutes(pages);
11047
11372
  const { i18n } = options;
11048
- const { navigation, navigationByLocale } = i18n ? buildI18nNavigation(pages, options, i18n, diagnostics) : {
11049
- navigation: buildNavigation(pages, {
11373
+ const currentPages = options.versions ? pages.filter((page) => page.version === "") : pages;
11374
+ let navigationByLocale = {};
11375
+ let navigation;
11376
+ if (i18n) {
11377
+ ({ navigation, navigationByLocale } = buildI18nNavigation(currentPages, options, i18n, diagnostics));
11378
+ } else {
11379
+ navigation = buildNavigation(currentPages, {
11050
11380
  basePath: options.basePath ?? "",
11051
11381
  diagnostics,
11052
11382
  display: options.navigation.sidebar.display,
@@ -11056,14 +11386,18 @@ var buildContentGraph = (pages, options) => {
11056
11386
  sharedFolderMeta: options.sharedFolderMeta,
11057
11387
  sidebar: options.navigation.sidebar.items,
11058
11388
  tabs: resolveTabLabels(options.navigation.tabs, "")
11059
- }),
11060
- navigationByLocale: {}
11061
- };
11062
- diagnostics.push(...validateNavIcons(navigation), ...validateNavStructure(navigation, pages));
11389
+ });
11390
+ }
11391
+ const navigationByVersion = {};
11392
+ for (const { id: id2 } of options.versions?.archived ?? []) {
11393
+ navigationByVersion[id2] = buildVersionNavigation(id2, pages.filter((page) => page.version === id2), options, diagnostics);
11394
+ }
11395
+ diagnostics.push(...validateNavIcons(navigation), ...validateNavStructure(navigation, currentPages));
11063
11396
  return {
11064
11397
  diagnostics,
11065
11398
  navigation,
11066
11399
  navigationByLocale,
11400
+ navigationByVersion,
11067
11401
  pages,
11068
11402
  routes
11069
11403
  };
@@ -11126,9 +11460,20 @@ var gitLastModifiedTimes = (root, contentRoots, sourcePaths) => {
11126
11460
  };
11127
11461
 
11128
11462
  // src/core/manifest.ts
11463
+ var versionAlternateKey = (versionKey, locale) => `${versionKey}\x00${locale}`;
11464
+ var versionAlternatesFor = (byKey, versionKey, locale) => {
11465
+ const key = versionAlternateKey(versionKey, locale);
11466
+ const existing = byKey.get(key);
11467
+ if (existing) {
11468
+ return existing;
11469
+ }
11470
+ const list2 = [];
11471
+ byKey.set(key, list2);
11472
+ return list2;
11473
+ };
11129
11474
  var MANIFEST_VERSION = 1;
11130
11475
  var contentIndexable = (page, config) => !page.meta.search.exclude && (!page.meta.sidebar.hidden || config.search.indexing.includeHiddenPages);
11131
- var buildFallbackRoutes = (graph, i18n, alternatesByKey, basePath) => {
11476
+ var buildFallbackRoutes = (graph, i18n, alternatesByKey, basePath, versionAlternatesByKey) => {
11132
11477
  const fallback = resolveFallbackLocale(i18n);
11133
11478
  if (!fallback) {
11134
11479
  return [];
@@ -11144,6 +11489,11 @@ var buildFallbackRoutes = (graph, i18n, alternatesByKey, basePath) => {
11144
11489
  if (present.has(key)) {
11145
11490
  continue;
11146
11491
  }
11492
+ const path = withBasePath(basePath, localizeRoute(key, code, i18n));
11493
+ const versionAlternates = versionAlternatesByKey ? versionAlternatesFor(versionAlternatesByKey, source.versionKey, code) : [];
11494
+ if (versionAlternatesByKey) {
11495
+ versionAlternates.push({ path, version: source.version });
11496
+ }
11147
11497
  routes.push({
11148
11498
  alternates: alternatesByKey.get(key) ?? [],
11149
11499
  collection: source.collection ?? "docs",
@@ -11157,10 +11507,12 @@ var buildFallbackRoutes = (graph, i18n, alternatesByKey, basePath) => {
11157
11507
  indexable: false,
11158
11508
  lastModified: source.lastModified,
11159
11509
  locale: code,
11160
- path: withBasePath(basePath, localizeRoute(key, code, i18n)),
11510
+ path,
11161
11511
  source: source.source,
11162
11512
  sourcePath: source.sourcePath,
11163
- title: source.title
11513
+ title: source.title,
11514
+ version: source.version,
11515
+ versionAlternates
11164
11516
  });
11165
11517
  }
11166
11518
  }
@@ -11178,6 +11530,12 @@ var buildManifest = (options) => {
11178
11530
  alternatesByKey.set(page.translationKey, list2);
11179
11531
  }
11180
11532
  }
11533
+ const versionAlternatesByKey = config.versions ? new Map : undefined;
11534
+ if (versionAlternatesByKey) {
11535
+ for (const page of graph.pages) {
11536
+ versionAlternatesFor(versionAlternatesByKey, page.versionKey, page.locale).push({ path: page.route, version: page.version });
11537
+ }
11538
+ }
11181
11539
  const routes = graph.pages.map((page) => ({
11182
11540
  alternates: alternatesByKey.get(page.translationKey) ?? [],
11183
11541
  collection: page.collection ?? "docs",
@@ -11193,10 +11551,18 @@ var buildManifest = (options) => {
11193
11551
  path: page.route,
11194
11552
  source: page.source,
11195
11553
  sourcePath: page.sourcePath,
11196
- title: page.title
11554
+ title: page.title,
11555
+ version: page.version,
11556
+ versionAlternates: versionAlternatesByKey ? versionAlternatesFor(versionAlternatesByKey, page.versionKey, page.locale) : []
11197
11557
  }));
11198
11558
  if (i18n) {
11199
- routes.push(...buildFallbackRoutes(graph, i18n, alternatesByKey, config.basePath));
11559
+ routes.push(...buildFallbackRoutes(graph, i18n, alternatesByKey, config.basePath, versionAlternatesByKey));
11560
+ }
11561
+ if (versionAlternatesByKey && config.versions) {
11562
+ const rank = new Map(config.versions.archived.map((version, index) => [version.id, index + 1]));
11563
+ for (const list2 of versionAlternatesByKey.values()) {
11564
+ list2.sort((a, b) => (rank.get(a.version) ?? 0) - (rank.get(b.version) ?? 0));
11565
+ }
11200
11566
  }
11201
11567
  routes.sort((a, b) => a.path.localeCompare(b.path));
11202
11568
  return {
@@ -11220,7 +11586,8 @@ var META_FILES = [
11220
11586
  "**/meta.$.js",
11221
11587
  "**/meta.$.mjs"
11222
11588
  ];
11223
- var resolveMeta = async (mod) => typeof mod === "function" ? await mod() : mod;
11589
+ var isMetaFactory = (mod) => typeof mod === "function";
11590
+ var resolveMeta = async (mod) => isMetaFactory(mod) ? await mod() : mod;
11224
11591
  var metaKeyFor = (prefix, dir) => {
11225
11592
  const clean = prefix ? prefix.replaceAll(/^\/+|\/+$/gu, "") : "";
11226
11593
  if (!clean) {
@@ -11229,8 +11596,9 @@ var metaKeyFor = (prefix, dir) => {
11229
11596
  return dir ? `${clean}/${dir}` : clean;
11230
11597
  };
11231
11598
  var discoverFolderMeta = async (sources, options = {}) => {
11232
- const list2 = typeof sources === "string" ? [{ root: sources }] : sources;
11599
+ const list2 = Array.isArray(sources) ? sources : [{ root: sources }];
11233
11600
  const localeDirs = new Set(options.localeDirs);
11601
+ const versionDirs = new Set(options.versionDirs);
11234
11602
  const load2 = createModuleLoader();
11235
11603
  const meta = new Map;
11236
11604
  const shared = new Map;
@@ -11259,7 +11627,12 @@ var discoverFolderMeta = async (sources, options = {}) => {
11259
11627
  for (const entry of loaded) {
11260
11628
  const dir = relative8(source.root, dirname6(entry.file));
11261
11629
  const [head, ...tail] = dir.split("/");
11262
- const key = head && localeDirs.has(head) ? `${head}/${metaKeyFor(source.prefix, tail.join("/"))}`.replace(/\/$/u, "") : metaKeyFor(source.prefix, dir);
11630
+ const version = head && versionDirs.has(head) ? head : "";
11631
+ const afterVersion = version ? tail : [head ?? "", ...tail];
11632
+ const [localeHead, ...localeTail] = afterVersion;
11633
+ const locale = localeHead && localeDirs.has(localeHead) ? localeHead : "";
11634
+ const rest = (locale ? localeTail : afterVersion).filter(Boolean).join("/");
11635
+ const key = [version, locale, metaKeyFor(source.prefix, rest)].filter(Boolean).join("/");
11263
11636
  if (!entry.ok) {
11264
11637
  diagnostics.push({
11265
11638
  code: "BLUME_META_LOAD_FAILED",
@@ -11331,7 +11704,7 @@ var CODE_FENCE = /^(?<delimiter>```|~~~)/u;
11331
11704
  var nextFenceState = (line, fence) => {
11332
11705
  const trimmed = line.trimStart();
11333
11706
  const delimiter = trimmed.match(CODE_FENCE)?.groups?.delimiter;
11334
- if (delimiter === undefined) {
11707
+ if (delimiter !== "```" && delimiter !== "~~~") {
11335
11708
  return fence;
11336
11709
  }
11337
11710
  if (fence === null) {
@@ -11527,13 +11900,16 @@ var deriveTitle = (meta, headings, id2) => {
11527
11900
  return titleCase(stripNumericPrefix(base.replace(extname2(base), "")));
11528
11901
  };
11529
11902
  var trimSlashes = (value) => value.replaceAll(/^\/+|\/+$/gu, "");
11903
+ var isStringValue = (value) => typeof value === "string";
11530
11904
  var withPrefix = (prefix, path) => {
11531
11905
  const clean = prefix ? trimSlashes(prefix) : "";
11532
11906
  return clean ? `${clean}/${path}` : path;
11533
11907
  };
11908
+ var isKeyCarrier = (segment) => typeof segment === "object" && segment !== null;
11909
+ var isSymbolKey = (key) => typeof key === "symbol";
11534
11910
  var segmentKey2 = (segment) => {
11535
- const key = typeof segment === "object" && segment !== null ? segment.key : segment;
11536
- return typeof key === "symbol" ? String(key) : key;
11911
+ const key = isKeyCarrier(segment) ? segment.key : segment;
11912
+ return isSymbolKey(key) ? String(key) : key;
11537
11913
  };
11538
11914
  var validateCustomKeys = (data, extend) => {
11539
11915
  const custom = {};
@@ -11564,7 +11940,7 @@ var validateCustomKeys = (data, extend) => {
11564
11940
  };
11565
11941
  };
11566
11942
  var parseEntryMeta = (entry, ctx) => {
11567
- const entryType = typeof entry.data.type === "string" ? entry.data.type : ctx.defaultType;
11943
+ const entryType = isStringValue(entry.data.type) ? entry.data.type : ctx.defaultType;
11568
11944
  const typeExtend = ctx.typeFrontmatter?.[entryType];
11569
11945
  const extend = ctx.frontmatterExtend || typeExtend ? { ...ctx.frontmatterExtend, ...typeExtend } : undefined;
11570
11946
  const known = extend ? Object.fromEntries(Object.entries(entry.data).filter(([key]) => !Object.hasOwn(extend, key))) : entry.data;
@@ -11600,13 +11976,16 @@ var normalizeEntry = (entry, ctx) => {
11600
11976
  if (meta.noindex) {
11601
11977
  meta.seo.noindex = true;
11602
11978
  }
11979
+ const { versions } = ctx;
11980
+ const { version, rest: versionlessRef } = versions ? detectVersionRef(entry.ref, versions) : { rest: entry.ref, version: "" };
11603
11981
  const { i18n } = ctx;
11604
- const { navPath: rawNavPath, locales } = i18n ? localePlacement(entry.ref, ext, i18n) : { locales: [""], navPath: entry.ref };
11982
+ const { navPath: rawNavPath, locales } = i18n ? localePlacement(versionlessRef, ext, i18n) : { locales: [""], navPath: versionlessRef };
11605
11983
  const navPath = withPrefix(ctx.source.prefix, rawNavPath);
11606
11984
  const slugInput = meta.slug ?? entry.slug;
11607
11985
  const slug = slugInput ? trimSlashes(slugInput) : "";
11608
11986
  const routeInput = withPrefix(ctx.source.prefix, slug ? `${slug}${ext}` : rawNavPath);
11609
- const { segments, groups, route: logicalRoute } = mapRoute(routeInput);
11987
+ const { segments, groups, route: versionKey } = mapRoute(routeInput);
11988
+ const logicalRoute = versionizeRoute(versionKey, version);
11610
11989
  const headings = extractHeadings(entry.body.text);
11611
11990
  const { staged } = ctx.source;
11612
11991
  const base = {
@@ -11630,7 +12009,9 @@ var normalizeEntry = (entry, ctx) => {
11630
12009
  source: { name: ctx.source.name, ref: entry.ref },
11631
12010
  sourcePath: entry.sourcePath,
11632
12011
  title: deriveTitle(meta, headings, navPath),
11633
- translationKey: logicalRoute
12012
+ translationKey: logicalRoute,
12013
+ version,
12014
+ versionKey
11634
12015
  };
11635
12016
  const pages = locales.map((locale) => ({
11636
12017
  ...base,
@@ -11740,6 +12121,7 @@ var operationKey = (method, path, operationId) => {
11740
12121
  return fromId || slugify(`${method}-${path}`);
11741
12122
  };
11742
12123
  var isOperation = (value) => typeof value === "object" && value !== null;
12124
+ var hasTagName = (tag) => typeof tag.name === "string";
11743
12125
  var tagSlugger = () => {
11744
12126
  const assigned = new Map;
11745
12127
  const taken = new Set;
@@ -11758,16 +12140,45 @@ var tagSlugger = () => {
11758
12140
  return slug;
11759
12141
  };
11760
12142
  };
11761
- var extractOperations = (document, baseRoute) => {
12143
+ var operationCollector = (baseRoute, tagMeta) => {
11762
12144
  const operations = [];
11763
12145
  const tagOrder = [];
11764
12146
  const tagsSeen = new Set;
11765
- const tagMeta = new Map((document.tags ?? []).map((tag) => [tag.name, tag.description ?? ""]));
11766
12147
  const seen = new Set;
11767
- const warnings = [];
11768
12148
  const slugForTag = tagSlugger();
11769
- for (const [path, rawItem] of Object.entries(document.paths ?? {})) {
11770
- const item = rawItem;
12149
+ const add2 = (entry) => {
12150
+ const tagSlug = slugForTag(entry.tag);
12151
+ if (!tagsSeen.has(entry.tag)) {
12152
+ tagsSeen.add(entry.tag);
12153
+ tagOrder.push(entry.tag);
12154
+ }
12155
+ let { key } = entry;
12156
+ while (seen.has(key)) {
12157
+ key = `${key}-${entry.method}`;
12158
+ }
12159
+ seen.add(key);
12160
+ operations.push({
12161
+ ...entry,
12162
+ key,
12163
+ route: `${baseRoute === "/" ? "" : baseRoute}/${tagSlug}/${key}`,
12164
+ tagSlug
12165
+ });
12166
+ };
12167
+ const finish = () => ({
12168
+ operations,
12169
+ tags: tagOrder.map((name) => ({
12170
+ description: tagMeta.get(name) ?? "",
12171
+ name,
12172
+ slug: slugForTag(name)
12173
+ }))
12174
+ });
12175
+ return { add: add2, finish };
12176
+ };
12177
+ var extractOperations = (document, baseRoute) => {
12178
+ const warnings = [];
12179
+ const tagMeta = new Map((document.tags ?? []).filter(hasTagName).map((tag) => [tag.name, tag.description ?? ""]));
12180
+ const collector = operationCollector(baseRoute, tagMeta);
12181
+ for (const [path, item] of Object.entries(document.paths ?? {})) {
11771
12182
  if (!item) {
11772
12183
  continue;
11773
12184
  }
@@ -11780,42 +12191,171 @@ var extractOperations = (document, baseRoute) => {
11780
12191
  if (!isOperation(operation)) {
11781
12192
  continue;
11782
12193
  }
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({
12194
+ collector.add({
11795
12195
  deprecated: operation.deprecated ?? false,
11796
12196
  description: operation.description ?? "",
11797
- key,
12197
+ key: operationKey(method, path, operation.operationId),
11798
12198
  method,
11799
12199
  operationId: operation.operationId,
11800
12200
  path,
11801
- route: `${baseRoute === "/" ? "" : baseRoute}/${tagSlug}/${key}`,
11802
12201
  summary: operation.summary ?? "",
11803
- tag,
11804
- tagSlug
12202
+ tag: operation.tags?.[0] ?? UNTAGGED
11805
12203
  });
11806
12204
  }
11807
12205
  }
11808
- const tags = tagOrder.map((name) => ({
11809
- description: tagMeta.get(name) ?? "",
11810
- name,
11811
- slug: slugForTag(name)
11812
- }));
11813
- return { operations, tags, warnings };
12206
+ return { ...collector.finish(), warnings };
12207
+ };
12208
+
12209
+ // src/openapi/asyncapi.ts
12210
+ var ASYNCAPI_ACTIONS = ["send", "receive"];
12211
+ var unescapePointer = (token) => token.replaceAll("~1", "/").replaceAll("~0", "~");
12212
+ var CHANNEL_REF = /^#\/channels\/(?<id>.+)$/u;
12213
+ var isString3 = (value) => typeof value === "string";
12214
+ var channelIdOf = (channel) => {
12215
+ const id2 = CHANNEL_REF.exec(channel?.$ref ?? "")?.groups?.id;
12216
+ return id2 === undefined ? undefined : unescapePointer(id2);
12217
+ };
12218
+ var channelAddress = (channelId, channel) => {
12219
+ const address = channel?.address;
12220
+ return isString3(address) && address !== "" ? address : channelId;
12221
+ };
12222
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
12223
+ var COMPONENT_SECTION_REF = /^#\/components\/(?<section>channels|operations)\/(?<name>[^/]+)$/u;
12224
+ var inlineComponentRefs = (document) => {
12225
+ const maps = [
12226
+ [document.channels, "channels"],
12227
+ [document.operations, "operations"]
12228
+ ];
12229
+ for (const [map, section] of maps) {
12230
+ if (!isObject(map)) {
12231
+ continue;
12232
+ }
12233
+ const table = document.components?.[section];
12234
+ for (const [id2, entry] of Object.entries(map)) {
12235
+ if (!isObject(entry) || !isString3(entry.$ref)) {
12236
+ continue;
12237
+ }
12238
+ const groups = COMPONENT_SECTION_REF.exec(entry.$ref)?.groups;
12239
+ if (groups?.section !== section) {
12240
+ continue;
12241
+ }
12242
+ const resolved = isObject(table) ? table[unescapePointer(groups.name ?? "")] : undefined;
12243
+ if (isObject(resolved) && !isString3(resolved.$ref)) {
12244
+ map[id2] = resolved;
12245
+ }
12246
+ }
12247
+ }
12248
+ };
12249
+ var TRAIT_REF = /^#\/components\/(?<section>operationTraits|messageTraits)\/(?<name>[^/]+)$/u;
12250
+ var applyAsyncApiTraits = (document) => {
12251
+ const resolveTrait = (trait) => {
12252
+ if (!isObject(trait)) {
12253
+ return;
12254
+ }
12255
+ if (!isString3(trait.$ref)) {
12256
+ return trait;
12257
+ }
12258
+ const groups = TRAIT_REF.exec(trait.$ref)?.groups;
12259
+ const section = groups?.section ? document.components?.[groups.section] : undefined;
12260
+ const resolved = section?.[groups?.name ?? ""];
12261
+ return isObject(resolved) ? resolved : undefined;
12262
+ };
12263
+ const mergeTraits = (node) => {
12264
+ const { traits } = node;
12265
+ if (!Array.isArray(traits)) {
12266
+ return;
12267
+ }
12268
+ delete node.traits;
12269
+ const merged = {};
12270
+ for (const trait of traits) {
12271
+ Object.assign(merged, resolveTrait(trait));
12272
+ }
12273
+ for (const [key, value] of Object.entries(merged)) {
12274
+ if (!(key in node)) {
12275
+ node[key] = value;
12276
+ }
12277
+ }
12278
+ };
12279
+ for (const operation of Object.values(document.operations ?? {})) {
12280
+ if (isObject(operation)) {
12281
+ mergeTraits(operation);
12282
+ }
12283
+ }
12284
+ const channelMaps = [
12285
+ ...Object.values(document.channels ?? {}),
12286
+ ...Object.values(document.components?.channels ?? {})
12287
+ ];
12288
+ const messageMaps = [
12289
+ ...channelMaps.map((channel) => isObject(channel) ? channel.messages : undefined),
12290
+ document.components?.messages
12291
+ ];
12292
+ for (const messages of messageMaps) {
12293
+ if (!isObject(messages)) {
12294
+ continue;
12295
+ }
12296
+ for (const message of Object.values(messages)) {
12297
+ if (isObject(message) && !isString3(message.$ref)) {
12298
+ mergeTraits(message);
12299
+ }
12300
+ }
12301
+ }
12302
+ return document;
12303
+ };
12304
+ var normalizeAsyncApiDocument = (document) => {
12305
+ inlineComponentRefs(document);
12306
+ return applyAsyncApiTraits(document);
12307
+ };
12308
+ var operationSite = (id2, operation, channels) => {
12309
+ const action = ASYNCAPI_ACTIONS.find((a) => a === operation.action);
12310
+ if (!action) {
12311
+ return `Operation "${id2}" declares no send/receive action; it is missing from the reference.`;
12312
+ }
12313
+ const channelId = channelIdOf(operation.channel);
12314
+ const channel = channelId === undefined ? undefined : channels[channelId];
12315
+ if (channelId === undefined || !isObject(channel) || isString3(channel.$ref)) {
12316
+ return `Operation "${id2}" references a channel that isn't declared under "channels"; it is missing from the reference.`;
12317
+ }
12318
+ return { action, address: channelAddress(channelId, channel), channelId };
12319
+ };
12320
+ var extractAsyncApiOperations = (document, baseRoute) => {
12321
+ const warnings = [];
12322
+ const tagMeta = new Map((document.info?.tags ?? []).filter((tag) => typeof tag?.name === "string").map((tag) => [tag.name, tag.description ?? ""]));
12323
+ const collector = operationCollector(baseRoute, tagMeta);
12324
+ const channels = document.channels ?? {};
12325
+ for (const [id2, operation] of Object.entries(document.operations ?? {})) {
12326
+ if (!isObject(operation)) {
12327
+ continue;
12328
+ }
12329
+ if (isString3(operation.$ref)) {
12330
+ warnings.push(`Operation "${id2}" is a reference that doesn't resolve to a components operation; it is missing from the reference.`);
12331
+ continue;
12332
+ }
12333
+ const site = operationSite(id2, operation, channels);
12334
+ if (isString3(site)) {
12335
+ warnings.push(site);
12336
+ continue;
12337
+ }
12338
+ const { action, address, channelId } = site;
12339
+ const tag = operation.tags?.find((candidate) => typeof candidate?.name === "string")?.name;
12340
+ collector.add({
12341
+ channelId,
12342
+ deprecated: operation.deprecated === true,
12343
+ description: operation.description ?? "",
12344
+ key: operationKey(action, address, id2),
12345
+ method: action,
12346
+ operationId: id2,
12347
+ path: address,
12348
+ summary: operation.title ?? operation.summary ?? "",
12349
+ tag: tag ?? address
12350
+ });
12351
+ }
12352
+ return { ...collector.finish(), warnings };
11814
12353
  };
11815
12354
 
11816
12355
  // src/openapi/parse.ts
11817
12356
  import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
11818
12357
  import { setTimeout as sleep } from "node:timers/promises";
12358
+ import { convert } from "@asyncapi/converter";
11819
12359
  import { normalize as normalize2, upgrade } from "@scalar/openapi-parser";
11820
12360
  import pRetry, { AbortError } from "p-retry";
11821
12361
  import { isAbsolute as isAbsolute3, join as join12 } from "pathe";
@@ -11969,27 +12509,74 @@ var readSpecText = async (spec, root, options) => {
11969
12509
  throw error;
11970
12510
  }
11971
12511
  };
12512
+ var isApiDocument = (value) => typeof value === "object" && value !== null;
11972
12513
  var parseSpec = async (spec, root, options = {}) => {
11973
12514
  const { text, warnings } = await readSpecText(spec, root, options);
11974
12515
  const normalized = normalize2(text);
11975
12516
  const { specification } = upgrade(normalized);
11976
- if (specification === null || typeof specification !== "object") {
12517
+ if (!isApiDocument(specification)) {
11977
12518
  throw new InvalidSpecError(`${spec} is not a valid OpenAPI document (expected a YAML or JSON object).`);
11978
12519
  }
11979
12520
  return { document: specification, warnings };
11980
12521
  };
12522
+ var isAsyncApiDocument = (value) => typeof value === "object" && value !== null && ("asyncapi" in value) && typeof value.asyncapi === "string" && value.asyncapi !== "";
12523
+ var parseAsyncApiSpec = async (spec, root, options = {}) => {
12524
+ const { text, warnings } = await readSpecText(spec, root, options);
12525
+ const normalized = normalize2(text);
12526
+ if (!isAsyncApiDocument(normalized)) {
12527
+ throw new InvalidSpecError(`${spec} is not a valid AsyncAPI document (expected a YAML or JSON object with an \`asyncapi\` version field).`);
12528
+ }
12529
+ const version = normalized.asyncapi;
12530
+ let document = normalized;
12531
+ if (!version.startsWith("3.")) {
12532
+ const captured = [];
12533
+ const originalWarn = console.warn;
12534
+ console.warn = (...args) => {
12535
+ captured.push(args.map(String).join(" "));
12536
+ };
12537
+ try {
12538
+ document = convert(document, "3.0.0");
12539
+ } catch (error) {
12540
+ throw new InvalidSpecError(`${spec} could not be converted to AsyncAPI 3.0 (${error.message}).`);
12541
+ } finally {
12542
+ console.warn = originalWarn;
12543
+ }
12544
+ warnings.push(...captured.map((message) => `Converting ${spec} to AsyncAPI 3.0: ${message}`));
12545
+ }
12546
+ return { document: normalizeAsyncApiDocument(document), warnings };
12547
+ };
11981
12548
 
11982
12549
  // src/openapi/render-mdx.ts
11983
12550
  import { fromMarkdown as fromMarkdown2 } from "mdast-util-from-markdown";
11984
12551
  import { toString as mdastToString } from "mdast-util-to-string";
11985
- var MDX_UNSAFE = /[<{}]/gu;
11986
- var ENTITIES = {
11987
- "<": "&lt;",
11988
- "{": "&#123;",
11989
- "}": "&#125;"
12552
+ import stringWidth3 from "string-width";
12553
+
12554
+ // src/core/text-width.ts
12555
+ import stringWidth2 from "string-width";
12556
+ var columnsPrefix = (text, max) => {
12557
+ let end = 0;
12558
+ let used = 0;
12559
+ const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
12560
+ for (const { index, segment } of graphemes.segment(text)) {
12561
+ const width = stringWidth2(segment);
12562
+ if (used + width > max) {
12563
+ break;
12564
+ }
12565
+ used += width;
12566
+ end = index + segment.length;
12567
+ }
12568
+ return text.slice(0, end);
11990
12569
  };
12570
+
12571
+ // src/openapi/render-mdx.ts
12572
+ var MDX_UNSAFE = /[<{}]/gu;
12573
+ var ENTITIES = new Map([
12574
+ ["<", "&lt;"],
12575
+ ["{", "&#123;"],
12576
+ ["}", "&#125;"]
12577
+ ]);
11991
12578
  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)}`);
12579
+ var escapeProse = (text) => text.replace(MDX_UNSAFE, (char) => ENTITIES.get(char) ?? char).replace(MDX_ESM_KEYWORD, (keyword) => `&#${keyword.codePointAt(0)};${keyword.slice(1)}`);
11993
12580
  var codeSpans = (text) => {
11994
12581
  const spans = [];
11995
12582
  const collect = (node) => {
@@ -12022,29 +12609,28 @@ var mdxSafe = (text) => {
12022
12609
  return out + escapeProse(text.slice(cursor));
12023
12610
  };
12024
12611
  var META_DESCRIPTION_MAX = 160;
12025
- var WHITESPACE = /\s+/gu;
12612
+ var WHITESPACE2 = /\s+/gu;
12026
12613
  var TRAILING_WORD = /\s+\S*$/u;
12027
12614
  var plainProse = (markdown) => {
12028
12615
  const tree = fromMarkdown2(markdown);
12029
12616
  const first = tree.children.find((node) => node.type === "paragraph") ?? tree.children[0];
12030
- return first ? mdastToString(first).replace(WHITESPACE, " ").trim() : "";
12617
+ return first ? mdastToString(first).replace(WHITESPACE2, " ").trim() : "";
12031
12618
  };
12032
12619
  var clip = (text, max) => {
12033
12620
  if (max <= 0) {
12034
12621
  return "";
12035
12622
  }
12036
- if (text.length <= max) {
12623
+ if (stringWidth3(text) <= max) {
12037
12624
  return text;
12038
12625
  }
12039
- const head = text.slice(0, max - 1);
12626
+ const head = columnsPrefix(text, max - 1);
12040
12627
  const onWordBoundary = head.replace(TRAILING_WORD, "");
12041
- return `${onWordBoundary.length >= max / 2 ? onWordBoundary : head}…`;
12628
+ return `${stringWidth3(onWordBoundary) >= max / 2 ? onWordBoundary : head}…`;
12042
12629
  };
12043
12630
  var apiName = (spec) => spec.title || spec.label;
12044
12631
  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);
12632
+ 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.`;
12633
+ const prose = clip(plainProse(operation.description || operation.summary), META_DESCRIPTION_MAX - stringWidth3(suffix) - 1);
12048
12634
  return clip([prose, suffix].filter(Boolean).join(" "), META_DESCRIPTION_MAX);
12049
12635
  };
12050
12636
  var withDescription = (description, component) => description.trim() ? `${mdxSafe(description.trim())}
@@ -12054,19 +12640,29 @@ var operationMdx = (spec, operation, reference) => {
12054
12640
  const method = operation.method.toUpperCase();
12055
12641
  const title = operation.summary || `${method} ${operation.path}`;
12056
12642
  const description = operation.description.trim() === operation.summary.trim() ? "" : operation.description;
12057
- return {
12058
- body: withDescription(description, `<Operation source="${spec.slug}" id="${operation.key}" />`),
12643
+ const flags = {};
12644
+ if (reference?.includeInLlms === false) {
12645
+ flags.ai = { exclude: true };
12646
+ }
12647
+ if (operation.deprecated) {
12648
+ flags.deprecated = true;
12649
+ }
12650
+ const searchFlags = {};
12651
+ if (reference?.includeInSearch === false) {
12652
+ searchFlags.exclude = true;
12653
+ }
12654
+ const seo = {
12655
+ description: operationDescription(spec, operation)
12656
+ };
12657
+ if (reference?.noindex) {
12658
+ seo.noindex = true;
12659
+ }
12660
+ return {
12661
+ body: withDescription(description, `<Operation source="${spec.slug}" id="${operation.key}" />`),
12059
12662
  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
- },
12663
+ ...flags,
12664
+ search: { ...searchFlags, tags: [operation.tag, method] },
12665
+ seo,
12070
12666
  sidebar: { badge: method, label: operation.summary || operation.path },
12071
12667
  title,
12072
12668
  type: "openapi-operation"
@@ -12107,6 +12703,19 @@ var overviewMdx = (spec, reference) => {
12107
12703
 
12108
12704
  `));
12109
12705
  }
12706
+ const flags = {};
12707
+ if (reference?.includeInLlms === false) {
12708
+ flags.ai = { exclude: true };
12709
+ }
12710
+ if (reference?.includeInSearch === false) {
12711
+ flags.search = { exclude: true };
12712
+ }
12713
+ const seo = {
12714
+ description: clip(plainProse(spec.description), META_DESCRIPTION_MAX) || `${apiName(spec)} API reference.`
12715
+ };
12716
+ if (reference?.noindex) {
12717
+ seo.noindex = true;
12718
+ }
12110
12719
  return {
12111
12720
  body: [
12112
12721
  withDescription(spec.description, `<ApiOverview source="${spec.slug}" />`),
@@ -12115,12 +12724,8 @@ var overviewMdx = (spec, reference) => {
12115
12724
 
12116
12725
  `),
12117
12726
  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
- },
12727
+ ...flags,
12728
+ seo,
12124
12729
  sidebar: { label: "Overview" },
12125
12730
  title: apiName(spec)
12126
12731
  }
@@ -12128,14 +12733,14 @@ var overviewMdx = (spec, reference) => {
12128
12733
  };
12129
12734
 
12130
12735
  // src/openapi/source.ts
12131
- var isOpenApiSource = (source) => source.kind === "openapi-source";
12736
+ var isOpenApiSource = (source) => ("kind" in source) && source.kind === "openapi-source";
12132
12737
  var routeToRef = (route) => route.replace(/^\/+/u, "");
12133
12738
  var toEntry = (rendered, ref) => {
12134
12739
  const raw = frontmatter_default.stringify(`${rendered.body}
12135
12740
  `, rendered.data);
12136
12741
  return {
12137
12742
  body: { format: "mdx", text: rendered.body },
12138
- data: rendered.data,
12743
+ data: { ...rendered.data },
12139
12744
  hash: hashText(raw),
12140
12745
  raw,
12141
12746
  ref
@@ -12154,22 +12759,43 @@ var tagFolderMeta = (spec, tags) => {
12154
12759
  { title: tag.name }
12155
12760
  ]));
12156
12761
  };
12762
+ var parseReference = async (reference, ctx) => {
12763
+ const options = { cacheDir: ctx.cacheDir, refresh: ctx.refresh };
12764
+ if (reference.kind === "asyncapi") {
12765
+ const { document: document2, warnings: warnings2 } = await parseAsyncApiSpec(reference.spec, ctx.projectRoot, options);
12766
+ const extracted2 = extractAsyncApiOperations(document2, reference.route);
12767
+ return {
12768
+ document: document2,
12769
+ extractWarnings: extracted2.warnings,
12770
+ operations: extracted2.operations,
12771
+ tags: extracted2.tags,
12772
+ warnings: warnings2
12773
+ };
12774
+ }
12775
+ const { document, warnings } = await parseSpec(reference.spec, ctx.projectRoot, options);
12776
+ const extracted = extractOperations(document, reference.route);
12777
+ return {
12778
+ document,
12779
+ extractWarnings: extracted.warnings,
12780
+ operations: extracted.operations,
12781
+ tags: extracted.tags,
12782
+ warnings
12783
+ };
12784
+ };
12157
12785
  var openApiSource = (references, ctx) => {
12158
12786
  let parsed = {};
12159
12787
  const loadReference = async (reference) => {
12788
+ const kindLabel = reference.kind === "asyncapi" ? "AsyncAPI" : "OpenAPI";
12789
+ const codePrefix = reference.kind === "asyncapi" ? "BLUME_ASYNCAPI" : "BLUME_OPENAPI";
12160
12790
  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);
12791
+ const { document, warnings, operations, tags, extractWarnings } = await parseReference(reference, ctx);
12167
12792
  const info = document.info ?? { title: reference.label, version: "" };
12168
12793
  const spec = {
12169
12794
  codeSamples: reference.display.codeSamples,
12170
12795
  description: info.description ?? "",
12171
12796
  document,
12172
12797
  expandSchemas: reference.display.expandSchemas,
12798
+ kind: reference.kind,
12173
12799
  label: reference.label,
12174
12800
  operations: Object.fromEntries(operations.map((operation) => [
12175
12801
  operation.key,
@@ -12187,21 +12813,21 @@ var openApiSource = (references, ctx) => {
12187
12813
  return {
12188
12814
  diagnostics: [
12189
12815
  ...warnings.map((message) => ({
12190
- code: "BLUME_OPENAPI_STALE",
12816
+ code: reference.kind === "asyncapi" ? "BLUME_ASYNCAPI_SPEC_WARNING" : "BLUME_OPENAPI_STALE",
12191
12817
  message,
12192
12818
  severity: "warning"
12193
12819
  })),
12194
12820
  ...extractWarnings.map((message) => ({
12195
- code: "BLUME_OPENAPI_REF_PATH_ITEM",
12196
- message: `In OpenAPI spec "${reference.spec}": ${message}`,
12821
+ code: reference.kind === "asyncapi" ? "BLUME_ASYNCAPI_SKIPPED_OPERATION" : "BLUME_OPENAPI_REF_PATH_ITEM",
12822
+ message: `In ${kindLabel} spec "${reference.spec}": ${message}`,
12197
12823
  severity: "warning"
12198
12824
  })),
12199
12825
  ...operations.length === 0 ? [
12200
12826
  {
12201
- code: "BLUME_OPENAPI_EMPTY",
12202
- message: `OpenAPI spec "${reference.spec}" for ${reference.route} declares no operations; its API reference is empty.`,
12827
+ code: `${codePrefix}_EMPTY`,
12828
+ message: `${kindLabel} spec "${reference.spec}" for ${reference.route} declares no operations; its API reference is empty.`,
12203
12829
  severity: "warning",
12204
- suggestion: "Check the spec points at an OpenAPI document with operations under `paths`."
12830
+ 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
12831
  }
12206
12832
  ] : []
12207
12833
  ],
@@ -12212,10 +12838,10 @@ var openApiSource = (references, ctx) => {
12212
12838
  };
12213
12839
  } catch (error) {
12214
12840
  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.`,
12841
+ code: `${codePrefix}_UNAVAILABLE`,
12842
+ message: `Could not load ${kindLabel} spec "${reference.spec}" for ${reference.route} (${error.message}); its reference pages were skipped.`,
12217
12843
  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."
12844
+ 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
12845
  };
12220
12846
  }
12221
12847
  };
@@ -12320,6 +12946,7 @@ import { fromMarkdown as fromMarkdown3 } from "mdast-util-from-markdown";
12320
12946
  import { gfmFromMarkdown } from "mdast-util-gfm";
12321
12947
  import { toString as mdastToString2 } from "mdast-util-to-string";
12322
12948
  import { gfm } from "micromark-extension-gfm";
12949
+ import stringWidth4 from "string-width";
12323
12950
  var DEFAULT_BASE_URL = "https://api.github.com";
12324
12951
  var DEFAULT_LIMIT = 100;
12325
12952
  var PER_PAGE = 100;
@@ -12329,35 +12956,24 @@ var EDGE_DASHES = /^-+|-+$/gu;
12329
12956
  var DESCRIPTION_MAX = 160;
12330
12957
  var DESCRIPTION_MIN = 110;
12331
12958
  var CHANGESET_HASH = /^(?<mark>\s*(?:[-*+]|\d+[.)])\s+)[0-9a-f]{7,40}:\s+/gmu;
12332
- var WHITESPACE2 = /\s+/gu;
12959
+ var WHITESPACE3 = /\s+/gu;
12333
12960
  var TRAILING_FRAGMENT = /[\s,;:.—–-]+$/u;
12334
12961
  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
12962
  var releaseDescription = (body) => {
12347
12963
  const tree = fromMarkdown3(body.replaceAll(CHANGESET_HASH, "$<mark>"), {
12348
12964
  extensions: [gfm()],
12349
12965
  mdastExtensions: [gfmFromMarkdown()]
12350
12966
  });
12351
- const text = tree.children.filter((node) => !NON_PROSE.has(node.type)).map((node) => mdastToString2(node, { includeHtml: false, includeImageAlt: false })).join(" ").replaceAll(WHITESPACE2, " ").trim();
12967
+ const text = tree.children.filter((node) => !NON_PROSE.has(node.type)).map((node) => mdastToString2(node, { includeHtml: false, includeImageAlt: false })).join(" ").replaceAll(WHITESPACE3, " ").trim();
12352
12968
  if (!text) {
12353
12969
  return;
12354
12970
  }
12355
- if (text.length <= DESCRIPTION_MAX) {
12971
+ if (stringWidth4(text) <= DESCRIPTION_MAX) {
12356
12972
  return text;
12357
12973
  }
12358
- const slice = graphemePrefix(text, DESCRIPTION_MAX - 1);
12974
+ const slice = columnsPrefix(text, DESCRIPTION_MAX - 1);
12359
12975
  const boundary = slice.lastIndexOf(" ");
12360
- const head = (boundary >= DESCRIPTION_MIN ? slice.slice(0, boundary) : slice).replace(TRAILING_FRAGMENT, "");
12976
+ const head = (boundary !== -1 && stringWidth4(slice.slice(0, boundary)) >= DESCRIPTION_MIN ? slice.slice(0, boundary) : slice).replace(TRAILING_FRAGMENT, "");
12361
12977
  return `${head}…`;
12362
12978
  };
12363
12979
  var slugifyTag = (tag) => tag.toLowerCase().replaceAll(NON_SLUG2, "-").replaceAll(EDGE_DASHES, "");
@@ -12380,18 +12996,20 @@ var releaseToEntry = (release) => {
12380
12996
  const description = releaseDescription(body);
12381
12997
  const data = {
12382
12998
  changelog: { category, version },
12383
- date,
12384
- ...description ? { seo: { description } } : {},
12385
- title,
12386
- type: "changelog"
12999
+ date
12387
13000
  };
13001
+ if (description) {
13002
+ data.seo = { description };
13003
+ }
13004
+ data.title = title;
13005
+ data.type = "changelog";
12388
13006
  const raw = frontmatter_default.stringify(`${body}
12389
13007
  `, data);
12390
13008
  const fallbackRef = `release-${release.id}`;
12391
13009
  const ref = `${slugifyTag(release.tag_name) || fallbackRef}.md`;
12392
13010
  return {
12393
13011
  body: { format: "md", text: body },
12394
- data,
13012
+ data: { ...data },
12395
13013
  editUrl: release.html_url,
12396
13014
  hash: hashText(raw),
12397
13015
  lastModified: date,
@@ -12688,7 +13306,12 @@ var richToMarkdown = (rich = []) => rich.map((node) => {
12688
13306
  }
12689
13307
  return node.href ? `[${text}](${node.href})` : text;
12690
13308
  }).join("");
12691
- var blockField = (block) => block[block.type]?.rich_text ?? [];
13309
+ var isBlockPayload = (value) => typeof value === "object";
13310
+ var payloadOf = (block) => {
13311
+ const value = block[block.type];
13312
+ return isBlockPayload(value) ? value : undefined;
13313
+ };
13314
+ var blockField = (block) => payloadOf(block)?.rich_text ?? [];
12692
13315
  var RATE_LIMITED = 429;
12693
13316
  var MAX_RETRIES = 4;
12694
13317
  var BASE_DELAY_MS = 500;
@@ -12720,7 +13343,7 @@ var LIST_BLOCKS = new Set([
12720
13343
  ]);
12721
13344
  var isListItem = (block) => block !== undefined && LIST_BLOCKS.has(block.type);
12722
13345
  var renderLeaf = (block) => {
12723
- const data = block[block.type] ?? {};
13346
+ const data = payloadOf(block) ?? {};
12724
13347
  const text = richToMarkdown(blockField(block));
12725
13348
  switch (block.type) {
12726
13349
  case "paragraph": {
@@ -12756,9 +13379,8 @@ ${text}
12756
13379
  \`\`\``;
12757
13380
  }
12758
13381
  case "image": {
12759
- const media = data;
12760
- const url = media.external?.url ?? media.file?.url;
12761
- return url ? `![${richToMarkdown(media.caption)}](${url})` : "";
13382
+ const url = data.external?.url ?? data.file?.url;
13383
+ return url ? `![${richToMarkdown(data.caption)}](${url})` : "";
12762
13384
  }
12763
13385
  default: {
12764
13386
  return null;
@@ -12878,7 +13500,7 @@ ${nested}`;
12878
13500
  };
12879
13501
  const orderOf = (page) => {
12880
13502
  const order = page.properties[props.order ?? "Order"]?.number;
12881
- return typeof order === "number" ? order : undefined;
13503
+ return order === null ? undefined : order;
12882
13504
  };
12883
13505
  const frontmatter = (page) => {
12884
13506
  const data = {};
@@ -12965,14 +13587,14 @@ ${nested}`;
12965
13587
  import { join as join16 } from "pathe";
12966
13588
 
12967
13589
  // src/core/sources/portable-text.ts
12968
- var HEADING_STYLES = {
12969
- h1: "# ",
12970
- h2: "## ",
12971
- h3: "### ",
12972
- h4: "#### ",
12973
- h5: "##### ",
12974
- h6: "###### "
12975
- };
13590
+ var HEADING_STYLES = new Map([
13591
+ ["h1", "# "],
13592
+ ["h2", "## "],
13593
+ ["h3", "### "],
13594
+ ["h4", "#### "],
13595
+ ["h5", "##### "],
13596
+ ["h6", "###### "]
13597
+ ]);
12976
13598
  var MARKDOWN_SPECIALS = /[\\`*_[\]~<]/gu;
12977
13599
  var escapeText = (text) => text.replaceAll(MARKDOWN_SPECIALS, String.raw`\$&`);
12978
13600
  var renderSpan = (span, defs) => {
@@ -13014,6 +13636,7 @@ var renderChildren = (block) => {
13014
13636
  const defs = new Map((block.markDefs ?? []).map((def) => [def._key, def]));
13015
13637
  return (block.children ?? []).map((span) => renderSpan(span, defs)).join("");
13016
13638
  };
13639
+ var isAltText = (value) => typeof value === "string";
13017
13640
  var renderBlock = (block, options) => {
13018
13641
  const custom = options.serializers?.[block._type];
13019
13642
  if (custom) {
@@ -13021,7 +13644,7 @@ var renderBlock = (block, options) => {
13021
13644
  }
13022
13645
  if (block._type === "image") {
13023
13646
  const url = options.imageUrl?.(block);
13024
- const alt = typeof block.alt === "string" ? block.alt : "";
13647
+ const alt = isAltText(block.alt) ? block.alt : "";
13025
13648
  return url ? `![${alt}](${url})` : "";
13026
13649
  }
13027
13650
  if (block._type !== "block") {
@@ -13036,7 +13659,7 @@ var renderBlock = (block, options) => {
13036
13659
  if (block.style === "blockquote") {
13037
13660
  return `> ${inline}`;
13038
13661
  }
13039
- return `${HEADING_STYLES[block.style ?? "normal"] ?? ""}${inline}`;
13662
+ return `${HEADING_STYLES.get(block.style ?? "normal") ?? ""}${inline}`;
13040
13663
  };
13041
13664
  var portableTextToMarkdown = (blocks, options = {}) => {
13042
13665
  const lines = blocks.map((block) => renderBlock(block, options));
@@ -13059,10 +13682,11 @@ var portableTextToMarkdown = (blocks, options = {}) => {
13059
13682
 
13060
13683
  // src/core/sources/sanity.ts
13061
13684
  var IMAGE_REF = /^image-(?<id>[a-f0-9]+)-(?<dims>\d+x\d+)-(?<ext>\w+)$/u;
13685
+ var isTraversable = (value) => typeof value === "object" && value !== null;
13062
13686
  var getPath = (doc, path) => {
13063
13687
  let current = doc;
13064
13688
  for (const key of path.split(".")) {
13065
- if (current && typeof current === "object") {
13689
+ if (isTraversable(current)) {
13066
13690
  current = current[key];
13067
13691
  } else {
13068
13692
  return;
@@ -13070,7 +13694,8 @@ var getPath = (doc, path) => {
13070
13694
  }
13071
13695
  return current;
13072
13696
  };
13073
- var asString = (value) => typeof value === "string" ? value : undefined;
13697
+ var isStringValue2 = (value) => typeof value === "string";
13698
+ var asString = (value) => isStringValue2(value) ? value : undefined;
13074
13699
  var imageUrlFromRef = (ref, projectId, dataset) => {
13075
13700
  const match = ref.match(IMAGE_REF);
13076
13701
  if (!match?.groups) {
@@ -13129,7 +13754,7 @@ var sanitySource = (options, ctx) => {
13129
13754
  const raw = frontmatter_default.stringify(markdown, data);
13130
13755
  return {
13131
13756
  body: { format: "md", text: markdown },
13132
- data,
13757
+ data: { ...data },
13133
13758
  hash: hashText(raw),
13134
13759
  lastModified: asString(getPath(doc, fields.lastModified ?? "_updatedAt")),
13135
13760
  raw,
@@ -13354,7 +13979,8 @@ var normalizeLoadedEntries = (loaded, config) => {
13354
13979
  prefix: source.prefix,
13355
13980
  staged: source.staged
13356
13981
  },
13357
- typeFrontmatter
13982
+ typeFrontmatter,
13983
+ versions: config.versions
13358
13984
  });
13359
13985
  if (normalized.pages.length === 0 && normalized.diagnostics.length > 0) {
13360
13986
  droppedPages += 1;
@@ -13387,7 +14013,10 @@ var scanProject = async (root, options = {}) => {
13387
14013
  const localeDirs = config.i18n && config.i18n.parser === "dir" ? config.i18n.locales.flatMap((locale) => locale.code === config.i18n?.defaultLocale ? [] : [locale.code]) : undefined;
13388
14014
  const [loaded, folderMeta] = await Promise.all([
13389
14015
  Promise.all(sources.map(async (source) => ({ source, ...await source.load() }))),
13390
- discoverFolderMeta(metaSources, { localeDirs })
14016
+ discoverFolderMeta(metaSources, {
14017
+ localeDirs,
14018
+ versionDirs: config.versions?.archived.map((version) => version.id)
14019
+ })
13391
14020
  ]);
13392
14021
  const sharedFolderMeta = new Map([
13393
14022
  ...loaded.flatMap(({ folderMeta: sourceMeta }) => Object.entries(sourceMeta ?? {})),
@@ -13415,10 +14044,12 @@ var scanProject = async (root, options = {}) => {
13415
14044
  folderMeta: folderMeta.meta,
13416
14045
  i18n: config.i18n,
13417
14046
  navigation: config.navigation,
13418
- sharedFolderMeta
14047
+ sharedFolderMeta,
14048
+ versions: config.versions
13419
14049
  });
13420
14050
  const manifest = buildManifest({ config, context, graph });
13421
- const i18nWarnings = config.i18n ? i18nDiagnostics(pages, config.i18n) : [];
14051
+ const i18nWarnings = config.i18n ? i18nDiagnostics(pages, config.i18n, config.versions) : [];
14052
+ const versionWarnings = config.versions ? versionsDiagnostics(pages, config.versions) : [];
13422
14053
  return {
13423
14054
  config,
13424
14055
  context,
@@ -13427,7 +14058,8 @@ var scanProject = async (root, options = {}) => {
13427
14058
  ...folderMeta.diagnostics,
13428
14059
  ...entryIdDiagnostics(pages, resolveDocsCollection(config, context).base),
13429
14060
  ...graph.diagnostics,
13430
- ...i18nWarnings
14061
+ ...i18nWarnings,
14062
+ ...versionWarnings
13431
14063
  ],
13432
14064
  droppedPages,
13433
14065
  graph,
@@ -13547,9 +14179,10 @@ var auditCommand = defineCommand2({
13547
14179
  return;
13548
14180
  }
13549
14181
  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(" | ")}).`);
14182
+ const gateInput = args["fail-on"] ?? (args.strict ? "warning" : "error");
14183
+ const gate = SEVERITIES.find((severity) => severity === gateInput);
14184
+ if (gate === undefined) {
14185
+ logger.error(`Invalid --fail-on "${gateInput}" (use ${SEVERITIES.join(" | ")}).`);
13553
14186
  process.exit(1);
13554
14187
  }
13555
14188
  const agents = Object.keys(AGENTS).filter((kind) => args[kind]);
@@ -13748,12 +14381,8 @@ var buildAgentReadability = (project) => {
13748
14381
  return site ? absoluteUrl(site, based) : based;
13749
14382
  };
13750
14383
  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
- };
14384
+ const markdown = negotiates ? { contentNegotiation: "text/markdown", pattern: abs("/{route}.md") } : { pattern: abs("/{route}.md") };
14385
+ const artifacts = { markdown };
13757
14386
  if (config.ai.llmsTxt.enabled) {
13758
14387
  artifacts.llmsFullTxt = abs("/llms-full.txt");
13759
14388
  artifacts.llmsTxt = abs("/llms.txt");
@@ -13934,6 +14563,8 @@ var parseYouTubeId = (input) => {
13934
14563
  };
13935
14564
 
13936
14565
  // src/ai/component-markdown.ts
14566
+ var isString4 = (value) => typeof value === "string";
14567
+ var isNumber = (value) => typeof value === "number";
13937
14568
  var evaluateExpression = (raw, frontmatter) => {
13938
14569
  try {
13939
14570
  const value = new Function("frontmatter", `"use strict"; return (${raw});`)(frontmatter);
@@ -13952,7 +14583,7 @@ var readProps = (node, frontmatter) => {
13952
14583
  }
13953
14584
  if (attribute.value === null || attribute.value === undefined) {
13954
14585
  props[attribute.name] = true;
13955
- } else if (typeof attribute.value === "string") {
14586
+ } else if (isString4(attribute.value)) {
13956
14587
  props[attribute.name] = attribute.value;
13957
14588
  } else {
13958
14589
  const result = evaluateExpression(attribute.value.value, frontmatter);
@@ -14003,9 +14634,10 @@ var cellCode = (value) => {
14003
14634
  const text = cellText(value);
14004
14635
  return text && !text.includes("`") ? `\`${text}\`` : text;
14005
14636
  };
14637
+ var isTypeEntryMap = (value) => typeof value === "object" && value !== null;
14006
14638
  var typeTable = ({ children, props }) => {
14007
14639
  const { type } = props;
14008
- if (type === null || typeof type !== "object") {
14640
+ if (!isTypeEntryMap(type)) {
14009
14641
  return null;
14010
14642
  }
14011
14643
  const entries = Object.entries(type);
@@ -14013,7 +14645,7 @@ var typeTable = ({ children, props }) => {
14013
14645
  const prop = cellCode(`${name}${info.required ? "" : "?"}`);
14014
14646
  const typeCell = info.typeDescriptionLink ? `[${cellCode(info.type)}](${cellText(info.typeDescriptionLink)})` : cellCode(info.type);
14015
14647
  const defaultCell = info.default === undefined ? "-" : cellCode(info.default);
14016
- const description = cellText([info.description, info.typeDescription].filter((part) => typeof part === "string" && part !== "").join(" "));
14648
+ const description = cellText([info.description, info.typeDescription].filter((part) => isString4(part) && part !== "").join(" "));
14017
14649
  return [prop, typeCell, defaultCell, description];
14018
14650
  });
14019
14651
  const table = rows.length > 0 ? markdownTable([["Prop", "Type", "Default", "Description"], ...rows], {
@@ -14024,8 +14656,8 @@ var typeTable = ({ children, props }) => {
14024
14656
  `);
14025
14657
  };
14026
14658
  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);
14659
+ const type = isString4(props.type) ? props.type : "info";
14660
+ const label = isString4(props.title) && props.title !== "" ? props.title : type.charAt(0).toUpperCase() + type.slice(1);
14029
14661
  if (!children) {
14030
14662
  return `> **${label}**`;
14031
14663
  }
@@ -14053,7 +14685,7 @@ var steps = ({ childComponents, children }) => {
14053
14685
  return children;
14054
14686
  }
14055
14687
  return items.map((step, index) => {
14056
- const title = typeof step.props.title === "string" && step.props.title !== "" ? `**${step.props.title}**` : "";
14688
+ const title = isString4(step.props.title) && step.props.title !== "" ? `**${step.props.title}**` : "";
14057
14689
  const content = [title, step.children].filter(Boolean).join(`
14058
14690
 
14059
14691
  `);
@@ -14068,7 +14700,7 @@ var tabs = ({ childComponents, children }) => {
14068
14700
  return children;
14069
14701
  }
14070
14702
  return items.map((tab, index) => {
14071
- const title = typeof tab.props.title === "string" && tab.props.title !== "" ? tab.props.title : `Tab ${index + 1}`;
14703
+ const title = isString4(tab.props.title) && tab.props.title !== "" ? tab.props.title : `Tab ${index + 1}`;
14072
14704
  return tab.children ? `**${title}**
14073
14705
 
14074
14706
  ${tab.children}` : `**${title}**`;
@@ -14078,17 +14710,17 @@ ${tab.children}` : `**${title}**`;
14078
14710
  };
14079
14711
  var youtube = ({ props }) => {
14080
14712
  let input = "";
14081
- if (typeof props.id === "string") {
14713
+ if (isString4(props.id)) {
14082
14714
  input = props.id;
14083
- } else if (typeof props.url === "string") {
14715
+ } else if (isString4(props.url)) {
14084
14716
  input = props.url;
14085
14717
  }
14086
14718
  const videoId = parseYouTubeId(input);
14087
14719
  if (!videoId) {
14088
14720
  return null;
14089
14721
  }
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";
14722
+ const start = isNumber(props.start) && props.start > 0 ? `&t=${Math.floor(props.start)}s` : "";
14723
+ const title = isString4(props.title) && props.title !== "" ? props.title : "Watch on YouTube";
14092
14724
  return `[${title}](https://www.youtube.com/watch?v=${videoId}${start})`;
14093
14725
  };
14094
14726
  var fencedBlock = (lang, code) => {
@@ -14102,7 +14734,7 @@ ${fence}`;
14102
14734
  };
14103
14735
  var exampleComponentSerializers = (examples) => ({
14104
14736
  Component: ({ props }) => {
14105
- const path = typeof props.path === "string" ? props.path : undefined;
14737
+ const path = isString4(props.path) ? props.path : undefined;
14106
14738
  const example = path === undefined ? undefined : examples[path];
14107
14739
  return example ? fencedBlock(example.lang, example.source) : null;
14108
14740
  }
@@ -14212,19 +14844,28 @@ var pageUrl = (route, site, base = "") => {
14212
14844
  const path = withBasePath(base, route);
14213
14845
  return encodeURI(site ? absoluteUrl(site, path) : path);
14214
14846
  };
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"));
14847
+ 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
14848
  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 }];
14849
+ const { i18n, versions } = project.config;
14850
+ const current = i18n ? i18n.locales.flatMap(({ code, label }) => {
14851
+ const nav = project.graph.navigationByLocale[code];
14852
+ if (!nav) {
14853
+ return [];
14854
+ }
14855
+ return [
14856
+ { label: code === i18n.defaultLocale ? undefined : label, nav }
14857
+ ];
14858
+ }) : [{ nav: project.graph.navigation }];
14859
+ const archived = (versions?.archived ?? []).flatMap((version) => {
14860
+ const byLocale = project.graph.navigationByVersion[version.id] ?? {};
14861
+ const versionLabel = `${version.label ?? version.id} (archived)`;
14862
+ return Object.entries(byLocale).flatMap(([code, nav]) => {
14863
+ const locale = i18n?.locales.find((entry) => entry.code === code);
14864
+ const label = locale && code !== i18n?.defaultLocale ? `${locale.label} — ${versionLabel}` : versionLabel;
14865
+ return [{ label, nav }];
14225
14866
  });
14226
- }
14227
- return [{ nav: project.graph.navigation }];
14867
+ });
14868
+ return [...current, ...archived];
14228
14869
  };
14229
14870
  var buildLlmsIndex = (project) => {
14230
14871
  const { config } = project;
@@ -14301,7 +14942,7 @@ var buildLlmsIndex = (project) => {
14301
14942
  };
14302
14943
  var buildFull = async (project) => {
14303
14944
  const { config } = project;
14304
- const pages = eligiblePages(project).toSorted((a, b) => a.route.localeCompare(b.route));
14945
+ const pages = eligiblePages(project, { versions: "current" }).toSorted((a, b) => a.route.localeCompare(b.route));
14305
14946
  const components = {
14306
14947
  ...exampleComponentSerializers(project.examples ?? {}),
14307
14948
  ...config.ai.markdownComponents
@@ -14446,6 +15087,7 @@ var collectEntries = async (dir, prefix = "") => {
14446
15087
  }
14447
15088
  return entries;
14448
15089
  };
15090
+ var isString5 = (value) => typeof value === "string";
14449
15091
  var skillMeta = (raw, dirName) => {
14450
15092
  let data;
14451
15093
  try {
@@ -14456,8 +15098,8 @@ var skillMeta = (raw, dirName) => {
14456
15098
  warning: `Skill "${dirName}" has unparsable SKILL.md frontmatter; skipped.`
14457
15099
  };
14458
15100
  }
14459
- const name = typeof data.name === "string" ? data.name : "";
14460
- const description = typeof data.description === "string" ? data.description : "";
15101
+ const name = isString5(data.name) ? data.name : "";
15102
+ const description = isString5(data.description) ? data.description : "";
14461
15103
  if (!(name && description)) {
14462
15104
  return {
14463
15105
  meta: null,
@@ -14582,6 +15224,8 @@ var MAX_RULES = 100;
14582
15224
  var MAX_RULE_LENGTH = 100;
14583
15225
  var FALLBACK_RULES = ["/*", "!/_astro/*", "!/*.md", "!/*.mdx", "!/*.txt"];
14584
15226
  var basePrefix = (base) => base && base !== "/" ? base.replace(/\/$/u, "") : "";
15227
+ var isJsonObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
15228
+ var isJsonString = (value) => typeof value === "string";
14585
15229
  var isNegativeRule = (rule) => rule.startsWith("!");
14586
15230
  var ruleBody = (rule) => isNegativeRule(rule) ? rule.slice(1) : rule;
14587
15231
  var buildRunWorkerFirstRules = (routePaths, base) => {
@@ -14836,15 +15480,15 @@ var injectWorkerNegotiation = (wranglerText, options) => {
14836
15480
  } catch {
14837
15481
  return null;
14838
15482
  }
14839
- if (config === null || typeof config !== "object" || Array.isArray(config)) {
15483
+ if (!isJsonObject(config)) {
14840
15484
  return null;
14841
15485
  }
14842
15486
  const { main } = config;
14843
- if (typeof main !== "string" || main.length === 0 || main === NEGOTIATION_WORKER_FILE) {
15487
+ if (!isJsonString(main) || main.length === 0 || main === NEGOTIATION_WORKER_FILE) {
14844
15488
  return null;
14845
15489
  }
14846
- const assets = config.assets;
14847
- if (assets === null || typeof assets !== "object" || typeof assets.binding !== "string") {
15490
+ const { assets } = config;
15491
+ if (!isJsonObject(assets) || !isJsonString(assets.binding)) {
14848
15492
  return null;
14849
15493
  }
14850
15494
  const {
@@ -15059,6 +15703,16 @@ var buildSitemapFiles = (project) => {
15059
15703
  }
15060
15704
  const base = siteRoot(site);
15061
15705
  const deployBase = normalizeBasePath(project.config.deployment.base);
15706
+ const { versions } = project.config;
15707
+ const archivedById = new Map((versions?.archived ?? []).map((version) => [version.id, version]));
15708
+ const currentKeys = versions ? new Set(project.graph.pages.flatMap((page) => page.version === "" ? [`${page.versionKey}\x00${page.locale}`] : [])) : null;
15709
+ const archivedExcluded = (page) => {
15710
+ if (!page.version) {
15711
+ return false;
15712
+ }
15713
+ const archived = archivedById.get(page.version);
15714
+ return Boolean(archived?.noindex || archived?.canonical === "latest" && currentKeys?.has(`${page.versionKey}\x00${page.locale}`));
15715
+ };
15062
15716
  const seen = new Set;
15063
15717
  const urls = [];
15064
15718
  const pushUrl = (route, lastModified) => {
@@ -15070,7 +15724,7 @@ var buildSitemapFiles = (project) => {
15070
15724
  urls.push(` <url><loc>${loc}</loc>${lastmodTag(lastModified)}</url>`);
15071
15725
  };
15072
15726
  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)) {
15727
+ if (page.meta.draft || page.meta.sidebar.hidden || page.meta.seo.noindex || ERROR_ROUTES3.has(page.route) || archivedExcluded(page)) {
15074
15728
  continue;
15075
15729
  }
15076
15730
  pushUrl(withBasePath(deployBase, page.route), page.lastModified);
@@ -15109,6 +15763,7 @@ ${references.join(`
15109
15763
 
15110
15764
  // src/deploy/vercel-negotiation.ts
15111
15765
  var ACCEPT_MARKDOWN_HEADER_VALUE = "(.*,)?\\s*text/(x-)?markdown(\\s*[;,].*)?$";
15766
+ var isString6 = (value) => typeof value === "string";
15112
15767
  var ACCEPT_MARKDOWN_CONDITION = [
15113
15768
  { key: "accept", type: "header", value: ACCEPT_MARKDOWN_HEADER_VALUE }
15114
15769
  ];
@@ -15163,7 +15818,7 @@ var buildNegotiationRoutes = (routePaths, homeTokens) => {
15163
15818
  return { headerRoutes, rewriteRoutes };
15164
15819
  };
15165
15820
  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;
15821
+ 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;
15167
15822
  var injectNegotiationRoutes = (configText, routePaths, homeLinkHeader, contentTypeOverrides, homeTokens) => {
15168
15823
  const overrideEntries = Object.entries(contentTypeOverrides ?? {});
15169
15824
  if (routePaths.length === 0 && !homeLinkHeader && overrideEntries.length === 0) {
@@ -15220,6 +15875,7 @@ import { gfmFromMarkdown as gfmFromMarkdown2 } from "mdast-util-gfm";
15220
15875
  import { gfm as gfm2 } from "micromark-extension-gfm";
15221
15876
 
15222
15877
  // src/search/facets.ts
15878
+ var isFacetValue = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean";
15223
15879
  var pageFacets = (page, config) => {
15224
15880
  const declared = config.content.types[page.contentType]?.facets;
15225
15881
  if (!declared || declared.length === 0 || !page.custom) {
@@ -15228,7 +15884,7 @@ var pageFacets = (page, config) => {
15228
15884
  const facets = {};
15229
15885
  for (const key of declared) {
15230
15886
  const value = page.custom[key];
15231
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
15887
+ if (isFacetValue(value)) {
15232
15888
  facets[key] = String(value);
15233
15889
  }
15234
15890
  }
@@ -15237,7 +15893,7 @@ var pageFacets = (page, config) => {
15237
15893
 
15238
15894
  // src/search/documents.ts
15239
15895
  var HTML_OR_JSX = /<\/?[a-zA-Z][^\n<>]*>|<\/?>/gu;
15240
- var WHITESPACE3 = /\s+/gu;
15896
+ var WHITESPACE4 = /\s+/gu;
15241
15897
  var INLINE_PARENTS = new Set([
15242
15898
  "delete",
15243
15899
  "emphasis",
@@ -15289,7 +15945,7 @@ var toPlainText = (markdown) => {
15289
15945
  });
15290
15946
  const out = [];
15291
15947
  collectText(tree, out);
15292
- return out.join("").replaceAll(WHITESPACE3, " ").trim();
15948
+ return out.join("").replaceAll(WHITESPACE4, " ").trim();
15293
15949
  };
15294
15950
  var buildCrumbIndex = (sidebar) => {
15295
15951
  const index = new Map;
@@ -15323,7 +15979,11 @@ var buildCrumbIndex = (sidebar) => {
15323
15979
  var buildSearchDocuments = async (project, options) => {
15324
15980
  const pageById = new Map(project.graph.pages.map((page) => [page.id, page]));
15325
15981
  const byLocale = Object.values(project.graph.navigationByLocale ?? {});
15326
- const sidebars = byLocale.length > 0 ? byLocale.map((nav) => nav.sidebar) : [project.graph.navigation?.sidebar ?? []];
15982
+ const byVersion = Object.values(project.graph.navigationByVersion ?? {}).flatMap((locales) => Object.values(locales)).map((nav) => nav.sidebar);
15983
+ const sidebars = [
15984
+ ...byLocale.length > 0 ? byLocale.map((nav) => nav.sidebar) : [project.graph.navigation?.sidebar ?? []],
15985
+ ...byVersion
15986
+ ];
15327
15987
  const crumbs = new Map;
15328
15988
  for (const sidebar of sidebars) {
15329
15989
  for (const [route, crumb] of buildCrumbIndex(sidebar)) {
@@ -15346,18 +16006,22 @@ var buildSearchDocuments = async (project, options) => {
15346
16006
  const tags = page?.meta?.search?.tags;
15347
16007
  const crumb = crumbs.get(route.path);
15348
16008
  const facets = page ? pageFacets(page, project.config) : undefined;
15349
- return {
16009
+ const document = {
15350
16010
  breadcrumb: crumb?.breadcrumb ?? [],
15351
16011
  content: body,
15352
16012
  contentType: route.contentType,
15353
16013
  description: page?.description ?? "",
15354
- ...facets ? { facets } : {},
15355
16014
  locale: route.locale,
15356
16015
  route: route.path,
15357
16016
  section: crumb?.section || "Docs",
15358
16017
  tags: tags && tags.length > 0 ? tags : undefined,
15359
- title: route.title
16018
+ title: route.title,
16019
+ version: route.version
15360
16020
  };
16021
+ if (facets) {
16022
+ document.facets = facets;
16023
+ }
16024
+ return document;
15361
16025
  }));
15362
16026
  };
15363
16027
  var toSearchRecords = (documents) => documents.map((doc) => ({
@@ -15367,7 +16031,8 @@ var toSearchRecords = (documents) => documents.map((doc) => ({
15367
16031
  locale: doc.locale,
15368
16032
  tag: doc.tags?.[0],
15369
16033
  title: doc.title,
15370
- url: doc.route
16034
+ url: doc.route,
16035
+ version: doc.version || "current"
15371
16036
  }));
15372
16037
 
15373
16038
  // src/search/sync/algolia.ts
@@ -15451,7 +16116,8 @@ var syncTypesense = async (records, config) => {
15451
16116
  { name: "content", type: "string" },
15452
16117
  { name: "url", type: "string" },
15453
16118
  { facet: true, name: "tag", optional: true, type: "string" },
15454
- { facet: true, name: "locale", optional: true, type: "string" }
16119
+ { facet: true, name: "locale", optional: true, type: "string" },
16120
+ { facet: true, name: "version", optional: true, type: "string" }
15455
16121
  ],
15456
16122
  name: config.collection
15457
16123
  });
@@ -15462,7 +16128,8 @@ var syncTypesense = async (records, config) => {
15462
16128
  locale: record.locale,
15463
16129
  tag: record.tag,
15464
16130
  title: record.title,
15465
- url: record.url
16131
+ url: record.url,
16132
+ version: record.version
15466
16133
  }));
15467
16134
  await client.collections(config.collection).documents().import(documents, { action: "upsert" });
15468
16135
  };
@@ -15500,6 +16167,8 @@ import {
15500
16167
  import { join as join21 } from "pathe";
15501
16168
  var lockPath = (outDir) => join21(outDir, "dev.lock");
15502
16169
  var isValidPid = (pid) => typeof pid === "number" && Number.isInteger(pid) && pid > 0;
16170
+ var isPortNumber = (port) => typeof port === "number";
16171
+ var isLockRecord = (data) => typeof data === "object" && data !== null;
15503
16172
  var parseLock = (raw) => {
15504
16173
  let data;
15505
16174
  try {
@@ -15510,10 +16179,10 @@ var parseLock = (raw) => {
15510
16179
  if (isValidPid(data)) {
15511
16180
  return { pid: data };
15512
16181
  }
15513
- if (typeof data === "object" && data !== null) {
16182
+ if (isLockRecord(data)) {
15514
16183
  const { pid, port } = data;
15515
16184
  if (isValidPid(pid)) {
15516
- return typeof port === "number" ? { pid, port } : { pid };
16185
+ return isPortNumber(port) ? { pid, port } : { pid };
15517
16186
  }
15518
16187
  }
15519
16188
  return null;
@@ -15534,10 +16203,13 @@ var readDevLock = (outDir) => {
15534
16203
  const lock = parseLock(readFileSync5(path, "utf-8"));
15535
16204
  return lock && isProcessAlive(lock.pid) ? lock : null;
15536
16205
  };
15537
- var lockPayload = (port) => JSON.stringify({
15538
- pid: process.pid,
15539
- ...port === undefined ? {} : { port }
15540
- });
16206
+ var lockPayload = (port) => {
16207
+ const info = { pid: process.pid };
16208
+ if (port !== undefined) {
16209
+ info.port = port;
16210
+ }
16211
+ return JSON.stringify(info);
16212
+ };
15541
16213
  var writeLock = (outDir, port) => {
15542
16214
  writeFileSync(lockPath(outDir), lockPayload(port));
15543
16215
  };
@@ -15669,27 +16341,39 @@ var buildMcpData = async (project) => {
15669
16341
  }
15670
16342
  const page = pageById.get(route.id);
15671
16343
  const facets = page ? pageFacets(page, config) : undefined;
15672
- routes.push({
16344
+ const entry = {
15673
16345
  contentType: route.contentType,
15674
16346
  description: page?.description,
15675
- ...facets ? { facets } : {},
15676
16347
  indexable: route.indexable,
15677
16348
  lastModified: route.lastModified ?? null,
16349
+ locale: route.locale,
15678
16350
  route: route.path,
15679
- title: route.title
15680
- });
16351
+ title: route.title,
16352
+ version: route.version
16353
+ };
16354
+ if (facets) {
16355
+ entry.facets = facets;
16356
+ }
16357
+ routes.push(entry);
15681
16358
  }
15682
- return {
16359
+ const data = {
15683
16360
  base: normalizeBasePath(config.deployment.base),
15684
16361
  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
- })),
16362
+ documents: documents.map((doc) => {
16363
+ const document = {
16364
+ content: doc.content,
16365
+ contentType: doc.contentType,
16366
+ description: doc.description,
16367
+ locale: doc.locale,
16368
+ route: doc.route,
16369
+ title: doc.title,
16370
+ version: doc.version
16371
+ };
16372
+ if (doc.facets) {
16373
+ document.facets = doc.facets;
16374
+ }
16375
+ return document;
16376
+ }),
15693
16377
  instructions: config.ai.mcp.instructions,
15694
16378
  name: config.ai.mcp.name ?? config.title,
15695
16379
  navigation: graph.navigation,
@@ -15698,6 +16382,14 @@ var buildMcpData = async (project) => {
15698
16382
  site: config.deployment.site ?? null,
15699
16383
  version: manifest.blumeVersion
15700
16384
  };
16385
+ if (config.versions) {
16386
+ data.archivedVersions = config.versions.archived.map((version) => version.id);
16387
+ data.navigationByVersion = graph.navigationByVersion;
16388
+ }
16389
+ if (config.i18n) {
16390
+ data.navigationByLocale = graph.navigationByLocale;
16391
+ }
16392
+ return data;
15701
16393
  };
15702
16394
 
15703
16395
  // src/ai/mcp/tools.ts
@@ -15705,7 +16397,7 @@ var READ_ONLY = { openWorldHint: false, readOnlyHint: true };
15705
16397
  var MCP_TOOLS = [
15706
16398
  {
15707
16399
  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.',
16400
+ 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
16401
  name: "search_docs",
15710
16402
  title: "Search documentation"
15711
16403
  },
@@ -15717,13 +16409,13 @@ var MCP_TOOLS = [
15717
16409
  },
15718
16410
  {
15719
16411
  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.",
16412
+ 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
16413
  name: "list_pages",
15722
16414
  title: "List pages"
15723
16415
  },
15724
16416
  {
15725
16417
  annotations: READ_ONLY,
15726
- description: "Return the documentation navigation tree (header tabs and the sidebar hierarchy), reflecting how the docs are organized for readers.",
16418
+ 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
16419
  name: "get_navigation",
15728
16420
  title: "Get navigation"
15729
16421
  }
@@ -15761,12 +16453,11 @@ var reverseDnsName = (input) => {
15761
16453
  var HTTP_URL2 = /^https?:\/\//u;
15762
16454
  var buildMcpServerCard = (input) => {
15763
16455
  const url = serverUrl(input);
15764
- return {
16456
+ const card = {
15765
16457
  $schema: SERVER_CARD_SCHEMA,
15766
16458
  capabilities: { tools: { listChanged: false } },
15767
16459
  description: truncate(`Model Context Protocol server for the ${input.name} documentation.`),
15768
16460
  name: reverseDnsName(input),
15769
- ...HTTP_URL2.test(url) ? { remotes: [{ type: "streamable-http", url }] } : {},
15770
16461
  serverInfo: { name: input.name, version: input.version },
15771
16462
  title: truncate(input.name),
15772
16463
  tools: MCP_TOOLS.map((tool) => ({
@@ -15778,9 +16469,15 @@ var buildMcpServerCard = (input) => {
15778
16469
  transport: "streamable-http",
15779
16470
  transports: [{ endpoint: url, type: "streamable-http" }],
15780
16471
  url,
15781
- version: input.version,
15782
- ...input.site ? { websiteUrl: siteRoot(input.site) } : {}
16472
+ version: input.version
15783
16473
  };
16474
+ if (HTTP_URL2.test(url)) {
16475
+ card.remotes = [{ type: "streamable-http", url }];
16476
+ }
16477
+ if (input.site) {
16478
+ card.websiteUrl = siteRoot(input.site);
16479
+ }
16480
+ return card;
15784
16481
  };
15785
16482
 
15786
16483
  // src/core/builtin-tags.ts
@@ -15853,12 +16550,13 @@ import { dirname as dirname8, extname as extname7, isAbsolute as isAbsolute6, re
15853
16550
  import ts from "typescript";
15854
16551
  var GROUPS = ["mdx", "layout", "islands"];
15855
16552
  var GROUP_SET = new Set(GROUPS);
15856
- var FRAMEWORK_BY_EXT = {
15857
- jsx: "react",
15858
- svelte: "svelte",
15859
- tsx: "react",
15860
- vue: "vue"
15861
- };
16553
+ var isGroup = (name) => GROUP_SET.has(name);
16554
+ var FRAMEWORK_BY_EXT = new Map([
16555
+ ["jsx", "react"],
16556
+ ["svelte", "svelte"],
16557
+ ["tsx", "react"],
16558
+ ["vue", "vue"]
16559
+ ]);
15862
16560
  var FRAMEWORK_LABEL = {
15863
16561
  react: "React",
15864
16562
  svelte: "Svelte",
@@ -15881,6 +16579,7 @@ var HYDRATION_MODES = new Set([
15881
16579
  "only",
15882
16580
  "visible"
15883
16581
  ]);
16582
+ var isHydrationMode = (value) => HYDRATION_MODES.has(value);
15884
16583
  var emptyAnalysis = () => ({
15885
16584
  islands: [],
15886
16585
  layout: [],
@@ -15962,7 +16661,7 @@ var toImport = (specifier, imported, dir) => {
15962
16661
  }
15963
16662
  }
15964
16663
  return {
15965
- framework: FRAMEWORK_BY_EXT[extension] ?? null,
16664
+ framework: FRAMEWORK_BY_EXT.get(extension) ?? null,
15966
16665
  name: imported,
15967
16666
  path
15968
16667
  };
@@ -15991,7 +16690,7 @@ var applyDescriptorProperty = (descriptor, property, imports, dir) => {
15991
16690
  } else if (ts.isIdentifier(init)) {
15992
16691
  descriptor.source = resolveIdentifier(init.text, imports, dir);
15993
16692
  }
15994
- } else if (name === "client" && ts.isStringLiteral(init) && HYDRATION_MODES.has(init.text)) {
16693
+ } else if (name === "client" && ts.isStringLiteral(init) && isHydrationMode(init.text)) {
15995
16694
  descriptor.client = init.text;
15996
16695
  } else if (name === "media" && ts.isStringLiteral(init)) {
15997
16696
  descriptor.media = init.text;
@@ -16025,13 +16724,14 @@ var finalize = (key, group, descriptor, label, identifier, warnings) => {
16025
16724
  if (!client && source?.framework) {
16026
16725
  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
16726
  }
16028
- return {
16029
- identifier,
16030
- key,
16031
- ...client ? { client } : {},
16032
- ...media ? { media } : {},
16033
- source
16034
- };
16727
+ const normalized = { identifier, key, source };
16728
+ if (client) {
16729
+ normalized.client = client;
16730
+ }
16731
+ if (media) {
16732
+ normalized.media = media;
16733
+ }
16734
+ return normalized;
16035
16735
  };
16036
16736
  var normalizeEntry2 = (entry, group, imports, dir, warnings) => {
16037
16737
  const defaultClient = group === "islands" ? "visible" : undefined;
@@ -16084,14 +16784,13 @@ var collectGroupOverrides = (property, imports, dir, result) => {
16084
16784
  return;
16085
16785
  }
16086
16786
  const name = propName(property.name);
16087
- if (!(name && GROUP_SET.has(name)) || !ts.isObjectLiteralExpression(property.initializer)) {
16787
+ if (!(name && isGroup(name)) || !ts.isObjectLiteralExpression(property.initializer)) {
16088
16788
  return;
16089
16789
  }
16090
- const group = name;
16091
16790
  for (const entry of property.initializer.properties) {
16092
- const normalized = normalizeEntry2(entry, group, imports, dir, result.warnings);
16791
+ const normalized = normalizeEntry2(entry, name, imports, dir, result.warnings);
16093
16792
  if (normalized) {
16094
- result[group].push(normalized);
16793
+ result[name].push(normalized);
16095
16794
  }
16096
16795
  }
16097
16796
  };
@@ -16125,9 +16824,10 @@ import { parseTsconfig } from "get-tsconfig";
16125
16824
  import { dirname as dirname10, join as join22, resolve as resolve7 } from "pathe";
16126
16825
  var CONFIG_DIR_TEMPLATE = "${configDir}";
16127
16826
  var substituteConfigDir = (value, configDir) => value.startsWith(CONFIG_DIR_TEMPLATE) ? join22(configDir, value.slice(CONFIG_DIR_TEMPLATE.length)) : value;
16827
+ var isPathTarget = (target) => typeof target === "string";
16128
16828
  var toAlias = (key, value, baseDir, configDir) => {
16129
16829
  const first = Array.isArray(value) ? value[0] : value;
16130
- if (typeof first !== "string") {
16830
+ if (!isPathTarget(first)) {
16131
16831
  return null;
16132
16832
  }
16133
16833
  const find = key.endsWith("/*") ? key.slice(0, -2) : key;
@@ -16157,14 +16857,14 @@ var resolveTsconfigAliases = (root) => {
16157
16857
  }
16158
16858
  const configDir = dirname10(entry);
16159
16859
  const baseDir = resolve7(configDir, substituteConfigDir(options?.baseUrl ?? ".", configDir));
16160
- const aliases = {};
16860
+ const entries = [];
16161
16861
  for (const [key, value] of Object.entries(paths)) {
16162
16862
  const alias = toAlias(key, value, baseDir, configDir);
16163
16863
  if (alias) {
16164
- aliases[alias.find] = alias.replacement;
16864
+ entries.push([alias.find, alias.replacement]);
16165
16865
  }
16166
16866
  }
16167
- return aliases;
16867
+ return Object.fromEntries(entries);
16168
16868
  };
16169
16869
 
16170
16870
  // src/og/derive.ts
@@ -16172,8 +16872,12 @@ import { existsSync as existsSync14 } from "node:fs";
16172
16872
  import { isAbsolute as isAbsolute7, join as join23 } from "pathe";
16173
16873
  var CARD_WEIGHTS = [400, 600];
16174
16874
  var absoluteSrc = (root, src) => isAbsolute7(src) ? src : join23(root, src);
16875
+ var isNumericWeight = (weight) => typeof weight === "number";
16876
+ var isRangeWeight = (weight) => typeof weight === "string";
16877
+ var isFontName = (value) => typeof value === "string";
16878
+ var isLocalOgFont = (font) => typeof font !== "string" && ("src" in font);
16175
16879
  var googleWeights = (weights) => {
16176
- const numbers = weights.filter((weight) => typeof weight === "number");
16880
+ const numbers = weights.filter(isNumericWeight);
16177
16881
  const used = numbers.filter((weight) => CARD_WEIGHTS.includes(weight));
16178
16882
  if (used.length > 0) {
16179
16883
  return used;
@@ -16182,20 +16886,22 @@ var googleWeights = (weights) => {
16182
16886
  return numbers;
16183
16887
  }
16184
16888
  const [first] = weights;
16185
- return weights.length === 1 && typeof first === "string" ? first : undefined;
16889
+ return weights.length === 1 && isRangeWeight(first) ? first : undefined;
16186
16890
  };
16187
16891
  var googleOgFont = (name, weights) => {
16188
16892
  const weight = googleWeights(weights);
16189
16893
  return weight === undefined ? { name } : { name, weight };
16190
16894
  };
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
- }));
16895
+ var localOgFonts = (font, root) => font.variants.map((variant) => {
16896
+ const entry = {
16897
+ name: font.name,
16898
+ src: absoluteSrc(root, variant.src)
16899
+ };
16900
+ const withWeight = isNumericWeight(variant.weight) ? { ...entry, weight: variant.weight } : entry;
16901
+ return variant.style === "normal" || variant.style === "italic" ? { ...withWeight, style: variant.style } : withWeight;
16902
+ });
16197
16903
  var roleFonts = (value, root) => {
16198
- if (typeof value === "string") {
16904
+ if (isFontName(value)) {
16199
16905
  if (!isFontSlug(value)) {
16200
16906
  return null;
16201
16907
  }
@@ -16205,14 +16911,13 @@ var roleFonts = (value, root) => {
16205
16911
  if ("variants" in value) {
16206
16912
  return localOgFonts(value, root);
16207
16913
  }
16208
- const remote = value;
16209
- if ((remote.provider ?? "google") !== "google") {
16914
+ if ((value.provider ?? "google") !== "google") {
16210
16915
  return null;
16211
16916
  }
16212
- return [googleOgFont(remote.name, remote.weights ?? CARD_WEIGHTS)];
16917
+ return [googleOgFont(value.name, value.weights ?? CARD_WEIGHTS)];
16213
16918
  };
16214
16919
  var roleFamily = (value) => {
16215
- if (typeof value === "string") {
16920
+ if (isFontName(value)) {
16216
16921
  return isFontSlug(value) ? GOOGLE_FONTS[value].family : null;
16217
16922
  }
16218
16923
  return value.name;
@@ -16242,12 +16947,13 @@ var deriveOgFonts = (fonts, root) => {
16242
16947
  }
16243
16948
  }
16244
16949
  }
16245
- return {
16246
- fonts: derived,
16247
- ...families.title || families.body ? { families } : {}
16248
- };
16950
+ const result = { fonts: derived };
16951
+ if (families.title || families.body) {
16952
+ result.families = families;
16953
+ }
16954
+ return result;
16249
16955
  };
16250
- var resolveOgFontSources = (fonts, root) => fonts.map((font) => typeof font !== "string" && ("src" in font) ? { ...font, src: absoluteSrc(root, font.src) } : font);
16956
+ var resolveOgFontSources = (fonts, root) => fonts.map((font) => isLocalOgFont(font) ? { ...font, src: absoluteSrc(root, font.src) } : font);
16251
16957
  var resolveOgFonts = (options, root) => {
16252
16958
  if (options.ogFonts) {
16253
16959
  return { fonts: resolveOgFontSources(options.ogFonts, root) };
@@ -16257,12 +16963,12 @@ var resolveOgFonts = (options, root) => {
16257
16963
  var missingFontFiles = (options, root) => {
16258
16964
  const sources = [];
16259
16965
  for (const value of Object.values(options.themeFonts ?? {})) {
16260
- if (typeof value !== "string" && "variants" in value) {
16966
+ if (!isFontName(value) && "variants" in value) {
16261
16967
  sources.push(...value.variants.map((variant) => absoluteSrc(root, variant.src)));
16262
16968
  }
16263
16969
  }
16264
16970
  for (const font of options.ogFonts) {
16265
- if (typeof font !== "string" && "src" in font) {
16971
+ if (isLocalOgFont(font)) {
16266
16972
  sources.push(absoluteSrc(root, font.src));
16267
16973
  }
16268
16974
  }
@@ -16299,9 +17005,10 @@ var ACCENTS = {
16299
17005
  red: "oklch(0.58 0.22 25)",
16300
17006
  teal: "oklch(0.6 0.12 195)"
16301
17007
  };
17008
+ var isAccentPreset = (value) => Object.hasOwn(ACCENTS, value);
16302
17009
  var CSS_COLOR = /^[\w\s#%.,()/+-]+$/u;
16303
17010
  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);
17011
+ var presetOrColor = (value) => isAccentPreset(value) ? ACCENTS[value] : safeColor(value, FALLBACK_ACCENT);
16305
17012
  var safeColorOrNull = (value) => value && CSS_COLOR.test(value.trim()) ? value.trim() : null;
16306
17013
  var RADII = {
16307
17014
  lg: "0.75rem",
@@ -16457,11 +17164,16 @@ var buildReferenceFiles = async (options) => {
16457
17164
  };
16458
17165
 
16459
17166
  // 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
- }));
17167
+ var resolveSearchPopular = (popular, basePath) => popular.map(({ href, icon, label }) => {
17168
+ const page = {
17169
+ label,
17170
+ route: withBasePath(basePath, href)
17171
+ };
17172
+ if (icon) {
17173
+ page.icon = icon;
17174
+ }
17175
+ return page;
17176
+ });
16465
17177
 
16466
17178
  // src/theme/entry.ts
16467
17179
  var DARK_VARIANT = `/* Dark mode is driven by data-theme on the <html> element. */
@@ -17423,12 +18135,13 @@ var VALID_MODES = new Set([
17423
18135
  "only",
17424
18136
  "visible"
17425
18137
  ]);
17426
- var FRAMEWORK_BY_EXT2 = {
17427
- jsx: "react",
17428
- svelte: "svelte",
17429
- tsx: "react",
17430
- vue: "vue"
17431
- };
18138
+ var isClientMode = (mode) => VALID_MODES.has(mode);
18139
+ var FRAMEWORK_BY_EXT2 = new Map([
18140
+ ["jsx", "react"],
18141
+ ["svelte", "svelte"],
18142
+ ["tsx", "react"],
18143
+ ["vue", "vue"]
18144
+ ]);
17432
18145
  var ISLAND_FILE = /\.(?<ext>jsx|svelte|tsx|vue)$/u;
17433
18146
  var CLIENT_EXPORT = /export\s+const\s+client\s*(?::[^=]+)?=\s*["'](?<mode>\w+)["']/u;
17434
18147
  var readClientMode = (source, file, warnings) => {
@@ -17436,7 +18149,7 @@ var readClientMode = (source, file, warnings) => {
17436
18149
  if (!mode) {
17437
18150
  return DEFAULT_CLIENT;
17438
18151
  }
17439
- if (!VALID_MODES.has(mode)) {
18152
+ if (!isClientMode(mode)) {
17440
18153
  warnings.push(`Island "${file}" declares an unknown client mode "${mode}"; defaulting to "${DEFAULT_CLIENT}". Use "load", "idle", "visible", or "only".`);
17441
18154
  return DEFAULT_CLIENT;
17442
18155
  }
@@ -17459,7 +18172,7 @@ var discoverIslands = async (root) => {
17459
18172
  const collectIsland = (file, source) => {
17460
18173
  const base = basename2(file);
17461
18174
  const ext = base.match(ISLAND_FILE)?.groups?.ext;
17462
- const framework = ext ? FRAMEWORK_BY_EXT2[ext] : undefined;
18175
+ const framework = ext ? FRAMEWORK_BY_EXT2.get(ext) : undefined;
17463
18176
  if (!framework) {
17464
18177
  return;
17465
18178
  }
@@ -17691,18 +18404,18 @@ var ensureDepsLink = async (outDir, pkgDir = packageRoot()) => {
17691
18404
  }
17692
18405
  return astroConflictWarning(blumeAstro, outDirHit?.pkg ?? null);
17693
18406
  };
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
- };
18407
+ var ISLAND_FRAMEWORK_DEPS = new Map([
18408
+ ["svelte", "@astrojs/svelte"],
18409
+ ["vue", "@astrojs/vue"]
18410
+ ]);
18411
+ var DEPLOYMENT_ADAPTER_DEPS = new Map([
18412
+ ["cloudflare", "@astrojs/cloudflare"],
18413
+ ["netlify", "@astrojs/netlify"]
18414
+ ]);
17702
18415
  var islandFrameworkWarnings = (frameworks, root) => {
17703
18416
  const warnings = [];
17704
18417
  for (const framework of frameworks) {
17705
- const dep = ISLAND_FRAMEWORK_DEPS[framework];
18418
+ const dep = ISLAND_FRAMEWORK_DEPS.get(framework);
17706
18419
  if (dep && !canResolveFrom(root, dep)) {
17707
18420
  warnings.push(`Islands use ${framework}, which needs "${dep}". Install it (e.g. \`npm install ${dep} ${framework}\`).`);
17708
18421
  }
@@ -17710,7 +18423,7 @@ var islandFrameworkWarnings = (frameworks, root) => {
17710
18423
  return warnings;
17711
18424
  };
17712
18425
  var deploymentAdapterWarnings = (deployment, root) => {
17713
- const dep = deployment.output === "server" && deployment.adapter ? DEPLOYMENT_ADAPTER_DEPS[deployment.adapter] : undefined;
18426
+ const dep = deployment.output === "server" && deployment.adapter ? DEPLOYMENT_ADAPTER_DEPS.get(deployment.adapter) : undefined;
17714
18427
  if (dep && !(canResolveFrom(root, dep) || canResolveFrom(packageRoot(), dep))) {
17715
18428
  return [
17716
18429
  `Deployment adapter "${deployment.adapter}" needs "${dep}", which isn't installed. Run \`npm install ${dep}\` (or your package manager's equivalent).`
@@ -17862,14 +18575,15 @@ var readLogoSvg = (project, source) => {
17862
18575
  ].find((path) => existsSync16(path));
17863
18576
  return file ? readFileSync8(file, "utf-8") : undefined;
17864
18577
  };
18578
+ var isStringShorthand = (value) => typeof value === "string";
17865
18579
  var resolveLogo = (project) => {
17866
18580
  const { logo } = project.config;
17867
18581
  if (!logo) {
17868
18582
  return null;
17869
18583
  }
17870
- const config = typeof logo === "string" ? { image: logo } : logo;
18584
+ const config = isStringShorthand(logo) ? { image: logo } : logo;
17871
18585
  const { href, image: source, text } = config;
17872
- const image = typeof source === "string" ? { light: source } : source;
18586
+ const image = isStringShorthand(source) ? { light: source } : source;
17873
18587
  const light = image?.light ?? image?.dark;
17874
18588
  const dark = image?.dark ?? image?.light;
17875
18589
  const alt = image?.alt ?? "";
@@ -17892,16 +18606,16 @@ var FAVICON_CANDIDATES = [
17892
18606
  "favicon.ico",
17893
18607
  "icon.ico"
17894
18608
  ];
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
- };
18609
+ var FAVICON_TYPES = new Map([
18610
+ ["ico", "image/x-icon"],
18611
+ ["jpeg", "image/jpeg"],
18612
+ ["jpg", "image/jpeg"],
18613
+ ["png", "image/png"],
18614
+ ["svg", "image/svg+xml"]
18615
+ ]);
17902
18616
  var faviconType = (name) => {
17903
18617
  const ext = name.split(".").pop()?.toLowerCase();
17904
- return ext ? FAVICON_TYPES[ext] : undefined;
18618
+ return ext ? FAVICON_TYPES.get(ext) : undefined;
17905
18619
  };
17906
18620
  var inlineDataUri = (file, type) => `data:${type};base64,${readFileSync8(file).toString("base64")}`;
17907
18621
  var defaultFavicon = () => ({
@@ -17937,7 +18651,7 @@ var resolveBanner = (config) => {
17937
18651
  if (!banner) {
17938
18652
  return null;
17939
18653
  }
17940
- if (typeof banner === "string") {
18654
+ if (isStringShorthand(banner)) {
17941
18655
  return { content: banner, dismissible: false, key: banner };
17942
18656
  }
17943
18657
  return {
@@ -18068,6 +18782,7 @@ var buildRuntimeData = (project) => {
18068
18782
  theme: config.theme,
18069
18783
  title: config.title,
18070
18784
  toc: config.toc,
18785
+ versions: config.versions ?? null,
18071
18786
  webmcp: {
18072
18787
  enabled: config.ai.webmcp,
18073
18788
  llms: config.ai.llmsTxt.enabled
@@ -18081,6 +18796,13 @@ var buildRuntimeData = (project) => {
18081
18796
  fontCssVars: configuredCssVars(config.theme.fonts),
18082
18797
  navigation: withRepoUrl(graph.navigation),
18083
18798
  navigationByLocale,
18799
+ navigationByVersion: Object.fromEntries(Object.entries(graph.navigationByVersion).map(([id2, byLocale]) => [
18800
+ id2,
18801
+ Object.fromEntries(Object.entries(byLocale).map(([code, nav]) => [
18802
+ code,
18803
+ withRepoUrl(nav)
18804
+ ]))
18805
+ ])),
18084
18806
  routes: manifest.routes.map((route) => ({
18085
18807
  alternates: route.alternates,
18086
18808
  collection: route.collection,
@@ -18094,7 +18816,9 @@ var buildRuntimeData = (project) => {
18094
18816
  lastModified: route.lastModified ?? null,
18095
18817
  locale: route.locale,
18096
18818
  path: route.path,
18097
- title: route.title
18819
+ title: route.title,
18820
+ version: route.version,
18821
+ versionAlternates: route.versionAlternates
18098
18822
  })),
18099
18823
  ui: defaultUi,
18100
18824
  uiByLocale
@@ -18530,6 +19254,7 @@ var prepareProject = async (options) => {
18530
19254
 
18531
19255
  // src/cli/commands/build.ts
18532
19256
  var ADAPTERS = ["vercel", "node", "netlify", "cloudflare"];
19257
+ var isAdapter = (value) => ADAPTERS.some((adapter) => adapter === value);
18533
19258
  var BUDGET_JS = "budget-js";
18534
19259
  var BUDGET_CSS = "budget-css";
18535
19260
  var validateBudgetFlags = (args) => {
@@ -18626,10 +19351,13 @@ var emitVercelNegotiation = async (project, routePaths, root) => {
18626
19351
  if (!existsSync18(configPath)) {
18627
19352
  return;
18628
19353
  }
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
- };
19354
+ const overrides = {};
19355
+ if (hasApiCatalog(config2)) {
19356
+ overrides[API_CATALOG_PATH.slice(1)] = API_CATALOG_TYPE;
19357
+ }
19358
+ if (config2.ai.webBotAuth.keys.length > 0) {
19359
+ overrides[SIGNATURES_DIRECTORY_PATH.slice(1)] = SIGNATURES_DIRECTORY_TYPE;
19360
+ }
18633
19361
  const rawMarkdown = await buildRawMarkdown(project);
18634
19362
  const home = rawMarkdown["/"];
18635
19363
  const injected = injectNegotiationRoutes(await readFile17(configPath, "utf-8"), routePaths, buildHomeLinkHeader(config2, routePaths), overrides, home ? markdownTokenCount(agentMarkdown(home)) : undefined);
@@ -18883,7 +19611,7 @@ var buildCommand = defineCommand3({
18883
19611
  logger.error(`Invalid --output "${args.output}" (use static | server).`);
18884
19612
  process.exit(1);
18885
19613
  }
18886
- if (args.adapter && !ADAPTERS.includes(args.adapter)) {
19614
+ if (args.adapter && !isAdapter(args.adapter)) {
18887
19615
  logger.error(`Invalid --adapter "${args.adapter}" (use ${ADAPTERS.join(" | ")}).`);
18888
19616
  process.exit(1);
18889
19617
  }
@@ -19295,7 +20023,7 @@ var askFiles = async (project, srcDir, genDir) => {
19295
20023
  const grounded = ask.provider !== "inkeep";
19296
20024
  const files = [
19297
20025
  {
19298
- content: askEndpointTemplate(resolveAskBackend(ask), grounded),
20026
+ content: askEndpointTemplate(resolveAskBackend(ask), grounded, ask.instructions),
19299
20027
  path: join33(srcDir, "pages", "api", "ask.ts")
19300
20028
  }
19301
20029
  ];
@@ -19794,14 +20522,15 @@ var commandsFor = (pm) => ({
19794
20522
  exec: { bun: "bunx", npm: "npx", pnpm: "pnpm exec", yarn: "yarn" }[pm],
19795
20523
  install: `${pm} install`
19796
20524
  });
20525
+ var isPackageManager = (value) => PACKAGE_MANAGERS.some((pm) => pm === value);
19797
20526
  var detectPackageManager = (userAgent) => {
19798
20527
  const name = userAgent?.split("/")[0];
19799
- return name !== undefined && PACKAGE_MANAGERS.includes(name) ? name : "npm";
20528
+ return name !== undefined && isPackageManager(name) ? name : "npm";
19800
20529
  };
19801
20530
  var detectProjectPackageManager = async (root) => {
19802
20531
  const detected = await detect({ cwd: root });
19803
20532
  const name = detected?.name;
19804
- return name !== undefined && PACKAGE_MANAGERS.includes(name) ? name : detectPackageManager(process.env.npm_config_user_agent);
20533
+ return name !== undefined && isPackageManager(name) ? name : detectPackageManager(process.env.npm_config_user_agent);
19805
20534
  };
19806
20535
  var validateContentDir = (root, dir) => isAbsolute9(dir) || relative16(root, join35(root, dir)).startsWith("..") ? "Must be a relative path inside the project." : undefined;
19807
20536
  var titleize = (raw) => {
@@ -19865,10 +20594,16 @@ export default defineConfig({
19865
20594
  description: "Documentation powered by Blume.",${STARTERS[answers.template].configExtra}${contentBlockFor(answers)}
19866
20595
  });
19867
20596
  `;
19868
- var extraDepsFor = (sources) => ({
19869
- ...sources.includes("notion") && { "@notionhq/client": "^2.2.15" },
19870
- ...sources.includes("sanity") && { "@sanity/client": "^7.25.0" }
19871
- });
20597
+ var extraDepsFor = (sources) => {
20598
+ const deps = {};
20599
+ if (sources.includes("notion")) {
20600
+ deps["@notionhq/client"] = "^2.2.15";
20601
+ }
20602
+ if (sources.includes("sanity")) {
20603
+ deps["@sanity/client"] = "^7.25.0";
20604
+ }
20605
+ return deps;
20606
+ };
19872
20607
  var buildPlan = (root, answers) => {
19873
20608
  const files = [
19874
20609
  {
@@ -20166,7 +20901,7 @@ import { join as join38 } from "pathe";
20166
20901
  import { readFile as readFile20, writeFile as writeFile12 } from "node:fs/promises";
20167
20902
  import spawn2 from "cross-spawn";
20168
20903
  import { join as join37 } from "pathe";
20169
- import { z as z3 } from "zod";
20904
+ import { z as z4 } from "zod";
20170
20905
  var KILL_GRACE_MS = 5000;
20171
20906
  var MCP_TOOL_NAMES = [
20172
20907
  "search_docs",
@@ -20292,10 +21027,10 @@ var codexArgs = (context) => {
20292
21027
  return [...base, "-"];
20293
21028
  };
20294
21029
  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()
21030
+ var claudeResultSchema = z4.object({
21031
+ is_error: z4.boolean().default(false),
21032
+ result: z4.string().default(""),
21033
+ total_cost_usd: z4.number().optional()
20299
21034
  });
20300
21035
  var tail = (value, max = 300) => {
20301
21036
  const trimmed = value.trim();
@@ -20349,11 +21084,11 @@ var readAgentOutput = async (kind, result, lastMessagePath) => {
20349
21084
  }
20350
21085
  return { isError: false, text };
20351
21086
  };
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()
21087
+ var verdictSchema = z4.object({
21088
+ missing: z4.array(z4.string()).default([]),
21089
+ notes: z4.string().default(""),
21090
+ pass: z4.boolean(),
21091
+ score: z4.number().min(0).max(1).optional()
20357
21092
  });
20358
21093
  var parseVerdict = (text) => {
20359
21094
  const start = text.indexOf("{");
@@ -20374,26 +21109,26 @@ var parseVerdict = (text) => {
20374
21109
  // src/eval/schema.ts
20375
21110
  import { readFile as readFile21 } from "node:fs/promises";
20376
21111
  import { load as load2 } from "js-yaml";
20377
- import { z as z4 } from "zod";
21112
+ import { z as z5 } from "zod";
20378
21113
  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)
21114
+ var questionSchema = z5.strictObject({
21115
+ expected: z5.array(z5.string().min(1)).min(1, "expected must list at least one fact"),
21116
+ id: z5.string().regex(ID_PATTERN, "id must be a kebab-case slug (a-z, 0-9, dashes)"),
21117
+ question: z5.string().min(1),
21118
+ routes: z5.union([z5.string(), z5.array(z5.string())]).default([]).transform((value) => Array.isArray(value) ? value : [value]),
21119
+ severity: z5.enum(["error", "warning"]).default("error"),
21120
+ skip: z5.boolean().default(false)
20386
21121
  });
20387
- var fullSchema = z4.strictObject({
20388
- questions: z4.array(questionSchema).min(1),
20389
- version: z4.literal(1).default(1)
21122
+ var fullSchema = z5.strictObject({
21123
+ questions: z5.array(questionSchema).min(1),
21124
+ version: z5.literal(1).default(1)
20390
21125
  });
20391
21126
  var evalsFileSchema = fullSchema.superRefine((value, context) => {
20392
21127
  const seen = new Set;
20393
21128
  for (const question of value.questions) {
20394
21129
  if (seen.has(question.id)) {
20395
21130
  context.addIssue({
20396
- code: z4.ZodIssueCode.custom,
21131
+ code: z5.ZodIssueCode.custom,
20397
21132
  message: `duplicate question id "${question.id}"`,
20398
21133
  path: ["questions"]
20399
21134
  });
@@ -20986,13 +21721,13 @@ var initCommand = defineCommand9({
20986
21721
  },
20987
21722
  async run({ args }) {
20988
21723
  const cwd = process.cwd();
20989
- const template = args.template;
20990
- if (template !== undefined && !TEMPLATES.includes(template)) {
21724
+ const template = TEMPLATES.find((candidate) => candidate === args.template);
21725
+ if (args.template !== undefined && template === undefined) {
20991
21726
  logger.error(`Unknown template "${args.template}" (use ${TEMPLATES.join(" | ")}).`);
20992
21727
  process.exit(1);
20993
21728
  }
20994
- const pm = args["package-manager"];
20995
- if (pm !== undefined && !PACKAGE_MANAGERS.includes(pm)) {
21729
+ const pm = PACKAGE_MANAGERS.find((candidate) => candidate === args["package-manager"]);
21730
+ if (args["package-manager"] !== undefined && pm === undefined) {
20996
21731
  logger.error(`Unknown package manager "${args["package-manager"]}" (use ${PACKAGE_MANAGERS.join(" | ")}).`);
20997
21732
  process.exit(1);
20998
21733
  }
@@ -21065,7 +21800,7 @@ import {
21065
21800
  CallToolRequestSchema,
21066
21801
  ListToolsRequestSchema
21067
21802
  } from "@modelcontextprotocol/sdk/types.js";
21068
- import { z as z5 } from "zod";
21803
+ import { z as z6 } from "zod";
21069
21804
 
21070
21805
  // src/search/orama-index.ts
21071
21806
  import { create, insertMultiple, search } from "@orama/orama";
@@ -21076,7 +21811,8 @@ var SCHEMA = {
21076
21811
  facetTerms: "enum[]",
21077
21812
  locale: "enum",
21078
21813
  route: "string",
21079
- title: "string"
21814
+ title: "string",
21815
+ version: "enum"
21080
21816
  };
21081
21817
  var toFacetTerms = (facets) => Object.entries(facets).map(([key, value]) => `${key}:${value}`);
21082
21818
  var BOOST = { description: 2, title: 3 };
@@ -21098,12 +21834,13 @@ var addBigrams = (run, tokens) => {
21098
21834
  previous = character;
21099
21835
  }
21100
21836
  };
21837
+ var hasSegmenter = (segmenter) => typeof segmenter === "function";
21101
21838
  var segmentingTokenizer = (locale) => {
21102
21839
  const language = locale?.toLowerCase().split(/[-_]/u)[0] ?? "";
21103
21840
  if (!SEGMENTED_LANGUAGES.has(language)) {
21104
21841
  return;
21105
21842
  }
21106
- if (typeof Intl.Segmenter !== "function") {
21843
+ if (!hasSegmenter(Intl.Segmenter)) {
21107
21844
  return;
21108
21845
  }
21109
21846
  const segmenter = new Intl.Segmenter(language, { granularity: "word" });
@@ -21152,28 +21889,33 @@ var segmentingTokenizer = (locale) => {
21152
21889
  };
21153
21890
  var buildOramaIndex = async (documents, locale) => {
21154
21891
  const tokenizer = segmentingTokenizer(locale);
21155
- const db = create({
21156
- schema: SCHEMA,
21157
- ...tokenizer ? { components: { tokenizer } } : {}
21158
- });
21892
+ const db = tokenizer ? create({ components: { tokenizer }, schema: SCHEMA }) : create({ schema: SCHEMA });
21159
21893
  await insertMultiple(db, documents.map((doc) => doc.facets ? { ...doc, facetTerms: toFacetTerms(doc.facets) } : doc));
21160
21894
  return db;
21161
21895
  };
21162
21896
  var ALL_TOKENS = 0;
21163
21897
  var queryOramaIndex = async (db, term, limit, filters) => {
21164
21898
  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 = {
21899
+ const where = {};
21900
+ if (filters?.locale) {
21901
+ where.locale = { eq: filters.locale };
21902
+ }
21903
+ if (filters?.version !== undefined) {
21904
+ where.version = { eq: filters.version };
21905
+ }
21906
+ if (filters?.contentTypes && filters.contentTypes.length > 0) {
21907
+ where.contentType = { in: filters.contentTypes };
21908
+ }
21909
+ if (facetTerms.length > 0) {
21910
+ where.facetTerms = { containsAll: facetTerms };
21911
+ }
21912
+ const unfiltered = {
21171
21913
  boost: BOOST,
21172
21914
  limit,
21173
21915
  properties: ["title", "description", "content"],
21174
- term,
21175
- ...Object.keys(where).length > 0 ? { where } : {}
21916
+ term
21176
21917
  };
21918
+ const params = Object.keys(where).length > 0 ? { ...unfiltered, where } : unfiltered;
21177
21919
  const bigrammed = BIGRAM_LANGUAGES.has(db.tokenizer?.language ?? "");
21178
21920
  const strict = bigrammed ? await search(db, { ...params, threshold: ALL_TOKENS }) : undefined;
21179
21921
  const found = strict && strict.hits.length > 0 ? strict : await search(db, params);
@@ -21184,36 +21926,55 @@ var queryOramaIndex = async (db, term, limit, filters) => {
21184
21926
  var DEFAULT_SEARCH_LIMIT = 8;
21185
21927
  var MAX_SEARCH_LIMIT = 20;
21186
21928
  var EXCERPT_LENGTH = 200;
21187
- var contentTypesField = z5.preprocess((value) => {
21929
+ var contentTypesField = z6.preprocess((value) => {
21188
21930
  const list2 = (Array.isArray(value) ? value : [value]).filter((entry) => typeof entry === "string");
21189
21931
  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)) {
21932
+ }, 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.'));
21933
+ var looseFacetObject = z6.record(z6.string(), z6.unknown());
21934
+ var filtersField = z6.preprocess((value) => {
21935
+ const candidate = looseFacetObject.safeParse(value);
21936
+ if (!candidate.success) {
21193
21937
  return;
21194
21938
  }
21195
- const entries = Object.entries(value).filter((entry) => typeof entry[1] === "string");
21939
+ const entries = Object.entries(candidate.data).filter((entry) => typeof entry[1] === "string");
21196
21940
  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);
21941
+ }, 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.'));
21942
+ var limitField = z6.preprocess((value) => {
21943
+ const num = Number(value);
21200
21944
  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));
21945
+ }, z6.int().min(1).max(MAX_SEARCH_LIMIT).optional().describe(`Maximum hits to return (default ${DEFAULT_SEARCH_LIMIT}).`));
21946
+ var textField = (description) => z6.preprocess((value) => {
21947
+ const parsed = z6.string().safeParse(value);
21948
+ return parsed.success ? parsed.data : "";
21949
+ }, z6.string().describe(description));
21950
+ var optionalTextField = (description) => z6.preprocess((value) => {
21951
+ const parsed = z6.string().safeParse(value);
21952
+ const trimmed = parsed.success ? parsed.data.trim() : "";
21953
+ return trimmed || undefined;
21954
+ }, z6.string().optional().describe(description));
21955
+ var localeField = optionalTextField("Only include pages in this locale (e.g. `fr`). Omit for every language.");
21956
+ 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
21957
  var TOOL_INPUTS = {
21204
- get_navigation: z5.object({}),
21205
- get_page: z5.object({
21958
+ get_navigation: z6.object({
21959
+ locale: optionalTextField("Locale whose navigation tree to return (defaults to the default locale)."),
21960
+ version: optionalTextField("Archived version id whose tree to return (defaults to the current docs).")
21961
+ }),
21962
+ get_page: z6.object({
21206
21963
  route: textField("The page route, e.g. `/guides/install`.")
21207
21964
  }),
21208
- list_pages: z5.object({
21965
+ list_pages: z6.object({
21209
21966
  contentTypes: contentTypesField,
21210
- filters: filtersField
21967
+ filters: filtersField,
21968
+ locale: localeField,
21969
+ version: versionField
21211
21970
  }),
21212
- search_docs: z5.object({
21971
+ search_docs: z6.object({
21213
21972
  contentTypes: contentTypesField,
21214
21973
  filters: filtersField,
21215
21974
  limit: limitField,
21216
- query: textField("The search query.")
21975
+ locale: localeField,
21976
+ query: textField("The search query."),
21977
+ version: versionField
21217
21978
  })
21218
21979
  };
21219
21980
  var inputSchemaFor = (schema) => {
@@ -21221,7 +21982,7 @@ var inputSchemaFor = (schema) => {
21221
21982
  $schema: _dialect,
21222
21983
  additionalProperties: _closed,
21223
21984
  ...rest
21224
- } = z5.toJSONSchema(schema);
21985
+ } = z6.toJSONSchema(schema);
21225
21986
  return rest;
21226
21987
  };
21227
21988
  var TOOL_DEFINITIONS = MCP_TOOLS.map((tool) => ({
@@ -21232,6 +21993,19 @@ var TOOL_DEFINITIONS = MCP_TOOLS.map((tool) => ({
21232
21993
  title: tool.title
21233
21994
  }));
21234
21995
  var matchesFacets = (facets, filters) => Object.entries(filters).every(([key, value]) => facets?.[key] === value);
21996
+ var asVersionScope = (value, data) => {
21997
+ if (!data.archivedVersions) {
21998
+ return;
21999
+ }
22000
+ if (value === "all") {
22001
+ return;
22002
+ }
22003
+ if (value === undefined || value === "latest" || value === "current") {
22004
+ return "";
22005
+ }
22006
+ return value;
22007
+ };
22008
+ var unknownVersionError = (versionId, data) => versionId && data.archivedVersions && !data.archivedVersions.includes(versionId) ? `Unknown version "${versionId}". Archived versions: ${data.archivedVersions.join(", ")}.` : null;
21235
22009
  var normalizeRoute2 = (input, data) => {
21236
22010
  let value = input.trim();
21237
22011
  if (/^https?:\/\//iu.test(value)) {
@@ -21256,10 +22030,10 @@ var excerptFor = (doc) => {
21256
22030
  const head = doc.content.slice(0, EXCERPT_LENGTH).trim();
21257
22031
  return doc.content.length > EXCERPT_LENGTH ? `${head}…` : head;
21258
22032
  };
21259
- var text = (value, isError = false) => ({
21260
- content: [{ text: value, type: "text" }],
21261
- ...isError ? { isError: true } : {}
21262
- });
22033
+ var text = (value, isError = false) => {
22034
+ const content = [{ text: value, type: "text" }];
22035
+ return isError ? { content, isError: true } : { content };
22036
+ };
21263
22037
  var createIndexProvider = (documents, locale) => {
21264
22038
  let dbPromise = null;
21265
22039
  return function provideIndex() {
@@ -21268,10 +22042,8 @@ var createIndexProvider = (documents, locale) => {
21268
22042
  };
21269
22043
  };
21270
22044
  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
- });
22045
+ const serverOptions = data.instructions ? { capabilities: { tools: {} }, instructions: data.instructions } : { capabilities: { tools: {} } };
22046
+ const server = new Server({ name: data.name, version: data.version }, serverOptions);
21275
22047
  server.setRequestHandler(ListToolsRequestSchema, () => ({
21276
22048
  tools: TOOL_DEFINITIONS
21277
22049
  }));
@@ -21282,16 +22054,24 @@ var buildServer = (data, index) => {
21282
22054
  const db = await index();
21283
22055
  const hits = await queryOramaIndex(db, input.query, input.limit ?? DEFAULT_SEARCH_LIMIT, {
21284
22056
  contentTypes: input.contentTypes,
21285
- facets: input.filters
22057
+ facets: input.filters,
22058
+ locale: input.locale,
22059
+ version: asVersionScope(input.version, data)
22060
+ });
22061
+ const results = hits.map((doc) => {
22062
+ const hit = {
22063
+ contentType: doc.contentType,
22064
+ excerpt: excerptFor(doc),
22065
+ facets: doc.facets,
22066
+ route: doc.route,
22067
+ title: doc.title,
22068
+ url: urlFor(doc.route, data)
22069
+ };
22070
+ if (data.archivedVersions) {
22071
+ hit.version = doc.version ?? "";
22072
+ }
22073
+ return hit;
21286
22074
  });
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
22075
  return text(JSON.stringify(results, null, 2));
21296
22076
  }
21297
22077
  if (name === "get_page") {
@@ -21304,20 +22084,40 @@ var buildServer = (data, index) => {
21304
22084
  return text(markdown);
21305
22085
  }
21306
22086
  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));
22087
+ const input = TOOL_INPUTS.list_pages.parse(args);
22088
+ const { contentTypes, filters, locale } = input;
22089
+ const versionScope = asVersionScope(input.version, data);
22090
+ 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));
22091
+ return text(JSON.stringify(routes.map((route) => {
22092
+ const listing = {
22093
+ contentType: route.contentType,
22094
+ description: route.description,
22095
+ facets: route.facets,
22096
+ lastModified: route.lastModified,
22097
+ route: route.route,
22098
+ title: route.title,
22099
+ url: urlFor(route.route, data)
22100
+ };
22101
+ if (data.archivedVersions) {
22102
+ listing.version = route.version;
22103
+ }
22104
+ return listing;
22105
+ }), null, 2));
21318
22106
  }
21319
22107
  if (name === "get_navigation") {
21320
- return text(JSON.stringify(data.navigation, null, 2));
22108
+ const { locale, version: versionId } = TOOL_INPUTS.get_navigation.parse(args);
22109
+ const unknownVersion = unknownVersionError(versionId, data);
22110
+ if (unknownVersion) {
22111
+ return text(unknownVersion, true);
22112
+ }
22113
+ let { navigation } = data;
22114
+ const byLocale = versionId ? data.navigationByVersion?.[versionId] : undefined;
22115
+ if (byLocale) {
22116
+ navigation = (locale ? byLocale[locale] : undefined) ?? byLocale[data.defaultLocale ?? ""] ?? Object.values(byLocale)[0] ?? navigation;
22117
+ } else if (locale && data.navigationByLocale?.[locale]) {
22118
+ navigation = data.navigationByLocale[locale];
22119
+ }
22120
+ return text(JSON.stringify(navigation, null, 2));
21321
22121
  }
21322
22122
  return text(`Unknown tool: ${name}`, true);
21323
22123
  });
@@ -21469,11 +22269,11 @@ var translateAgentArgs = (kind, lastMessagePath) => kind === "claude" ? claudeAr
21469
22269
  import { createHash as createHash5 } from "node:crypto";
21470
22270
  import { readFile as readFile23 } from "node:fs/promises";
21471
22271
  import { join as join42 } from "pathe";
21472
- import { z as z6 } from "zod";
22272
+ import { z as z7 } from "zod";
21473
22273
  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)
22274
+ var ledgerSchema = z7.object({
22275
+ files: z7.record(z7.string(), z7.record(z7.string(), z7.string())),
22276
+ version: z7.literal(1)
21477
22277
  });
21478
22278
  var emptyLedger = () => ({
21479
22279
  files: {},
@@ -21743,6 +22543,7 @@ import { readFile as readFile24 } from "node:fs/promises";
21743
22543
  import { dirname as dirname15, join as join43, relative as relative19 } from "pathe";
21744
22544
  import { glob as glob8 } from "tinyglobby";
21745
22545
  var META_FILES2 = ["**/meta.ts", "**/meta.js", "**/meta.mjs"];
22546
+ var isFactoryModule = (value) => typeof value === "function";
21746
22547
  var metaTargetPath = (meta, locale) => join43(meta.contentRoot, locale, meta.dir, "meta.ts");
21747
22548
  var discoverTranslatableMeta = async (project) => {
21748
22549
  const { i18n } = project.config;
@@ -21771,7 +22572,7 @@ var discoverTranslatableMeta = async (project) => {
21771
22572
  readFile24(file, "utf-8"),
21772
22573
  load3(file)
21773
22574
  ]);
21774
- if (typeof mod === "function") {
22575
+ if (isFactoryModule(mod)) {
21775
22576
  diagnostics.push({
21776
22577
  code: "BLUME_TRANSLATE_META_FACTORY",
21777
22578
  file,
@@ -21803,7 +22604,7 @@ var generateMetaModule = (meta, translatedTitle) => {
21803
22604
  ...meta,
21804
22605
  title: translatedTitle
21805
22606
  };
21806
- const lines = Object.keys(data).toSorted().filter((key) => data[key] !== undefined).map((key) => ` ${key}: ${JSON.stringify(data[key])},`);
22607
+ const lines = Object.entries(data).toSorted(([a], [b]) => a < b ? -1 : 1).filter(([, value]) => value !== undefined).map(([key, value]) => ` ${key}: ${JSON.stringify(value)},`);
21807
22608
  return [
21808
22609
  "// Generated by `blume translate` — edit the default locale's meta file",
21809
22610
  "// and rerun the translation instead of editing this copy.",
@@ -21863,10 +22664,12 @@ var stripOuterFence = (text2) => {
21863
22664
  };
21864
22665
  var countFenceLines = (text2) => text2.split(`
21865
22666
  `).filter((line) => FENCE_LINE.test(line)).length;
22667
+ var isKeyedObject = (value) => typeof value === "object" && value !== null;
22668
+ var isString7 = (value) => typeof value === "string";
21866
22669
  var getPath2 = (data, path) => {
21867
22670
  let value = data;
21868
22671
  for (const key of path) {
21869
- if (typeof value !== "object" || value === null) {
22672
+ if (!isKeyedObject(value)) {
21870
22673
  return;
21871
22674
  }
21872
22675
  value = value[key];
@@ -21921,7 +22724,7 @@ var validateTranslation = (sourceText, agentText) => {
21921
22724
  for (const path of TRANSLATABLE_KEY_PATHS) {
21922
22725
  const original = getPath2(source.data, path);
21923
22726
  const translated = getPath2(parsed.data, path);
21924
- if (typeof original === "string" && typeof translated === "string" && translated.trim() !== "") {
22727
+ if (isString7(original) && isString7(translated) && translated.trim() !== "") {
21925
22728
  setPath(data, path, translated);
21926
22729
  }
21927
22730
  }
@@ -21941,12 +22744,12 @@ var parseMetaTitles = (agentText, expectedKeys) => {
21941
22744
  parsed = undefined;
21942
22745
  }
21943
22746
  }
21944
- const record = typeof parsed === "object" && parsed !== null ? parsed : {};
22747
+ const record = isKeyedObject(parsed) ? parsed : {};
21945
22748
  const titles = {};
21946
22749
  const missing = [];
21947
22750
  for (const key of expectedKeys) {
21948
22751
  const value = record[key];
21949
- if (typeof value === "string" && value.trim() !== "") {
22752
+ if (isString7(value) && value.trim() !== "") {
21950
22753
  titles[key] = value;
21951
22754
  } else {
21952
22755
  missing.push(key);
@@ -22103,7 +22906,7 @@ var translatablePages = (project, i18n) => {
22103
22906
  const seen = new Set;
22104
22907
  const universe = [];
22105
22908
  for (const page2 of project.graph.pages) {
22106
- if (page2.locale !== i18n.defaultLocale || page2.fallback || !page2.sourcePath || seen.has(page2.sourcePath)) {
22909
+ if (page2.locale !== i18n.defaultLocale || page2.fallback || !page2.sourcePath || seen.has(page2.sourcePath) || page2.version !== "") {
22107
22910
  continue;
22108
22911
  }
22109
22912
  const ext = extname8(page2.sourcePath);
@@ -22673,8 +23476,249 @@ var validateCommand = defineCommand14({
22673
23476
  }
22674
23477
  });
22675
23478
 
23479
+ // src/cli/commands/version.ts
23480
+ import { defineCommand as defineCommand15 } from "citty";
23481
+
23482
+ // src/core/version-cut.ts
23483
+ import { existsSync as existsSync28 } from "node:fs";
23484
+ import { cp as cp3, readdir as readdir3, readFile as readFile27, rm as rm5 } from "node:fs/promises";
23485
+ import { join as join48, relative as relative21 } from "pathe";
23486
+ class CutError extends Error {
23487
+ constructor(message) {
23488
+ super(message);
23489
+ this.name = "CutError";
23490
+ }
23491
+ }
23492
+ var INLINE_CODE2 = /`[^`]*`/gu;
23493
+ var ROOT_LINK = /(?<prefix>\]\(|href="|src=")(?<target>\/[^\s"')]*)/gu;
23494
+ var buildRouteRewrites = (project, id2) => {
23495
+ const { basePath, i18n } = project.config;
23496
+ const { contentRoot: contentRoot2 } = project.context;
23497
+ const rewrites = new Map;
23498
+ for (const page2 of project.graph.pages) {
23499
+ const { sourcePath } = page2;
23500
+ if (page2.version !== "" || !sourcePath || relative21(contentRoot2, sourcePath).startsWith("..")) {
23501
+ continue;
23502
+ }
23503
+ const logical = versionizeRoute(page2.versionKey, id2);
23504
+ rewrites.set(stripBasePath(basePath, page2.route), i18n ? localizeRoute(logical, page2.locale, i18n) : logical);
23505
+ }
23506
+ return rewrites;
23507
+ };
23508
+ var rewriteLine2 = (line, rewrites) => {
23509
+ const masked = line.replaceAll(INLINE_CODE2, (span) => " ".repeat(span.length));
23510
+ let out = "";
23511
+ let cursor = 0;
23512
+ let count = 0;
23513
+ for (const match of masked.matchAll(ROOT_LINK)) {
23514
+ const target = match.groups?.target ?? "";
23515
+ const hash = target.search(/[#?]/u);
23516
+ const path = hash === -1 ? target : target.slice(0, hash);
23517
+ const suffix = hash === -1 ? "" : target.slice(hash);
23518
+ const bare = path !== "/" && path.endsWith("/") ? path.slice(0, -1) : path;
23519
+ const replacement = rewrites.get(bare);
23520
+ if (replacement === undefined) {
23521
+ continue;
23522
+ }
23523
+ const offset = (match.index ?? 0) + (match.groups?.prefix?.length ?? 0);
23524
+ out += line.slice(cursor, offset) + replacement + suffix;
23525
+ cursor = offset + target.length;
23526
+ count += 1;
23527
+ }
23528
+ return count === 0 ? line : out + line.slice(cursor);
23529
+ };
23530
+ var rewriteSnapshotLinks = (source, rewrites) => {
23531
+ let fence = null;
23532
+ let count = 0;
23533
+ const lines = source.split(`
23534
+ `).map((line) => {
23535
+ const next = nextFenceState(line, fence);
23536
+ const inFence = fence !== null || next !== null;
23537
+ fence = next;
23538
+ if (inFence) {
23539
+ return line;
23540
+ }
23541
+ const rewrittenLine = rewriteLine2(line, rewrites);
23542
+ if (rewrittenLine !== line) {
23543
+ count += 1;
23544
+ }
23545
+ return rewrittenLine;
23546
+ });
23547
+ return { count, text: lines.join(`
23548
+ `) };
23549
+ };
23550
+ var snippetFor = (id2, hasVersions) => hasVersions ? `Add to versions.archived in blume.config.ts (newest first):
23551
+
23552
+ { id: "${id2}" },
23553
+ ` : `Add to blume.config.ts:
23554
+
23555
+ versions: {
23556
+ archived: [{ id: "${id2}" }],
23557
+ current: { label: "…" },
23558
+ },
23559
+ `;
23560
+ var insertArchivedVersion = async (configPath, id2) => {
23561
+ let text2;
23562
+ try {
23563
+ text2 = await readFile27(configPath, "utf-8");
23564
+ } catch {
23565
+ return false;
23566
+ }
23567
+ const match = text2.match(/(?<lead>archived:\s*\[)/u);
23568
+ if (!match || match.index === undefined) {
23569
+ return false;
23570
+ }
23571
+ const insertAt = match.index + (match.groups?.lead?.length ?? 0);
23572
+ const lineStart = text2.lastIndexOf(`
23573
+ `, match.index) + 1;
23574
+ const indent = text2.slice(lineStart).match(/^\s*/u)?.[0] ?? "";
23575
+ const rest = text2.slice(insertAt);
23576
+ let entry;
23577
+ if (rest.trimStart().startsWith("]")) {
23578
+ entry = `{ id: "${id2}" }`;
23579
+ } else if (rest.startsWith(`
23580
+ `)) {
23581
+ entry = `
23582
+ ${indent} { id: "${id2}" },`;
23583
+ } else {
23584
+ entry = `{ id: "${id2}" }, `;
23585
+ }
23586
+ await writeTextAtomic(configPath, text2.slice(0, insertAt) + entry + rest);
23587
+ return true;
23588
+ };
23589
+ var cutVersion = async (root, id2, options = {}) => {
23590
+ if (!VERSION_ID.test(id2)) {
23591
+ 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}".`);
23592
+ }
23593
+ const project = await scanProject(root);
23594
+ const errors = project.diagnostics.filter((diagnostic) => diagnostic.severity === "error");
23595
+ if (errors.length > 0) {
23596
+ throw new CutError(`The project has ${errors.length} error diagnostic(s) — fix them before cutting a version (run \`blume validate\`).`);
23597
+ }
23598
+ const { contentRoot: contentRoot2 } = project.context;
23599
+ if (project.config.versions?.archived.some((version) => version.id === id2)) {
23600
+ throw new CutError(`Version "${id2}" is already registered in versions.archived.`);
23601
+ }
23602
+ const dir = join48(contentRoot2, id2);
23603
+ if (existsSync28(dir)) {
23604
+ if (!options.force) {
23605
+ throw new CutError(`${dir} already exists — pass --force to overwrite the snapshot.`);
23606
+ }
23607
+ await rm5(dir, { force: true, recursive: true });
23608
+ }
23609
+ const excluded = new Set([
23610
+ id2,
23611
+ "node_modules",
23612
+ ...project.config.versions?.archived.map((version) => version.id) ?? []
23613
+ ]);
23614
+ const entries = await readdir3(contentRoot2, { withFileTypes: true });
23615
+ let copied = 0;
23616
+ await Promise.all(entries.map(async (entry) => {
23617
+ if (excluded.has(entry.name) || entry.name.startsWith(".") || entry.isDirectory() && VERSION_LIKE.test(entry.name)) {
23618
+ return;
23619
+ }
23620
+ await cp3(join48(contentRoot2, entry.name), join48(dir, entry.name), {
23621
+ recursive: true
23622
+ });
23623
+ }));
23624
+ const rewrites = buildRouteRewrites(project, id2);
23625
+ const rewritten = [];
23626
+ const walk = async (current) => {
23627
+ const children = await readdir3(current, { withFileTypes: true });
23628
+ await Promise.all(children.map(async (child) => {
23629
+ const abs = join48(current, child.name);
23630
+ if (child.isDirectory()) {
23631
+ await walk(abs);
23632
+ return;
23633
+ }
23634
+ copied += 1;
23635
+ if (!/\.mdx?$/u.test(child.name)) {
23636
+ return;
23637
+ }
23638
+ const source = await readFile27(abs, "utf-8");
23639
+ const { text: text2, count } = rewriteSnapshotLinks(source, rewrites);
23640
+ if (count > 0) {
23641
+ await writeTextAtomic(abs, text2);
23642
+ rewritten.push({ count, file: relative21(dir, abs) });
23643
+ }
23644
+ }));
23645
+ };
23646
+ await walk(dir);
23647
+ const configPath = join48(root, "blume.config.ts");
23648
+ const configUpdated = project.config.versions !== undefined && await insertArchivedVersion(configPath, id2);
23649
+ return {
23650
+ configSnippet: configUpdated ? null : snippetFor(id2, project.config.versions !== undefined),
23651
+ configUpdated,
23652
+ copied,
23653
+ dir,
23654
+ rewritten
23655
+ };
23656
+ };
23657
+
23658
+ // src/cli/commands/version.ts
23659
+ var versionCommand = defineCommand15({
23660
+ args: {
23661
+ force: {
23662
+ description: "Overwrite an existing snapshot directory.",
23663
+ type: "boolean"
23664
+ },
23665
+ id: {
23666
+ description: 'Version id to cut (e.g. "v1.0").',
23667
+ required: false,
23668
+ type: "positional"
23669
+ }
23670
+ },
23671
+ meta: {
23672
+ description: "Freeze the current docs as an archived version.",
23673
+ name: "version"
23674
+ },
23675
+ async run({ args }) {
23676
+ const root = process.cwd();
23677
+ if (!args.id) {
23678
+ const { config: config2 } = await loadConfig(root);
23679
+ if (!config2.versions) {
23680
+ logger.info("Versioning is not configured. Cut the first version with `blume version <id>` (e.g. `blume version v1.0`).");
23681
+ return;
23682
+ }
23683
+ const { current, archived } = config2.versions;
23684
+ process.stdout.write(` ${current.label} (current)${current.badge ? ` — ${current.badge}` : ""}
23685
+ `);
23686
+ for (const version of archived) {
23687
+ process.stdout.write(` ${version.label ?? version.id} — ${version.id}/
23688
+ `);
23689
+ }
23690
+ return;
23691
+ }
23692
+ try {
23693
+ const result = await cutVersion(root, args.id, { force: args.force });
23694
+ logger.success(`Snapshot ${result.dir} (${result.copied} file(s) copied)`);
23695
+ const totalRewrites = result.rewritten.reduce((sum, entry) => sum + entry.count, 0);
23696
+ if (totalRewrites > 0) {
23697
+ logger.info(`Rewrote root-absolute links in ${result.rewritten.length} page(s) (${totalRewrites} line(s)).`);
23698
+ }
23699
+ if (result.configUpdated) {
23700
+ logger.success(`Added "${args.id}" to versions.archived in blume.config.ts`);
23701
+ } else if (result.configSnippet) {
23702
+ logger.info(result.configSnippet);
23703
+ }
23704
+ logger.info("Archived versions are frozen — future edits belong in the live tree. Restart `blume dev` to pick up the snapshot.");
23705
+ } catch (error) {
23706
+ if (error instanceof CutError) {
23707
+ logger.error(error.message);
23708
+ process.exit(1);
23709
+ }
23710
+ if (error instanceof BlumeError) {
23711
+ logger.error(error.diagnostic.message);
23712
+ process.exit(1);
23713
+ }
23714
+ reportInternalError(error);
23715
+ process.exit(1);
23716
+ }
23717
+ }
23718
+ });
23719
+
22676
23720
  // src/cli/index.ts
22677
- var main = defineCommand15({
23721
+ var main = defineCommand16({
22678
23722
  meta: {
22679
23723
  description: "Markdown-first documentation powered by Astro and Vite.",
22680
23724
  name: "blume",
@@ -22694,7 +23738,8 @@ var main = defineCommand15({
22694
23738
  preview: previewCommand,
22695
23739
  sync: syncCommand,
22696
23740
  translate: translateCommand,
22697
- validate: validateCommand
23741
+ validate: validateCommand,
23742
+ version: versionCommand
22698
23743
  }
22699
23744
  });
22700
23745
  loadEnvFiles(process.cwd());
@@ -22708,5 +23753,5 @@ process.on("unhandledRejection", (error) => {
22708
23753
  });
22709
23754
  runMain(main);
22710
23755
 
22711
- //# debugId=07BF7082AA0A5EEC64756E2164756E21
23756
+ //# debugId=6377B095BFD3F5AE64756E2164756E21
22712
23757
  //# sourceMappingURL=index.js.map