blume 0.7.0 → 0.8.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 (168) hide show
  1. package/CHANGELOG.md +618 -0
  2. package/LICENSE +21 -0
  3. package/README.md +107 -0
  4. package/dist/cli/index.js +1487 -360
  5. package/dist/cli/index.js.map +91 -85
  6. package/dist/types/ai/component-markdown.d.ts +34 -0
  7. package/dist/types/components/content/youtube.d.ts +18 -0
  8. package/dist/types/core/base-path.d.ts +9 -0
  9. package/dist/types/core/config-input.d.ts +36 -2
  10. package/dist/types/core/config.d.ts +3 -2
  11. package/dist/types/core/data.d.ts +2 -0
  12. package/dist/types/core/i18n-ui.d.ts +476 -132
  13. package/dist/types/core/schema.d.ts +216 -145
  14. package/dist/types/index.d.ts +1 -0
  15. package/dist/types/openapi/references.d.ts +60 -0
  16. package/docs/01-quickstart.mdx +5 -2
  17. package/docs/02-deployment.mdx +8 -8
  18. package/docs/03-faq.mdx +46 -16
  19. package/docs/advanced/custom-pages.mdx +1 -1
  20. package/docs/advanced/skills.mdx +1 -1
  21. package/docs/configuration/ai.mdx +49 -10
  22. package/docs/configuration/customization.mdx +11 -0
  23. package/docs/configuration/index.mdx +27 -3
  24. package/docs/configuration/seo.mdx +2 -2
  25. package/docs/content/components.mdx +1 -1
  26. package/docs/content/i18n.mdx +1 -1
  27. package/docs/content/navigation.mdx +3 -3
  28. package/docs/content/sources.mdx +1 -1
  29. package/docs/content/syntax.mdx +4 -2
  30. package/docs/index.mdx +2 -2
  31. package/docs/reference/cli.mdx +8 -6
  32. package/package.json +14 -4
  33. package/skills/blume/SKILL.md +5 -3
  34. package/skills/blume-update-docs/SKILL.md +3 -2
  35. package/src/ai/agent-readability.ts +9 -8
  36. package/src/ai/ask-context.ts +7 -2
  37. package/src/ai/ask-data.ts +3 -0
  38. package/src/ai/component-markdown.ts +461 -0
  39. package/src/ai/llms.ts +135 -26
  40. package/src/ai/markdown.ts +35 -6
  41. package/src/ai/mcp/data.ts +25 -4
  42. package/src/ai/mcp/discovery.ts +10 -3
  43. package/src/ai/mcp/server.ts +21 -7
  44. package/src/ai/visibility.ts +74 -0
  45. package/src/astro/component-slots.ts +11 -1
  46. package/src/astro/generate.ts +76 -45
  47. package/src/astro/integration.ts +1 -1
  48. package/src/astro/markdown-negotiation.ts +1 -1
  49. package/src/astro/pages.ts +81 -19
  50. package/src/astro/templates.ts +99 -12
  51. package/src/blume-modules.d.ts +8 -0
  52. package/src/cli/commands/build.ts +99 -19
  53. package/src/cli/commands/check.ts +1 -1
  54. package/src/cli/commands/dev.ts +26 -5
  55. package/src/cli/commands/eject.ts +47 -19
  56. package/src/cli/commands/init.ts +120 -180
  57. package/src/cli/commands/preview.ts +4 -1
  58. package/src/cli/commands/validate.ts +43 -2
  59. package/src/cli/dev-lock.ts +8 -4
  60. package/src/cli/eject-scripts.ts +72 -0
  61. package/src/cli/env.ts +15 -5
  62. package/src/cli/init/questions.ts +158 -0
  63. package/src/cli/init/scaffold.ts +380 -0
  64. package/src/components/content/AccordionItem.astro +23 -4
  65. package/src/components/content/Badge.astro +3 -1
  66. package/src/components/content/Card.astro +4 -2
  67. package/src/components/content/Step.astro +10 -1
  68. package/src/components/content/Tabs.astro +15 -3
  69. package/src/components/content/Tile.astro +2 -1
  70. package/src/components/content/Tooltip.astro +3 -1
  71. package/src/components/content/Update.astro +9 -2
  72. package/src/components/content/auto-type-table.ts +7 -1
  73. package/src/components/content/base-href.ts +33 -0
  74. package/src/components/content/changelog-element.ts +9 -2
  75. package/src/components/content/mermaid-element.ts +7 -2
  76. package/src/components/islands/AskAI.astro +5 -2
  77. package/src/components/islands/ask-ai.tsx +56 -6
  78. package/src/components/islands/hooks.ts +28 -8
  79. package/src/components/layout/Banner.astro +10 -2
  80. package/src/components/layout/Header.astro +13 -4
  81. package/src/components/layout/Logo.astro +11 -3
  82. package/src/components/layout/NavTree.astro +17 -3
  83. package/src/components/layout/PageActions.astro +25 -10
  84. package/src/components/layout/PageLayout.astro +45 -8
  85. package/src/components/layout/ReferenceLayout.astro +8 -1
  86. package/src/components/layout/RootLayout.astro +67 -9
  87. package/src/components/layout/Search.astro +94 -22
  88. package/src/components/layout/search/algolia.ts +11 -2
  89. package/src/components/layout/search/endpoint.ts +11 -5
  90. package/src/components/layout/search/orama-cloud.ts +8 -2
  91. package/src/components/layout/search/types.ts +5 -1
  92. package/src/components/layout/search/typesense.ts +4 -1
  93. package/src/components/layout/toc-element.ts +1 -1
  94. package/src/components/openapi/ApiTagOperations.astro +2 -1
  95. package/src/components/openapi/Operation.astro +47 -40
  96. package/src/components/openapi/RequestPanel.astro +1 -1
  97. package/src/components/openapi/helpers.ts +71 -3
  98. package/src/components/openapi/panel.ts +1 -1
  99. package/src/core/base-path.ts +24 -0
  100. package/src/core/builtin-tags.ts +2 -0
  101. package/src/core/config-input.ts +37 -2
  102. package/src/core/config.ts +3 -2
  103. package/src/core/data.ts +2 -0
  104. package/src/core/graph.ts +15 -5
  105. package/src/core/i18n-ui.ts +45 -0
  106. package/src/core/last-modified.ts +13 -6
  107. package/src/core/links.ts +32 -8
  108. package/src/core/navigation.ts +29 -4
  109. package/src/core/package-json.ts +17 -2
  110. package/src/core/project-graph.ts +15 -6
  111. package/src/core/schema.ts +36 -2
  112. package/src/core/sources/assets.ts +6 -1
  113. package/src/core/sources/filesystem.ts +4 -0
  114. package/src/core/sources/mdx-remote.ts +23 -14
  115. package/src/core/sources/normalize.ts +152 -50
  116. package/src/core/sources/notion.ts +8 -8
  117. package/src/core/ui-packs/ar.ts +1 -0
  118. package/src/core/ui-packs/bg.ts +1 -0
  119. package/src/core/ui-packs/bn.ts +1 -0
  120. package/src/core/ui-packs/ca.ts +1 -0
  121. package/src/core/ui-packs/cs.ts +1 -0
  122. package/src/core/ui-packs/da.ts +1 -0
  123. package/src/core/ui-packs/de.ts +1 -0
  124. package/src/core/ui-packs/el.ts +1 -0
  125. package/src/core/ui-packs/es.ts +1 -0
  126. package/src/core/ui-packs/fa.ts +1 -0
  127. package/src/core/ui-packs/fi.ts +1 -0
  128. package/src/core/ui-packs/fr.ts +2 -1
  129. package/src/core/ui-packs/he.ts +1 -0
  130. package/src/core/ui-packs/hi.ts +1 -0
  131. package/src/core/ui-packs/hr.ts +1 -0
  132. package/src/core/ui-packs/hu.ts +1 -0
  133. package/src/core/ui-packs/id.ts +1 -0
  134. package/src/core/ui-packs/it.ts +1 -0
  135. package/src/core/ui-packs/ja.ts +1 -0
  136. package/src/core/ui-packs/ko.ts +1 -0
  137. package/src/core/ui-packs/nl.ts +1 -0
  138. package/src/core/ui-packs/no.ts +1 -0
  139. package/src/core/ui-packs/pl.ts +1 -0
  140. package/src/core/ui-packs/pt-br.ts +1 -0
  141. package/src/core/ui-packs/pt.ts +1 -0
  142. package/src/core/ui-packs/ro.ts +1 -0
  143. package/src/core/ui-packs/ru.ts +1 -0
  144. package/src/core/ui-packs/sk.ts +1 -0
  145. package/src/core/ui-packs/sr.ts +1 -0
  146. package/src/core/ui-packs/sv.ts +1 -0
  147. package/src/core/ui-packs/th.ts +1 -0
  148. package/src/core/ui-packs/tr.ts +1 -0
  149. package/src/core/ui-packs/uk.ts +1 -0
  150. package/src/core/ui-packs/vi.ts +1 -0
  151. package/src/core/ui-packs/zh-tw.ts +1 -0
  152. package/src/core/ui-packs/zh.ts +1 -0
  153. package/src/deploy/adapter-output.ts +18 -8
  154. package/src/deploy/redirects.ts +7 -2
  155. package/src/deploy/sitemap.ts +53 -11
  156. package/src/index.ts +5 -0
  157. package/src/markdown/base-links.ts +10 -8
  158. package/src/markdown/index.ts +15 -3
  159. package/src/markdown/inline-code.ts +7 -2
  160. package/src/markdown/package-commands.ts +10 -4
  161. package/src/openapi/model.ts +12 -4
  162. package/src/openapi/parse.ts +21 -0
  163. package/src/openapi/references.ts +38 -8
  164. package/src/openapi/source.ts +59 -10
  165. package/src/registry/eject.ts +184 -12
  166. package/src/registry/registry.ts +0 -3
  167. package/src/search/documents.ts +34 -2
  168. package/src/seo/jsonld.ts +13 -12
package/dist/cli/index.js CHANGED
@@ -48,7 +48,6 @@ import { dirname as dirname3, join as join4 } from "pathe";
48
48
  // src/registry/registry.ts
49
49
  import { join as join3 } from "pathe";
50
50
  var packageSrc = join3(packageRoot(), "src");
51
- var itemsRoot = join3(packageRoot(), "src", "registry", "items");
52
51
  var layoutComponent = (config) => {
53
52
  const target = `components/blume/${config.file}`;
54
53
  return {
@@ -663,14 +662,17 @@ var buildAgentReadability = (project) => {
663
662
  }
664
663
  const site = config.deployment.site ?? null;
665
664
  const deployBase = normalizeBasePath(config.deployment.base);
666
- const abs = (path) => site ? `${site.replace(/\/+$/u, "")}${withBasePath(deployBase, path)}` : path;
665
+ const abs = (path) => {
666
+ const based = withBasePath(deployBase, path);
667
+ return site ? `${site.replace(/\/+$/u, "")}${based}` : based;
668
+ };
667
669
  const artifacts = {
668
670
  markdown: {
669
671
  contentNegotiation: "text/markdown",
670
672
  pattern: abs("/{route}.md")
671
673
  }
672
674
  };
673
- if (config.ai.llmsTxt) {
675
+ if (config.ai.llmsTxt.enabled) {
674
676
  artifacts.llmsFullTxt = abs("/llms-full.txt");
675
677
  artifacts.llmsTxt = abs("/llms.txt");
676
678
  }
@@ -744,37 +746,376 @@ var readEntryText = async (ctx, page) => {
744
746
  return "";
745
747
  };
746
748
 
749
+ // src/ai/component-markdown.ts
750
+ import { mdxToMdast } from "satteri";
751
+
752
+ // src/components/content/youtube.ts
753
+ var BARE_ID = /^[\w-]{11}$/u;
754
+ var URL_ID = /(?:youtu\.be\/|\/embed\/|\/shorts\/|\/live\/|[?&]v=)(?<id>[\w-]{11})/u;
755
+ var parseYouTubeId = (input) => {
756
+ const value = input.trim();
757
+ if (!value) {
758
+ return null;
759
+ }
760
+ if (BARE_ID.test(value)) {
761
+ return value;
762
+ }
763
+ return URL_ID.exec(value)?.groups?.id ?? null;
764
+ };
765
+
766
+ // src/ai/component-markdown.ts
767
+ var evaluateExpression = (raw) => {
768
+ try {
769
+ const value = new Function(`"use strict"; return (${raw});`)();
770
+ return { ok: true, value };
771
+ } catch {
772
+ return { ok: false, value: undefined };
773
+ }
774
+ };
775
+ var readProps = (node) => {
776
+ const props = {};
777
+ let lossy = false;
778
+ for (const attribute of node.attributes ?? []) {
779
+ if (attribute.type !== "mdxJsxAttribute" || !attribute.name) {
780
+ lossy = true;
781
+ continue;
782
+ }
783
+ if (attribute.value === null || attribute.value === undefined) {
784
+ props[attribute.name] = true;
785
+ } else if (typeof attribute.value === "string") {
786
+ props[attribute.name] = attribute.value;
787
+ } else {
788
+ const result = evaluateExpression(attribute.value.value);
789
+ if (result.ok) {
790
+ props[attribute.name] = result.value;
791
+ } else {
792
+ lossy = true;
793
+ }
794
+ }
795
+ }
796
+ return { lossy, props };
797
+ };
798
+ var hasOffsets = (node) => typeof node.position?.start?.offset === "number" && typeof node.position?.end?.offset === "number";
799
+ var applySplices = (text, splices) => {
800
+ let result = text;
801
+ for (const splice of [...splices].toSorted((a, b) => b.start - a.start)) {
802
+ const lineStart = result.lastIndexOf(`
803
+ `, splice.start - 1) + 1;
804
+ const prefix = result.slice(lineStart, splice.start);
805
+ const indent = /^[\t ]+$/u.test(prefix) ? prefix : "";
806
+ const replacement = indent ? splice.text.split(`
807
+ `).map((line, index) => index === 0 || line === "" ? line : `${indent}${line}`).join(`
808
+ `) : splice.text;
809
+ result = result.slice(0, splice.start) + replacement + result.slice(splice.end);
810
+ }
811
+ return result;
812
+ };
813
+ var dedent = (text) => {
814
+ const lines = text.split(`
815
+ `);
816
+ const rest = lines.slice(1).filter((line) => line.trim() !== "");
817
+ if (rest.length === 0) {
818
+ return text;
819
+ }
820
+ const indent = Math.min(...rest.map((line) => line.length - line.trimStart().length));
821
+ if (indent === 0) {
822
+ return text;
823
+ }
824
+ return [
825
+ lines[0],
826
+ ...lines.slice(1).map((line) => line.trim() === "" ? "" : line.slice(indent))
827
+ ].join(`
828
+ `);
829
+ };
830
+ var isJsxElement = (node) => node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement";
831
+ var cellText = (value) => String(value ?? "").replaceAll(/\s*\n\s*/gu, " ").replaceAll("|", "\\|").trim();
832
+ var cellCode = (value) => {
833
+ const text = cellText(value);
834
+ return text && !text.includes("`") ? `\`${text}\`` : text;
835
+ };
836
+ var typeTable = ({ children, props }) => {
837
+ const { type } = props;
838
+ if (type === null || typeof type !== "object") {
839
+ return null;
840
+ }
841
+ const entries = Object.entries(type);
842
+ const rows = entries.map(([name, info]) => {
843
+ const prop = cellCode(`${name}${info.required ? "" : "?"}`);
844
+ const typeCell = info.typeDescriptionLink ? `[${cellCode(info.type)}](${cellText(info.typeDescriptionLink)})` : cellCode(info.type);
845
+ const defaultCell = info.default === undefined ? "-" : cellCode(info.default);
846
+ const description = cellText([info.description, info.typeDescription].filter((part) => typeof part === "string" && part !== "").join(" "));
847
+ return `| ${prop} | ${typeCell} | ${defaultCell} | ${description} |`;
848
+ });
849
+ const table = rows.length > 0 ? [
850
+ "| Prop | Type | Default | Description |",
851
+ "| --- | --- | --- | --- |",
852
+ ...rows
853
+ ].join(`
854
+ `) : "";
855
+ return [table, children].filter(Boolean).join(`
856
+
857
+ `);
858
+ };
859
+ var callout = ({ children, props }) => {
860
+ const type = typeof props.type === "string" ? props.type : "info";
861
+ const label = typeof props.title === "string" && props.title !== "" ? props.title : type.charAt(0).toUpperCase() + type.slice(1);
862
+ if (!children) {
863
+ return `> **${label}**`;
864
+ }
865
+ const body = children.split(`
866
+ `).map((line) => line.trim() === "" ? ">" : `> ${line}`).join(`
867
+ `);
868
+ return `> **${label}**
869
+ >
870
+ ${body}`;
871
+ };
872
+ var listItem = (index, content) => {
873
+ const marker = `${index}. `;
874
+ return content.split(`
875
+ `).map((line, lineIndex) => {
876
+ if (lineIndex === 0) {
877
+ return `${marker}${line}`;
878
+ }
879
+ return line === "" ? "" : ` ${line}`;
880
+ }).join(`
881
+ `);
882
+ };
883
+ var steps = ({ childComponents, children }) => {
884
+ const items = childComponents("Step");
885
+ if (items.length === 0) {
886
+ return children;
887
+ }
888
+ return items.map((step, index) => {
889
+ const title = typeof step.props.title === "string" && step.props.title !== "" ? `**${step.props.title}**` : "";
890
+ const content = [title, step.children].filter(Boolean).join(`
891
+
892
+ `);
893
+ return listItem(index + 1, content);
894
+ }).join(`
895
+
896
+ `);
897
+ };
898
+ var tabs = ({ childComponents, children }) => {
899
+ const items = childComponents("Tab");
900
+ if (items.length === 0) {
901
+ return children;
902
+ }
903
+ return items.map((tab, index) => {
904
+ const title = typeof tab.props.title === "string" && tab.props.title !== "" ? tab.props.title : `Tab ${index + 1}`;
905
+ return tab.children ? `**${title}**
906
+
907
+ ${tab.children}` : `**${title}**`;
908
+ }).join(`
909
+
910
+ `);
911
+ };
912
+ var youtube = ({ props }) => {
913
+ let input = "";
914
+ if (typeof props.id === "string") {
915
+ input = props.id;
916
+ } else if (typeof props.url === "string") {
917
+ input = props.url;
918
+ }
919
+ const videoId = parseYouTubeId(input);
920
+ if (!videoId) {
921
+ return null;
922
+ }
923
+ const start = typeof props.start === "number" && props.start > 0 ? `&t=${Math.floor(props.start)}s` : "";
924
+ const title = typeof props.title === "string" && props.title !== "" ? props.title : "Watch on YouTube";
925
+ return `[${title}](https://www.youtube.com/watch?v=${videoId}${start})`;
926
+ };
927
+ var SERIALIZERS = {
928
+ Callout: callout,
929
+ Steps: steps,
930
+ Tabs: tabs,
931
+ TypeTable: typeTable,
932
+ YouTube: youtube
933
+ };
934
+ var escapeRegExp2 = (value) => value.replaceAll(/[$()*+.?[\\\]^{|}]/gu, String.raw`\$&`);
935
+ var componentHint = (registry2) => new RegExp(`<(?:${Object.keys(registry2).map(escapeRegExp2).join("|")})[\\s/>]`, "u");
936
+ var BUILT_IN_HINT = componentHint(SERIALIZERS);
937
+ var renderChildren = (walk, node) => {
938
+ const children = (node.children ?? []).filter(hasOffsets);
939
+ const [first] = children;
940
+ if (!first) {
941
+ return "";
942
+ }
943
+ const start = first.position.start.offset;
944
+ const end = children.at(-1)?.position.end.offset ?? start;
945
+ const splices = [];
946
+ collectSplices(walk, children, splices);
947
+ const spliced = applySplices(walk.source.slice(start, end), splices.map((splice) => ({
948
+ ...splice,
949
+ end: splice.end - start,
950
+ start: splice.start - start
951
+ })));
952
+ return dedent(spliced).trim();
953
+ };
954
+ var serializeElement = (serializer, walk, node) => serializer({
955
+ ...readProps(node),
956
+ childComponents: (name) => (node.children ?? []).filter((child) => isJsxElement(child) && child.name === name).map((child) => ({
957
+ ...readProps(child),
958
+ children: renderChildren(walk, child)
959
+ })),
960
+ children: renderChildren(walk, node)
961
+ });
962
+ var collectSplices = (walk, nodes, out) => {
963
+ for (const node of nodes) {
964
+ const serializer = node.type === "mdxJsxFlowElement" && node.name ? walk.registry[node.name] : undefined;
965
+ if (serializer && hasOffsets(node)) {
966
+ const text = serializeElement(serializer, walk, node);
967
+ if (text !== null) {
968
+ out.push({
969
+ end: node.position.end.offset,
970
+ start: node.position.start.offset,
971
+ text
972
+ });
973
+ continue;
974
+ }
975
+ }
976
+ collectSplices(walk, node.children ?? [], out);
977
+ }
978
+ };
979
+ var downlevelComponents = (source, components) => {
980
+ const custom = components && Object.keys(components).length > 0;
981
+ const registry2 = custom ? { ...SERIALIZERS, ...components } : SERIALIZERS;
982
+ const hint = custom ? componentHint(registry2) : BUILT_IN_HINT;
983
+ if (!hint.test(source)) {
984
+ return source;
985
+ }
986
+ let tree;
987
+ try {
988
+ tree = mdxToMdast(source);
989
+ } catch {
990
+ return source;
991
+ }
992
+ const splices = [];
993
+ collectSplices({ registry: registry2, source }, tree.children ?? [], splices);
994
+ return splices.length > 0 ? applySplices(source, splices) : source;
995
+ };
996
+
997
+ // src/ai/visibility.ts
998
+ var CODE_FENCE_BLOCK = /^(?<fence>`{3,}|~{3,})[^\n]*\n[\s\S]*?^\k<fence>[^\n]*(?=\n|$)/gmu;
999
+ var FENCE_TOKEN = /\u0000blume-fence-(?<index>\d+)\u0000/gu;
1000
+ var visibilityBlock = (audience) => new RegExp(`<Visibility\\s+for\\s*=\\s*(?:"${audience}"|'${audience}')\\s*>(?<inner>[\\s\\S]*?)</Visibility\\s*>`, "gu");
1001
+ var BLOCKS = {
1002
+ agents: visibilityBlock("agents"),
1003
+ web: visibilityBlock("web")
1004
+ };
1005
+ var applyAudienceVisibility = (markdown, audience) => {
1006
+ const fences = [];
1007
+ const masked = markdown.replace(CODE_FENCE_BLOCK, (block) => {
1008
+ fences.push(block);
1009
+ return `\x00blume-fence-${fences.length - 1}\x00`;
1010
+ });
1011
+ let touched = false;
1012
+ const filtered = masked.replaceAll(BLOCKS[audience === "agents" ? "web" : "agents"], () => {
1013
+ touched = true;
1014
+ return "";
1015
+ }).replaceAll(BLOCKS[audience], (_match, inner) => {
1016
+ touched = true;
1017
+ return inner;
1018
+ });
1019
+ const tidied = touched ? filtered.replaceAll(/\n{3,}/gu, `
1020
+
1021
+ `) : filtered;
1022
+ return tidied.replaceAll(FENCE_TOKEN, (token, index) => fences[Number(index)] ?? token);
1023
+ };
1024
+ var applyAgentVisibility = (markdown) => applyAudienceVisibility(markdown, "agents");
1025
+
747
1026
  // src/ai/llms.ts
748
1027
  var pageUrl = (route, site, base = "") => {
749
- if (!site) {
750
- return route;
1028
+ const path = withBasePath(base, route);
1029
+ return encodeURI(site ? `${site.replace(/\/$/u, "")}${path}` : path);
1030
+ };
1031
+ var eligiblePages = (project) => project.graph.pages.filter((page) => !(page.meta.draft || page.meta.sidebar.hidden || page.meta.seo.noindex) && (project.config.ai.llmsTxt.openapi || page.source.name !== "openapi"));
1032
+ var indexedNavigations = (project) => {
1033
+ const { i18n } = project.config;
1034
+ if (i18n) {
1035
+ return i18n.locales.flatMap(({ code, label }) => {
1036
+ const nav = project.graph.navigationByLocale[code];
1037
+ if (!nav) {
1038
+ return [];
1039
+ }
1040
+ return [{ label: code === i18n.defaultLocale ? undefined : label, nav }];
1041
+ });
751
1042
  }
752
- return `${site.replace(/\/$/u, "")}${withBasePath(base, route)}`;
1043
+ return [{ nav: project.graph.navigation }];
753
1044
  };
754
- var orderedPages = (project) => [...project.graph.pages].filter((page) => !page.meta.draft).sort((a, b) => a.route.localeCompare(b.route));
755
1045
  var buildIndex = (project) => {
756
1046
  const { config } = project;
757
1047
  const { site } = config.deployment;
758
- const lines = [`# ${config.title}`];
759
- if (config.description) {
760
- lines.push("", `> ${config.description}`);
761
- }
762
- lines.push("", "## Docs", "");
763
- for (const page of orderedPages(project)) {
764
- const url = pageUrl(page.route, site, normalizeBasePath(config.deployment.base));
1048
+ const base = normalizeBasePath(config.deployment.base);
1049
+ const eligible = eligiblePages(project);
1050
+ const byRoute = new Map(eligible.map((page) => [page.route, page]));
1051
+ const seen = new Set;
1052
+ const line = (page) => {
1053
+ seen.add(page.route);
765
1054
  const summary = page.description ? `: ${page.description}` : "";
766
- lines.push(`- [${page.title}](${url})${summary}`);
1055
+ return `- [${page.title}](${pageUrl(page.route, site, base)})${summary}`;
1056
+ };
1057
+ const renderLevel = (nodes, depth) => {
1058
+ const list = [];
1059
+ const groupBlocks = [];
1060
+ for (const node of nodes) {
1061
+ if (node.kind === "page") {
1062
+ const page = byRoute.get(node.route);
1063
+ if (page && !seen.has(page.route)) {
1064
+ list.push(line(page));
1065
+ }
1066
+ continue;
1067
+ }
1068
+ const rootPage = node.route ? byRoute.get(node.route) : undefined;
1069
+ const blocks2 = renderLevel(node.children, depth + 1);
1070
+ if (rootPage && !seen.has(rootPage.route)) {
1071
+ blocks2.unshift(line(rootPage));
1072
+ }
1073
+ if (blocks2.length > 0) {
1074
+ groupBlocks.push(`${"#".repeat(Math.min(depth, 6))} ${node.label}`, ...blocks2);
1075
+ }
1076
+ }
1077
+ return list.length > 0 ? [list.join(`
1078
+ `), ...groupBlocks] : groupBlocks;
1079
+ };
1080
+ const renderNav = (nav, depth) => {
1081
+ const loose = nav.sidebar.filter((node) => node.kind === "page");
1082
+ const groups = nav.sidebar.filter((node) => node.kind === "group");
1083
+ const looseBlocks = renderLevel(loose, depth + 1);
1084
+ return [
1085
+ ...looseBlocks.length > 0 ? [`${"#".repeat(depth)} Docs`, ...looseBlocks] : [],
1086
+ ...renderLevel(groups, depth)
1087
+ ];
1088
+ };
1089
+ const blocks = [];
1090
+ for (const { label, nav } of indexedNavigations(project)) {
1091
+ if (label) {
1092
+ const localized = renderNav(nav, 3);
1093
+ if (localized.length > 0) {
1094
+ blocks.push(`## ${label}`, ...localized);
1095
+ }
1096
+ continue;
1097
+ }
1098
+ blocks.push(...renderNav(nav, 2));
767
1099
  }
768
- return `${lines.join(`
1100
+ const leftover = eligible.filter((page) => !seen.has(page.route)).toSorted((a, b) => a.route.localeCompare(b.route));
1101
+ if (leftover.length > 0) {
1102
+ blocks.push(blocks.length > 0 ? "## Other" : "## Docs", leftover.map(line).join(`
1103
+ `));
1104
+ }
1105
+ const header = config.description ? `# ${config.title}
1106
+
1107
+ > ${config.description}` : `# ${config.title}`;
1108
+ return `${[header, ...blocks].join(`
1109
+
769
1110
  `)}
770
1111
  `;
771
1112
  };
772
1113
  var buildFull = async (project) => {
773
1114
  const { config } = project;
774
- const pages = orderedPages(project);
1115
+ const pages = eligiblePages(project).toSorted((a, b) => a.route.localeCompare(b.route));
775
1116
  const sections = await Promise.all(pages.map(async (page) => {
776
1117
  const raw = await readEntryText(project, page);
777
- const body = frontmatter_default(raw).content.trim();
1118
+ const body = downlevelComponents(applyAgentVisibility(frontmatter_default(raw).content), config.ai.markdownComponents).trim();
778
1119
  const url = pageUrl(page.route, config.deployment.site, normalizeBasePath(config.deployment.base));
779
1120
  return [`# ${page.title}`, `Source: ${url}`, "", body].join(`
780
1121
  `);
@@ -897,7 +1238,7 @@ import { existsSync as existsSync4 } from "node:fs";
897
1238
  import { cp, mkdir as mkdir2, rm } from "node:fs/promises";
898
1239
  import { dirname as dirname4, join as join6 } from "pathe";
899
1240
  var ADAPTER_OUTPUT_PATHS = {
900
- netlify: ".netlify",
1241
+ netlify: ".netlify/v1",
901
1242
  vercel: ".vercel/output"
902
1243
  };
903
1244
  var deployStaticDir = (config, context) => {
@@ -905,7 +1246,11 @@ var deployStaticDir = (config, context) => {
905
1246
  if (output === "server" && adapter === "vercel") {
906
1247
  return join6(context.root, ".vercel", "output", "static");
907
1248
  }
908
- return context.distDir ?? join6(context.root, "dist");
1249
+ const dist = context.distDir ?? join6(context.root, "dist");
1250
+ if (output === "server" && adapter === "node") {
1251
+ return join6(dist, "client");
1252
+ }
1253
+ return dist;
909
1254
  };
910
1255
  var surfaceAdapterOutput = async (config, context) => {
911
1256
  const { adapter, output } = config.deployment;
@@ -940,8 +1285,8 @@ var buildNetlifyRedirects = (redirects) => `${redirects.map((redirect) => `${red
940
1285
  var buildVercelConfig = (redirects) => `${JSON.stringify({
941
1286
  redirects: redirects.map((redirect) => ({
942
1287
  destination: redirect.to,
943
- permanent: redirect.status === 301 || redirect.status === 308,
944
- source: redirect.from
1288
+ source: redirect.from,
1289
+ statusCode: redirect.status
945
1290
  }))
946
1291
  }, null, 2)}
947
1292
  `;
@@ -986,7 +1331,71 @@ var buildRobots = (project) => {
986
1331
  `;
987
1332
  };
988
1333
 
1334
+ // src/astro/pages.ts
1335
+ import { extname, relative as relative4 } from "pathe";
1336
+ import { glob, globSync } from "tinyglobby";
1337
+ var PAGE_GLOB = ["**/*.astro"];
1338
+ var toPageRoutes = (pagesRoot, files) => {
1339
+ files.sort();
1340
+ return files.map((file) => {
1341
+ const rel = relative4(pagesRoot, file);
1342
+ const withoutExt = rel.slice(0, rel.length - extname(rel).length);
1343
+ const parts = withoutExt.split("/");
1344
+ if (parts.at(-1) === "index") {
1345
+ parts.pop();
1346
+ }
1347
+ const pattern = parts.length === 0 ? "/" : `/${parts.join("/")}`;
1348
+ return { entrypoint: file, pattern };
1349
+ });
1350
+ };
1351
+ var discoverPages = async (pagesRoot) => toPageRoutes(pagesRoot, await glob(PAGE_GLOB, { absolute: true, cwd: pagesRoot, onlyFiles: true }));
1352
+ var discoverPagesSync = (pagesRoot) => toPageRoutes(pagesRoot, globSync(PAGE_GLOB, { absolute: true, cwd: pagesRoot, onlyFiles: true }));
1353
+ var routeIsTaken = (pages, contentPages, route) => pages.some((page) => page.pattern === route) || contentPages.some((page) => page.route === route);
1354
+ var PRIVATE_SEGMENT = /^[._]/u;
1355
+ var staticSegments = (pattern) => {
1356
+ const segments = pattern.split("/").filter(Boolean);
1357
+ return segments.some((part) => PRIVATE_SEGMENT.test(part) || part.includes("[")) ? null : segments;
1358
+ };
1359
+ var customStaticRoutes = (pages) => {
1360
+ const routes = new Set;
1361
+ for (const { pattern } of pages) {
1362
+ const segments = staticSegments(pattern);
1363
+ if (segments !== null) {
1364
+ routes.add(segments.length === 0 ? "/" : `/${segments.join("/")}`);
1365
+ }
1366
+ }
1367
+ return [...routes];
1368
+ };
1369
+ var hasGeneratedChangelog = (project, userPages) => {
1370
+ const hasChangelog = project.graph.pages.some((page) => page.contentType === "changelog" && !(page.meta.draft || page.meta.sidebar.hidden));
1371
+ const hasChangelogSource = (project.config.content.sources ?? []).some((source) => source.type === "github-releases");
1372
+ return (hasChangelog || hasChangelogSource) && !routeIsTaken(userPages, project.graph.pages, "/changelog");
1373
+ };
1374
+ var humanizeSegment = (segment) => segment.split(/[-_]/u).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
1375
+ var customOgRoutes = (pages, siteTitle) => {
1376
+ const seen = new Set;
1377
+ const routes = [];
1378
+ const collectRoute = (pattern) => {
1379
+ const segments = staticSegments(pattern);
1380
+ if (segments === null) {
1381
+ return;
1382
+ }
1383
+ const slug = segments.length === 0 ? "index" : segments.join("/");
1384
+ if (seen.has(slug)) {
1385
+ return;
1386
+ }
1387
+ seen.add(slug);
1388
+ const last = segments.at(-1);
1389
+ routes.push({ slug, title: last ? humanizeSegment(last) : siteTitle });
1390
+ };
1391
+ for (const { pattern } of pages) {
1392
+ collectRoute(pattern);
1393
+ }
1394
+ return routes;
1395
+ };
1396
+
989
1397
  // src/deploy/sitemap.ts
1398
+ var ERROR_ROUTES = new Set(["/404", "/500"]);
990
1399
  var lastmodTag = (value) => {
991
1400
  if (!value) {
992
1401
  return "";
@@ -1001,13 +1410,29 @@ var buildSitemap = (project) => {
1001
1410
  }
1002
1411
  const base = site.replace(/\/$/u, "");
1003
1412
  const deployBase = normalizeBasePath(project.config.deployment.base);
1413
+ const seen = new Set;
1004
1414
  const urls = [];
1415
+ const pushUrl = (route, lastModified) => {
1416
+ const loc = escapeXml(encodeURI(`${base}${route}`));
1417
+ if (seen.has(loc)) {
1418
+ return;
1419
+ }
1420
+ seen.add(loc);
1421
+ urls.push(` <url><loc>${loc}</loc>${lastmodTag(lastModified)}</url>`);
1422
+ };
1005
1423
  for (const page of project.graph.pages) {
1006
- if (page.meta.draft || page.meta.sidebar.hidden || page.meta.seo.noindex) {
1424
+ if (page.meta.draft || page.meta.sidebar.hidden || page.meta.seo.noindex || ERROR_ROUTES.has(page.route)) {
1007
1425
  continue;
1008
1426
  }
1009
- const loc = escapeXml(encodeURI(`${base}${withBasePath(deployBase, page.route)}`));
1010
- urls.push(` <url><loc>${loc}</loc>${lastmodTag(page.lastModified)}</url>`);
1427
+ pushUrl(withBasePath(deployBase, page.route), page.lastModified);
1428
+ }
1429
+ const userPages = project.context.pagesRoot ? discoverPagesSync(project.context.pagesRoot) : [];
1430
+ const extraRoutes = customStaticRoutes(userPages).filter((route) => !ERROR_ROUTES.has(route));
1431
+ if (hasGeneratedChangelog(project, userPages)) {
1432
+ extraRoutes.push("/changelog");
1433
+ }
1434
+ for (const route of extraRoutes) {
1435
+ pushUrl(withBasePath(deployBase, route));
1011
1436
  }
1012
1437
  urls.sort();
1013
1438
  return `<?xml version="1.0" encoding="UTF-8"?>
@@ -1055,6 +1480,7 @@ var ar = {
1055
1480
  send: "إرسال",
1056
1481
  title: "اسأل الذكاء الاصطناعي"
1057
1482
  },
1483
+ banner: { dismiss: "إغلاق الإعلان" },
1058
1484
  feedback: {
1059
1485
  no: "لا",
1060
1486
  question: "هل كانت هذه الصفحة مفيدة؟",
@@ -1101,6 +1527,7 @@ var bg = {
1101
1527
  send: "Изпрати",
1102
1528
  title: "Попитай ИИ"
1103
1529
  },
1530
+ banner: { dismiss: "Затваряне на съобщението" },
1104
1531
  feedback: {
1105
1532
  no: "Не",
1106
1533
  question: "Беше ли полезна тази страница?",
@@ -1147,6 +1574,7 @@ var bn = {
1147
1574
  send: "পাঠান",
1148
1575
  title: "AI-কে জিজ্ঞাসা করুন"
1149
1576
  },
1577
+ banner: { dismiss: "ঘোষণা বন্ধ করুন" },
1150
1578
  feedback: {
1151
1579
  no: "না",
1152
1580
  question: "এই পৃষ্ঠাটি কি সহায়ক ছিল?",
@@ -1193,6 +1621,7 @@ var ca = {
1193
1621
  send: "Envia",
1194
1622
  title: "Pregunta a la IA"
1195
1623
  },
1624
+ banner: { dismiss: "Tanca l'anunci" },
1196
1625
  feedback: {
1197
1626
  no: "No",
1198
1627
  question: "Aquesta pàgina t'ha estat útil?",
@@ -1239,6 +1668,7 @@ var cs = {
1239
1668
  send: "Odeslat",
1240
1669
  title: "Zeptat se AI"
1241
1670
  },
1671
+ banner: { dismiss: "Zavřít oznámení" },
1242
1672
  feedback: {
1243
1673
  no: "Ne",
1244
1674
  question: "Byla tato stránka užitečná?",
@@ -1285,6 +1715,7 @@ var da = {
1285
1715
  send: "Send",
1286
1716
  title: "Spørg AI"
1287
1717
  },
1718
+ banner: { dismiss: "Luk meddelelsen" },
1288
1719
  feedback: {
1289
1720
  no: "Nej",
1290
1721
  question: "Var denne side nyttig?",
@@ -1331,6 +1762,7 @@ var de = {
1331
1762
  send: "Senden",
1332
1763
  title: "KI fragen"
1333
1764
  },
1765
+ banner: { dismiss: "Ankündigung schließen" },
1334
1766
  feedback: {
1335
1767
  no: "Nein",
1336
1768
  question: "War diese Seite hilfreich?",
@@ -1377,6 +1809,7 @@ var el = {
1377
1809
  send: "Αποστολή",
1378
1810
  title: "Ρωτήστε την AI"
1379
1811
  },
1812
+ banner: { dismiss: "Κλείσιμο ανακοίνωσης" },
1380
1813
  feedback: {
1381
1814
  no: "Όχι",
1382
1815
  question: "Σας φάνηκε χρήσιμη αυτή η σελίδα;",
@@ -1423,6 +1856,7 @@ var es = {
1423
1856
  send: "Enviar",
1424
1857
  title: "Preguntar a la IA"
1425
1858
  },
1859
+ banner: { dismiss: "Cerrar el anuncio" },
1426
1860
  feedback: {
1427
1861
  no: "No",
1428
1862
  question: "¿Te ha resultado útil esta página?",
@@ -1469,6 +1903,7 @@ var fa = {
1469
1903
  send: "ارسال",
1470
1904
  title: "از هوش مصنوعی بپرسید"
1471
1905
  },
1906
+ banner: { dismiss: "بستن اطلاعیه" },
1472
1907
  feedback: {
1473
1908
  no: "خیر",
1474
1909
  question: "آیا این صفحه مفید بود؟",
@@ -1515,6 +1950,7 @@ var fi = {
1515
1950
  send: "Lähetä",
1516
1951
  title: "Kysy tekoälyltä"
1517
1952
  },
1953
+ banner: { dismiss: "Sulje ilmoitus" },
1518
1954
  feedback: {
1519
1955
  no: "Ei",
1520
1956
  question: "Oliko tästä sivusta apua?",
@@ -1543,7 +1979,7 @@ var fr = {
1543
1979
  actions: {
1544
1980
  addToCursor: "Ajouter à Cursor",
1545
1981
  addToVscode: "Ajouter à VS Code",
1546
- askAI: "Demander à l'IA",
1982
+ askAI: "Demander à l'IA à propos de cette page",
1547
1983
  connectMcp: "Se connecter à MCP",
1548
1984
  copied: "Copié !",
1549
1985
  copyClaudeCode: "Copier la commande Claude Code",
@@ -1561,6 +1997,7 @@ var fr = {
1561
1997
  send: "Envoyer",
1562
1998
  title: "Demander à l'IA"
1563
1999
  },
2000
+ banner: { dismiss: "Fermer l'annonce" },
1564
2001
  feedback: {
1565
2002
  no: "Non",
1566
2003
  question: "Cette page vous a-t-elle été utile ?",
@@ -1607,6 +2044,7 @@ var he = {
1607
2044
  send: "שלח",
1608
2045
  title: "שאל את ה-AI"
1609
2046
  },
2047
+ banner: { dismiss: "סגירת ההודעה" },
1610
2048
  feedback: {
1611
2049
  no: "לא",
1612
2050
  question: "האם העמוד הזה היה מועיל?",
@@ -1653,6 +2091,7 @@ var hi = {
1653
2091
  send: "भेजें",
1654
2092
  title: "AI से पूछें"
1655
2093
  },
2094
+ banner: { dismiss: "घोषणा बंद करें" },
1656
2095
  feedback: {
1657
2096
  no: "नहीं",
1658
2097
  question: "क्या यह पेज सहायक था?",
@@ -1699,6 +2138,7 @@ var hr = {
1699
2138
  send: "Pošalji",
1700
2139
  title: "Pitaj AI"
1701
2140
  },
2141
+ banner: { dismiss: "Zatvori obavijest" },
1702
2142
  feedback: {
1703
2143
  no: "Ne",
1704
2144
  question: "Je li vam ova stranica bila korisna?",
@@ -1745,6 +2185,7 @@ var hu = {
1745
2185
  send: "Küldés",
1746
2186
  title: "Kérdezd az AI-t"
1747
2187
  },
2188
+ banner: { dismiss: "Közlemény bezárása" },
1748
2189
  feedback: {
1749
2190
  no: "Nem",
1750
2191
  question: "Hasznos volt ez az oldal?",
@@ -1791,6 +2232,7 @@ var id = {
1791
2232
  send: "Kirim",
1792
2233
  title: "Tanya AI"
1793
2234
  },
2235
+ banner: { dismiss: "Tutup pengumuman" },
1794
2236
  feedback: {
1795
2237
  no: "Tidak",
1796
2238
  question: "Apakah halaman ini membantu?",
@@ -1837,6 +2279,7 @@ var it = {
1837
2279
  send: "Invia",
1838
2280
  title: "Chiedi all'IA"
1839
2281
  },
2282
+ banner: { dismiss: "Chiudi l'annuncio" },
1840
2283
  feedback: {
1841
2284
  no: "No",
1842
2285
  question: "Questa pagina ti è stata utile?",
@@ -1883,6 +2326,7 @@ var ja = {
1883
2326
  send: "送信",
1884
2327
  title: "AI に質問"
1885
2328
  },
2329
+ banner: { dismiss: "お知らせを閉じる" },
1886
2330
  feedback: {
1887
2331
  no: "いいえ",
1888
2332
  question: "このページは役に立ちましたか?",
@@ -1929,6 +2373,7 @@ var ko = {
1929
2373
  send: "보내기",
1930
2374
  title: "AI에게 질문"
1931
2375
  },
2376
+ banner: { dismiss: "공지 닫기" },
1932
2377
  feedback: {
1933
2378
  no: "아니요",
1934
2379
  question: "이 페이지가 도움이 되었나요?",
@@ -1975,6 +2420,7 @@ var nl = {
1975
2420
  send: "Verzenden",
1976
2421
  title: "AI vragen"
1977
2422
  },
2423
+ banner: { dismiss: "Aankondiging sluiten" },
1978
2424
  feedback: {
1979
2425
  no: "Nee",
1980
2426
  question: "Was deze pagina nuttig?",
@@ -2021,6 +2467,7 @@ var no = {
2021
2467
  send: "Send",
2022
2468
  title: "Spør AI"
2023
2469
  },
2470
+ banner: { dismiss: "Lukk kunngjøringen" },
2024
2471
  feedback: {
2025
2472
  no: "Nei",
2026
2473
  question: "Var denne siden nyttig?",
@@ -2067,6 +2514,7 @@ var pl = {
2067
2514
  send: "Wyślij",
2068
2515
  title: "Zapytaj AI"
2069
2516
  },
2517
+ banner: { dismiss: "Zamknij ogłoszenie" },
2070
2518
  feedback: {
2071
2519
  no: "Nie",
2072
2520
  question: "Czy ta strona była pomocna?",
@@ -2113,6 +2561,7 @@ var ptBR = {
2113
2561
  send: "Enviar",
2114
2562
  title: "Perguntar à IA"
2115
2563
  },
2564
+ banner: { dismiss: "Fechar o anúncio" },
2116
2565
  feedback: {
2117
2566
  no: "Não",
2118
2567
  question: "Esta página foi útil?",
@@ -2159,6 +2608,7 @@ var pt = {
2159
2608
  send: "Enviar",
2160
2609
  title: "Perguntar à IA"
2161
2610
  },
2611
+ banner: { dismiss: "Fechar o anúncio" },
2162
2612
  feedback: {
2163
2613
  no: "Não",
2164
2614
  question: "Esta página foi útil?",
@@ -2205,6 +2655,7 @@ var ro = {
2205
2655
  send: "Trimite",
2206
2656
  title: "Întreabă AI"
2207
2657
  },
2658
+ banner: { dismiss: "Închide anunțul" },
2208
2659
  feedback: {
2209
2660
  no: "Nu",
2210
2661
  question: "Ți-a fost utilă această pagină?",
@@ -2251,6 +2702,7 @@ var ru = {
2251
2702
  send: "Отправить",
2252
2703
  title: "Спросить ИИ"
2253
2704
  },
2705
+ banner: { dismiss: "Закрыть объявление" },
2254
2706
  feedback: {
2255
2707
  no: "Нет",
2256
2708
  question: "Эта страница была полезной?",
@@ -2297,6 +2749,7 @@ var sk = {
2297
2749
  send: "Odoslať",
2298
2750
  title: "Opýtať sa AI"
2299
2751
  },
2752
+ banner: { dismiss: "Zavrieť oznámenie" },
2300
2753
  feedback: {
2301
2754
  no: "Nie",
2302
2755
  question: "Bola táto stránka užitočná?",
@@ -2343,6 +2796,7 @@ var sr = {
2343
2796
  send: "Пошаљи",
2344
2797
  title: "Питајте AI"
2345
2798
  },
2799
+ banner: { dismiss: "Затвори обавештење" },
2346
2800
  feedback: {
2347
2801
  no: "Не",
2348
2802
  question: "Да ли вам је ова страница помогла?",
@@ -2389,6 +2843,7 @@ var sv = {
2389
2843
  send: "Skicka",
2390
2844
  title: "Fråga AI"
2391
2845
  },
2846
+ banner: { dismiss: "Stäng meddelandet" },
2392
2847
  feedback: {
2393
2848
  no: "Nej",
2394
2849
  question: "Var den här sidan till hjälp?",
@@ -2435,6 +2890,7 @@ var th = {
2435
2890
  send: "ส่ง",
2436
2891
  title: "ถาม AI"
2437
2892
  },
2893
+ banner: { dismiss: "ปิดประกาศ" },
2438
2894
  feedback: {
2439
2895
  no: "ไม่",
2440
2896
  question: "หน้านี้มีประโยชน์หรือไม่?",
@@ -2481,6 +2937,7 @@ var tr = {
2481
2937
  send: "Gönder",
2482
2938
  title: "Yapay zekâya sor"
2483
2939
  },
2940
+ banner: { dismiss: "Duyuruyu kapat" },
2484
2941
  feedback: {
2485
2942
  no: "Hayır",
2486
2943
  question: "Bu sayfa yardımcı oldu mu?",
@@ -2527,6 +2984,7 @@ var uk = {
2527
2984
  send: "Надіслати",
2528
2985
  title: "Запитати ШІ"
2529
2986
  },
2987
+ banner: { dismiss: "Закрити оголошення" },
2530
2988
  feedback: {
2531
2989
  no: "Ні",
2532
2990
  question: "Чи була ця сторінка корисною?",
@@ -2573,6 +3031,7 @@ var vi = {
2573
3031
  send: "Gửi",
2574
3032
  title: "Hỏi AI"
2575
3033
  },
3034
+ banner: { dismiss: "Đóng thông báo" },
2576
3035
  feedback: {
2577
3036
  no: "Không",
2578
3037
  question: "Trang này có hữu ích không?",
@@ -2619,6 +3078,7 @@ var zhTW = {
2619
3078
  send: "傳送",
2620
3079
  title: "向 AI 提問"
2621
3080
  },
3081
+ banner: { dismiss: "關閉公告" },
2622
3082
  feedback: {
2623
3083
  no: "沒有幫助",
2624
3084
  question: "這個頁面有幫助嗎?",
@@ -2665,6 +3125,7 @@ var zh = {
2665
3125
  send: "发送",
2666
3126
  title: "向 AI 提问"
2667
3127
  },
3128
+ banner: { dismiss: "关闭公告" },
2668
3129
  feedback: {
2669
3130
  no: "没有帮助",
2670
3131
  question: "这个页面有帮助吗?",
@@ -2903,9 +3364,16 @@ var toPlainText = (markdown) => {
2903
3364
  };
2904
3365
  var buildCrumbIndex = (sidebar) => {
2905
3366
  const index = new Map;
3367
+ const groupRoutes = new Map;
2906
3368
  const walk = (nodes, trail) => {
2907
3369
  for (const node of nodes) {
2908
3370
  if (node.kind === "group") {
3371
+ if (node.route && !groupRoutes.has(node.route)) {
3372
+ groupRoutes.set(node.route, {
3373
+ breadcrumb: [...trail, node.label],
3374
+ section: node.label
3375
+ });
3376
+ }
2909
3377
  walk(node.children, [...trail, node.label]);
2910
3378
  } else if (node.route) {
2911
3379
  index.set(node.route, {
@@ -2916,6 +3384,11 @@ var buildCrumbIndex = (sidebar) => {
2916
3384
  }
2917
3385
  };
2918
3386
  walk(sidebar, []);
3387
+ for (const [route, crumbs] of groupRoutes) {
3388
+ if (!index.has(route)) {
3389
+ index.set(route, crumbs);
3390
+ }
3391
+ }
2919
3392
  return index;
2920
3393
  };
2921
3394
  var buildSearchDocuments = async (project, options) => {
@@ -2939,7 +3412,8 @@ var buildSearchDocuments = async (project, options) => {
2939
3412
  const page = pageById.get(route.id);
2940
3413
  const raw = page ? await readEntryText(project, page) : "";
2941
3414
  const source = raw ? frontmatter_default(raw).content : "";
2942
- const body = options?.content === "markdown" ? source.trim() : toPlainText(source);
3415
+ const visible = applyAudienceVisibility(source, options?.audience ?? "web");
3416
+ const body = options?.content === "markdown" ? visible.trim() : toPlainText(visible);
2943
3417
  const tags = page?.meta?.search?.tags;
2944
3418
  const crumb = crumbs.get(route.path);
2945
3419
  return {
@@ -3235,10 +3709,11 @@ var updateDevLockPort = (outDir, port) => {
3235
3709
  }
3236
3710
  };
3237
3711
  var describeDevLock = (lock) => lock.port === undefined ? "" : ` at http://localhost:${lock.port}`;
3238
- var refuseIfDevRunning = (root, action, runtimeDir) => {
3239
- const lock = readDevLock(resolveRuntimeDir(root, runtimeDir));
3712
+ var refuseIfDevRunning = (root, action, options = {}) => {
3713
+ const lock = readDevLock(resolveRuntimeDir(root, options.runtimeDir));
3240
3714
  if (lock) {
3241
- logger.error(`A \`blume dev\` server is running${describeDevLock(lock)}; ${action} would corrupt its .blume runtime. Reuse that server, stop it first, or re-run with --isolated to build/verify against .blume-verify without touching it.`);
3715
+ const remedies = options.isolatedHint ? "Reuse that server, stop it first, or re-run with --isolated to build/verify against .blume-verify without touching it." : "Reuse that server or stop it first.";
3716
+ logger.error(`A \`blume dev\` server is running${describeDevLock(lock)}; ${action} would corrupt its .blume runtime. ${remedies}`);
3242
3717
  process.exit(1);
3243
3718
  }
3244
3719
  };
@@ -3262,6 +3737,7 @@ import { glob as glob5 } from "tinyglobby";
3262
3737
  // src/ai/ask-data.ts
3263
3738
  var buildAskData = async (project) => {
3264
3739
  const documents = await buildSearchDocuments(project, {
3740
+ audience: "agents",
3265
3741
  content: "markdown",
3266
3742
  includeWhenDisabled: true
3267
3743
  });
@@ -3332,6 +3808,7 @@ var askBackendRuntimeDep = (ask) => {
3332
3808
 
3333
3809
  // src/ai/markdown.ts
3334
3810
  import { readFile as readFile4 } from "node:fs/promises";
3811
+ var agentMarkdown = (entry) => entry.md ?? entry.mdx;
3335
3812
  var buildRawMarkdown = async (project) => {
3336
3813
  const pageById = new Map(project.graph.pages.map((page) => [page.id, page]));
3337
3814
  const readRoute = async (route) => {
@@ -3341,17 +3818,29 @@ var buildRawMarkdown = async (project) => {
3341
3818
  }
3342
3819
  return route.sourcePath ? await readFile4(route.sourcePath, "utf-8") : "";
3343
3820
  };
3344
- const entries = await Promise.all(project.manifest.routes.map(async (route) => [route.path, await readRoute(route)]));
3821
+ const entries = await Promise.all(project.manifest.routes.map(async (route) => {
3822
+ const source = applyAgentVisibility(await readRoute(route));
3823
+ const md = downlevelComponents(source, project.config.ai.markdownComponents);
3824
+ const entry = md === source ? { mdx: source } : { md, mdx: source };
3825
+ return [route.path, entry];
3826
+ }));
3345
3827
  return Object.fromEntries(entries);
3346
3828
  };
3347
3829
 
3348
3830
  // src/ai/mcp/data.ts
3349
3831
  var buildMcpData = async (project) => {
3350
3832
  const { config, graph, manifest } = project;
3351
- const [documents, pages] = await Promise.all([
3352
- buildSearchDocuments(project, { includeWhenDisabled: true }),
3833
+ const [documents, rawMarkdown] = await Promise.all([
3834
+ buildSearchDocuments(project, {
3835
+ audience: "agents",
3836
+ includeWhenDisabled: true
3837
+ }),
3353
3838
  buildRawMarkdown(project)
3354
3839
  ]);
3840
+ const pages = Object.fromEntries(Object.entries(rawMarkdown).map(([route, entry]) => [
3841
+ route,
3842
+ agentMarkdown(entry)
3843
+ ]));
3355
3844
  const descriptionById = new Map(graph.pages.map((page) => [page.id, page.description]));
3356
3845
  const routes = [];
3357
3846
  for (const route of manifest.routes) {
@@ -3368,6 +3857,7 @@ var buildMcpData = async (project) => {
3368
3857
  });
3369
3858
  }
3370
3859
  return {
3860
+ base: normalizeBasePath(config.deployment.base),
3371
3861
  documents: documents.map((doc) => ({
3372
3862
  content: doc.content,
3373
3863
  description: doc.description,
@@ -3414,7 +3904,10 @@ var MCP_TOOLS = [
3414
3904
  ];
3415
3905
 
3416
3906
  // src/ai/mcp/discovery.ts
3417
- var serverUrl = (input) => input.site ? `${input.site.replace(/\/+$/u, "")}${input.route}` : input.route;
3907
+ var serverUrl = (input) => {
3908
+ const path = withBasePath(input.base, input.route);
3909
+ return input.site ? `${input.site.replace(/\/+$/u, "")}${path}` : path;
3910
+ };
3418
3911
  var buildMcpDiscovery = (input) => ({
3419
3912
  servers: [
3420
3913
  {
@@ -3504,7 +3997,7 @@ var validateUsedComponents = (pages, extraTags, registryNames) => {
3504
3997
 
3505
3998
  // src/core/component-overrides.ts
3506
3999
  import { existsSync as existsSync7 } from "node:fs";
3507
- import { dirname as dirname5, extname, isAbsolute as isAbsolute2, resolve as resolve3 } from "pathe";
4000
+ import { dirname as dirname5, extname as extname2, isAbsolute as isAbsolute2, resolve as resolve3 } from "pathe";
3508
4001
  import ts from "typescript";
3509
4002
  var GROUPS = ["mdx", "layout", "islands"];
3510
4003
  var GROUP_SET = new Set(GROUPS);
@@ -3603,17 +4096,17 @@ var probeExtension = (base) => {
3603
4096
  return null;
3604
4097
  };
3605
4098
  var toImport = (specifier, imported, dir) => {
3606
- const relative4 = specifier.startsWith(".") || isAbsolute2(specifier);
4099
+ const relative5 = specifier.startsWith(".") || isAbsolute2(specifier);
3607
4100
  let path = specifier;
3608
- let extension = extname(specifier).slice(1).toLowerCase();
3609
- if (relative4) {
4101
+ let extension = extname2(specifier).slice(1).toLowerCase();
4102
+ if (relative5) {
3610
4103
  const absolute = isAbsolute2(specifier) ? specifier : resolve3(dir, specifier);
3611
4104
  if (extension) {
3612
4105
  path = absolute;
3613
4106
  } else {
3614
4107
  const probed = probeExtension(absolute);
3615
4108
  path = probed ?? absolute;
3616
- extension = probed ? extname(probed).slice(1).toLowerCase() : "";
4109
+ extension = probed ? extname2(probed).slice(1).toLowerCase() : "";
3617
4110
  }
3618
4111
  }
3619
4112
  return {
@@ -3779,14 +4272,20 @@ var uiStringsObject = z.object({
3779
4272
  connectMcp: z.string().default("Connect to MCP"),
3780
4273
  copied: z.string().default("Copied!"),
3781
4274
  copyClaudeCode: z.string().default("Copy Claude Code command"),
4275
+ copyCode: z.string().default("Copy code"),
3782
4276
  copyCodex: z.string().default("Copy Codex command"),
3783
4277
  copyMarkdown: z.string().default("Copy as Markdown"),
3784
4278
  copyServerUrl: z.string().default("Copy server URL"),
3785
4279
  edit: z.string().default("Edit on GitHub"),
4280
+ export: z.string().default("Export"),
4281
+ exportEpub: z.string().default("Export to EPUB"),
4282
+ exportPdf: z.string().default("Export to PDF"),
4283
+ generating: z.string().default("Generating…"),
3786
4284
  openInChat: z.string().default("Open in chat"),
3787
4285
  scrollToTop: z.string().default("Scroll to top")
3788
4286
  }).default({}),
3789
4287
  ask: z.object({
4288
+ ai: z.string().default("AI"),
3790
4289
  clear: z.string().default("Clear conversation"),
3791
4290
  close: z.string().default("Close"),
3792
4291
  copy: z.string().default("Copy conversation"),
@@ -3796,7 +4295,17 @@ var uiStringsObject = z.object({
3796
4295
  placeholder: z.string().default("Ask a question…"),
3797
4296
  send: z.string().default("Send"),
3798
4297
  tip: z.string().default("Tip: You can open and close chat with"),
3799
- title: z.string().default("Ask AI")
4298
+ title: z.string().default("Ask AI"),
4299
+ you: z.string().default("You")
4300
+ }).default({}),
4301
+ banner: z.object({
4302
+ dismiss: z.string().default("Dismiss announcement")
4303
+ }).default({}),
4304
+ changelog: z.object({
4305
+ showReleases: z.string().default("Show {version} releases")
4306
+ }).default({}),
4307
+ content: z.object({
4308
+ diagramError: z.string().default("Could not render this diagram.")
3800
4309
  }).default({}),
3801
4310
  feedback: z.object({
3802
4311
  no: z.string().default("No"),
@@ -3808,6 +4317,18 @@ var uiStringsObject = z.object({
3808
4317
  label: z.string().default("Language"),
3809
4318
  untranslated: z.string().default("Not translated")
3810
4319
  }).default({}),
4320
+ nav: z.object({
4321
+ back: z.string().default("Back"),
4322
+ closeNavigation: z.string().default("Close navigation"),
4323
+ deprecated: z.string().default("deprecated"),
4324
+ featured: z.string().default("Featured"),
4325
+ githubRepository: z.string().default("GitHub repository"),
4326
+ navigation: z.string().default("Navigation"),
4327
+ primary: z.string().default("Primary"),
4328
+ sections: z.string().default("Sections"),
4329
+ toggleNavigation: z.string().default("Toggle navigation"),
4330
+ toggleTheme: z.string().default("Toggle color theme")
4331
+ }).default({}),
3811
4332
  notFound: z.object({
3812
4333
  description: z.string().default("We couldn't find the page you're looking for."),
3813
4334
  home: z.string().default("Back to home"),
@@ -3821,11 +4342,19 @@ var uiStringsObject = z.object({
3821
4342
  }).default({}),
3822
4343
  search: z.object({
3823
4344
  allLanguages: z.string().default("All languages"),
4345
+ askAi: z.string().default("Ask AI"),
4346
+ askAiHint: z.string().default("Get an instant answer from AI"),
3824
4347
  button: z.string().default("Search"),
3825
4348
  devOnly: z.string().default("Search is available in the production build."),
4349
+ error: z.string().default("Something went wrong. Please try again."),
3826
4350
  label: z.string().default("Search docs"),
4351
+ navigate: z.string().default("navigate"),
3827
4352
  noResults: z.string().default("No results found."),
3828
- placeholder: z.string().default("Search documentation…")
4353
+ open: z.string().default("open"),
4354
+ placeholder: z.string().default("Search documentation…"),
4355
+ popular: z.string().default("Popular"),
4356
+ preview: z.string().default("preview"),
4357
+ results: z.string().default("Results")
3829
4358
  }).default({}),
3830
4359
  toc: z.object({
3831
4360
  title: z.string().default("On this page")
@@ -4060,7 +4589,7 @@ var sourcesOf = (block) => {
4060
4589
  }
4061
4590
  return sources;
4062
4591
  };
4063
- var referencesFor = (kind, block, defaultLabel, renderer, display) => {
4592
+ var referencesFor = (kind, block, defaultLabel, renderer, display, basePath) => {
4064
4593
  if (!block.enabled) {
4065
4594
  return [];
4066
4595
  }
@@ -4078,6 +4607,7 @@ var referencesFor = (kind, block, defaultLabel, renderer, display) => {
4078
4607
  route = normalizeRoute(`${base}/${suffix || index + 1}`);
4079
4608
  }
4080
4609
  return {
4610
+ basePath,
4081
4611
  display,
4082
4612
  kind,
4083
4613
  label,
@@ -4094,8 +4624,8 @@ var resolveReferences = (config) => [
4094
4624
  ...referencesFor("openapi", config.openapi, "API Reference", config.openapi.renderer, {
4095
4625
  codeSamples: config.openapi.codeSamples,
4096
4626
  expandSchemas: config.openapi.expandSchemas
4097
- }),
4098
- ...referencesFor("asyncapi", config.asyncapi, "Events", "scalar", NO_DISPLAY)
4627
+ }, config.basePath),
4628
+ ...referencesFor("asyncapi", config.asyncapi, "Events", "scalar", NO_DISPLAY, config.basePath)
4099
4629
  ];
4100
4630
  var referenceTabs = (config) => resolveReferences(config).map((ref) => ({
4101
4631
  label: ref.label,
@@ -4105,10 +4635,11 @@ var blumeReferenceOf = (ref, seen, usedSlugs) => {
4105
4635
  if (ref.kind !== "openapi" || ref.renderer !== "blume") {
4106
4636
  return null;
4107
4637
  }
4108
- if (seen.has(ref.route)) {
4638
+ const kept = seen.get(ref.route);
4639
+ if (kept) {
4640
+ (kept.collisions ??= []).push(`Two API reference sources resolve to ${ref.route}; keeping the first.`);
4109
4641
  return null;
4110
4642
  }
4111
- seen.add(ref.route);
4112
4643
  let { slug } = ref;
4113
4644
  let n = 2;
4114
4645
  while (usedSlugs.has(slug)) {
@@ -4116,10 +4647,12 @@ var blumeReferenceOf = (ref, seen, usedSlugs) => {
4116
4647
  n += 1;
4117
4648
  }
4118
4649
  usedSlugs.add(slug);
4119
- return slug === ref.slug ? ref : { ...ref, slug };
4650
+ const accepted = slug === ref.slug ? ref : { ...ref, slug };
4651
+ seen.set(ref.route, accepted);
4652
+ return accepted;
4120
4653
  };
4121
4654
  var blumeReferences = (config) => {
4122
- const seen = new Set;
4655
+ const seen = new Map;
4123
4656
  const usedSlugs = new Set;
4124
4657
  const result = [];
4125
4658
  for (const ref of resolveReferences(config)) {
@@ -4243,9 +4776,14 @@ var extractOperations = (document, baseRoute) => {
4243
4776
  const tagsSeen = new Set;
4244
4777
  const tagMeta = new Map((document.tags ?? []).map((tag) => [tag.name, tag.description ?? ""]));
4245
4778
  const seen = new Set;
4779
+ const warnings = [];
4246
4780
  for (const [path, rawItem] of Object.entries(document.paths ?? {})) {
4247
4781
  const item = rawItem;
4248
- if (!item || "$ref" in item) {
4782
+ if (!item) {
4783
+ continue;
4784
+ }
4785
+ if ("$ref" in item) {
4786
+ warnings.push(`Path "${path}" is a $ref to a shared path item; referenced path items are not resolved, so its operations are missing from the reference. Inline the path item under "paths" to render it.`);
4249
4787
  continue;
4250
4788
  }
4251
4789
  for (const method of HTTP_METHODS) {
@@ -4283,7 +4821,7 @@ var extractOperations = (document, baseRoute) => {
4283
4821
  name,
4284
4822
  slug: slugify2(name) || "operations"
4285
4823
  }));
4286
- return { operations, tags };
4824
+ return { operations, tags, warnings };
4287
4825
  };
4288
4826
 
4289
4827
  // src/openapi/parse.ts
@@ -4307,6 +4845,13 @@ var PROXY_ENV_VARS = [
4307
4845
  "ALL_PROXY",
4308
4846
  "all_proxy"
4309
4847
  ];
4848
+
4849
+ class InvalidSpecError extends Error {
4850
+ constructor(message) {
4851
+ super(message);
4852
+ this.name = "InvalidSpecError";
4853
+ }
4854
+ }
4310
4855
  var proxyInstalled = false;
4311
4856
  var ensureProxyDispatcher = async () => {
4312
4857
  if (proxyInstalled || !PROXY_ENV_VARS.some((name) => process.env[name])) {
@@ -4417,6 +4962,9 @@ var parseSpec = async (spec, root, options = {}) => {
4417
4962
  const { text, warnings } = await readSpecText(spec, root, options);
4418
4963
  const normalized = normalize2(text);
4419
4964
  const { specification } = upgrade(normalized);
4965
+ if (specification === null || typeof specification !== "object") {
4966
+ throw new InvalidSpecError(`${spec} is not a valid OpenAPI document (expected a YAML or JSON object).`);
4967
+ }
4420
4968
  return { document: specification, warnings };
4421
4969
  };
4422
4970
 
@@ -4533,7 +5081,11 @@ var openApiSource = (references, ctx) => {
4533
5081
  const loadReference = async (reference) => {
4534
5082
  try {
4535
5083
  const { document, warnings } = await parseSpec(reference.spec, ctx.projectRoot, { cacheDir: ctx.cacheDir, refresh: ctx.refresh });
4536
- const { operations, tags } = extractOperations(document, reference.route);
5084
+ const {
5085
+ operations,
5086
+ tags,
5087
+ warnings: extractWarnings
5088
+ } = extractOperations(document, reference.route);
4537
5089
  const info = document.info ?? { title: reference.label, version: "" };
4538
5090
  const spec = {
4539
5091
  codeSamples: reference.display.codeSamples,
@@ -4541,7 +5093,13 @@ var openApiSource = (references, ctx) => {
4541
5093
  document,
4542
5094
  expandSchemas: reference.display.expandSchemas,
4543
5095
  label: reference.label,
4544
- operations: Object.fromEntries(operations.map((operation) => [operation.key, operation])),
5096
+ operations: Object.fromEntries(operations.map((operation) => [
5097
+ operation.key,
5098
+ {
5099
+ ...operation,
5100
+ route: withBasePath(reference.basePath, operation.route)
5101
+ }
5102
+ ])),
4545
5103
  route: reference.route,
4546
5104
  slug: reference.slug,
4547
5105
  tags,
@@ -4549,11 +5107,26 @@ var openApiSource = (references, ctx) => {
4549
5107
  version: info.version ?? ""
4550
5108
  };
4551
5109
  return {
4552
- diagnostics: warnings.map((message) => ({
4553
- code: "BLUME_OPENAPI_STALE",
4554
- message,
4555
- severity: "warning"
4556
- })),
5110
+ diagnostics: [
5111
+ ...warnings.map((message) => ({
5112
+ code: "BLUME_OPENAPI_STALE",
5113
+ message,
5114
+ severity: "warning"
5115
+ })),
5116
+ ...extractWarnings.map((message) => ({
5117
+ code: "BLUME_OPENAPI_REF_PATH_ITEM",
5118
+ message: `In OpenAPI spec "${reference.spec}": ${message}`,
5119
+ severity: "warning"
5120
+ })),
5121
+ ...operations.length === 0 ? [
5122
+ {
5123
+ code: "BLUME_OPENAPI_EMPTY",
5124
+ message: `OpenAPI spec "${reference.spec}" for ${reference.route} declares no operations; its API reference is empty.`,
5125
+ severity: "warning",
5126
+ suggestion: "Check the spec points at an OpenAPI document with operations under `paths`."
5127
+ }
5128
+ ] : []
5129
+ ],
4557
5130
  entries: specEntries(spec, operations),
4558
5131
  slug: reference.slug,
4559
5132
  spec
@@ -4563,14 +5136,18 @@ var openApiSource = (references, ctx) => {
4563
5136
  code: "BLUME_OPENAPI_UNAVAILABLE",
4564
5137
  message: `Could not load OpenAPI spec "${reference.spec}" for ${reference.route} (${error.message}); its reference pages were skipped.`,
4565
5138
  severity: ctx.mode === "build" ? "error" : "warning",
4566
- suggestion: "Check the spec URL/path is reachable from the build environment; behind a proxy, set HTTP(S)_PROXY."
5139
+ 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."
4567
5140
  };
4568
5141
  }
4569
5142
  };
4570
5143
  const load2 = async () => {
4571
5144
  const results = await Promise.all(references.map(loadReference));
4572
5145
  const entries = [];
4573
- const diagnostics = [];
5146
+ const diagnostics = references.flatMap((reference) => (reference.collisions ?? []).map((message) => ({
5147
+ code: "BLUME_OPENAPI_ROUTE_COLLISION",
5148
+ message,
5149
+ severity: "warning"
5150
+ })));
4574
5151
  const data = {};
4575
5152
  for (const result of results) {
4576
5153
  if ("severity" in result) {
@@ -4596,8 +5173,8 @@ var openApiSource = (references, ctx) => {
4596
5173
  // src/core/sources/filesystem.ts
4597
5174
  import { existsSync as existsSync8, watch as fsWatch } from "node:fs";
4598
5175
  import { readFile as readFile7 } from "node:fs/promises";
4599
- import { extname as extname2, isAbsolute as isAbsolute4, join as join12, relative as relative4, resolve as resolve4 } from "pathe";
4600
- import { glob } from "tinyglobby";
5176
+ import { extname as extname3, isAbsolute as isAbsolute4, join as join12, relative as relative5, resolve as resolve4 } from "pathe";
5177
+ import { glob as glob2 } from "tinyglobby";
4601
5178
 
4602
5179
  // src/core/sources/watch.ts
4603
5180
  var BLUME_IGNORE_DIRS = [
@@ -4627,7 +5204,7 @@ var ignoringWatchListener = (onChange, ignoreDirs = BLUME_WATCH_IGNORE_DIRS) =>
4627
5204
  var filesystemSource = (options) => {
4628
5205
  const contentRoot = isAbsolute4(options.root) ? options.root : join12(resolve4(options.projectRoot), options.root);
4629
5206
  const load2 = async () => {
4630
- const files = await glob(options.include, {
5207
+ const files = await glob2(options.include, {
4631
5208
  absolute: true,
4632
5209
  cwd: contentRoot,
4633
5210
  ignore: [...options.exclude, ...baselineScanIgnore()],
@@ -4636,13 +5213,14 @@ var filesystemSource = (options) => {
4636
5213
  files.sort();
4637
5214
  const entries = await Promise.all(files.map(async (file) => {
4638
5215
  const source = await readFile7(file, "utf-8");
4639
- const ext = extname2(file).toLowerCase();
5216
+ const ext = extname3(file).toLowerCase();
4640
5217
  const format = ext === ".mdx" ? "mdx" : "md";
4641
5218
  const parsed = frontmatter_default(source);
4642
5219
  return {
4643
5220
  body: { format, text: parsed.content },
4644
5221
  data: parsed.data,
4645
- ref: relative4(contentRoot, file),
5222
+ raw: source,
5223
+ ref: relative5(contentRoot, file),
4646
5224
  sourcePath: file
4647
5225
  };
4648
5226
  }));
@@ -4878,14 +5456,9 @@ var mdxRemoteSource = (options, ctx) => {
4878
5456
  const doFetch = options.fetchImpl ?? globalThis.fetch;
4879
5457
  const cache = snapshotCache(ctx.cacheDir);
4880
5458
  let snapshot = new Map;
4881
- const enumerate = async () => {
4882
- if (options.github) {
4883
- return await enumerateGithub(options.github, options.include, doFetch);
4884
- }
4885
- if (options.files && options.url) {
4886
- const base = options.url.replace(/\/$/u, "");
4887
- const refs = options.files.flatMap((ref) => matchesInclude(ref, options.include) ? [{ editUrl: `${base}/${ref}`, fetchUrl: `${base}/${ref}`, ref }] : []);
4888
- return { refs, truncated: false };
5459
+ const assertConfigured = () => {
5460
+ if (options.github || options.files && options.url) {
5461
+ return;
4889
5462
  }
4890
5463
  throw new BlumeError({
4891
5464
  code: "BLUME_SOURCE_MISCONFIGURED",
@@ -4893,6 +5466,14 @@ var mdxRemoteSource = (options, ctx) => {
4893
5466
  severity: "error"
4894
5467
  });
4895
5468
  };
5469
+ const enumerate = async () => {
5470
+ if (options.github) {
5471
+ return await enumerateGithub(options.github, options.include, doFetch);
5472
+ }
5473
+ const base = (options.url ?? "").replace(/\/$/u, "");
5474
+ const refs = (options.files ?? []).flatMap((ref) => matchesInclude(ref, options.include) ? [{ editUrl: `${base}/${ref}`, fetchUrl: `${base}/${ref}`, ref }] : []);
5475
+ return { refs, truncated: false };
5476
+ };
4896
5477
  const fetchEntry = async (item) => {
4897
5478
  const res = await doFetch(item.fetchUrl, {
4898
5479
  headers: githubHeaders2(item.fetchUrl)
@@ -4913,6 +5494,7 @@ var mdxRemoteSource = (options, ctx) => {
4913
5494
  };
4914
5495
  };
4915
5496
  const load2 = async (refresh = ctx.refresh ?? true) => {
5497
+ assertConfigured();
4916
5498
  const skipped = [];
4917
5499
  const result = await loadWithCache(options.name, cache, async () => {
4918
5500
  const { refs, truncated } = await enumerate();
@@ -4973,22 +5555,22 @@ import { join as join14 } from "pathe";
4973
5555
 
4974
5556
  // src/core/sources/assets.ts
4975
5557
  import { mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
4976
- import { extname as extname3, join as join13 } from "pathe";
5558
+ import { extname as extname4, join as join13 } from "pathe";
4977
5559
  var MD_IMAGE = /!\[(?<alt>[^\]]*)\]\((?<url>[^)\s]+)\)/gu;
4978
5560
  var REMOTE = /^https?:\/\//u;
4979
5561
  var SAFE_EXT = /^\.[a-z0-9]+$/iu;
4980
- var CODE_FENCE_BLOCK = /^(?<fence>`{3,}|~{3,})[^\n]*\n[\s\S]*?^\k<fence>[^\n]*(?=\n|$)/gmu;
4981
- var FENCE_TOKEN = /\u0000blume-fence-(?<index>\d+)\u0000/gu;
5562
+ var CODE_FENCE_BLOCK2 = /^(?<fence>`{3,}|~{3,})[^\n]*\n[\s\S]*?^\k<fence>[^\n]*(?=\n|$)/gmu;
5563
+ var FENCE_TOKEN2 = /\u0000blume-fence-(?<index>\d+)\u0000/gu;
4982
5564
  var extFor = (url) => {
4983
5565
  const clean = url.split("?")[0] ?? url;
4984
- const ext = extname3(clean);
5566
+ const ext = extname4(clean);
4985
5567
  return SAFE_EXT.test(ext) ? ext.toLowerCase() : ".png";
4986
5568
  };
4987
5569
  var materializeAssets = async (markdown, ctx) => {
4988
5570
  const doFetch = ctx.fetchImpl ?? globalThis.fetch;
4989
5571
  const diagnostics = [];
4990
5572
  const fences = [];
4991
- const masked = markdown.replace(CODE_FENCE_BLOCK, (block) => {
5573
+ const masked = markdown.replace(CODE_FENCE_BLOCK2, (block) => {
4992
5574
  fences.push(block);
4993
5575
  return `\x00blume-fence-${fences.length - 1}\x00`;
4994
5576
  });
@@ -5007,7 +5589,7 @@ var materializeAssets = async (markdown, ctx) => {
5007
5589
  throw new Error(`${res.status}`);
5008
5590
  }
5009
5591
  const bytes = new Uint8Array(await res.arrayBuffer());
5010
- const file = `${hashText(url)}${extFor(url)}`;
5592
+ const file = `${hashText(url.split("?")[0] ?? url)}${extFor(url)}`;
5011
5593
  await mkdir5(ctx.assetsDir, { recursive: true });
5012
5594
  await writeFile5(join13(ctx.assetsDir, file), bytes);
5013
5595
  rewrites.set(url, `${ctx.assetsBaseUrl}/${file}`);
@@ -5022,14 +5604,14 @@ var materializeAssets = async (markdown, ctx) => {
5022
5604
  const rewritten = masked.replaceAll(MD_IMAGE, (match, alt, url) => {
5023
5605
  const local = rewrites.get(url);
5024
5606
  return local ? `![${alt}](${local})` : match;
5025
- }).replaceAll(FENCE_TOKEN, (token, index) => fences[Number(index)] ?? token);
5607
+ }).replaceAll(FENCE_TOKEN2, (token, index) => fences[Number(index)] ?? token);
5026
5608
  return { diagnostics, markdown: rewritten };
5027
5609
  };
5028
5610
 
5029
5611
  // src/core/sources/normalize.ts
5030
5612
  import { existsSync as existsSync9, readFileSync as readFileSync3 } from "node:fs";
5031
5613
  import GithubSlugger from "github-slugger";
5032
- import { extname as extname4 } from "pathe";
5614
+ import { extname as extname5 } from "pathe";
5033
5615
 
5034
5616
  // src/core/schema.ts
5035
5617
  import { z as z2 } from "zod";
@@ -5503,7 +6085,16 @@ var aiConfigSchema = z2.strictObject({
5503
6085
  });
5504
6086
  }
5505
6087
  }).optional(),
5506
- llmsTxt: z2.boolean().default(true)
6088
+ llmsTxt: z2.union([
6089
+ z2.boolean(),
6090
+ z2.strictObject({
6091
+ enabled: z2.boolean().default(true),
6092
+ openapi: z2.boolean().default(true)
6093
+ })
6094
+ ]).default(true).transform((value) => typeof value === "boolean" ? { enabled: value, openapi: true } : value),
6095
+ markdownComponents: z2.record(z2.custom((value) => typeof value === "function", {
6096
+ message: "Expected a serializer function."
6097
+ })).default({})
5507
6098
  });
5508
6099
  var featuredLinkSchema = z2.strictObject({
5509
6100
  href: z2.string(),
@@ -5534,7 +6125,7 @@ var mcpConfigSchema = z2.strictObject({
5534
6125
  enabled: z2.boolean().default(false),
5535
6126
  instructions: z2.string().optional(),
5536
6127
  name: z2.string().optional(),
5537
- route: z2.string().default("/mcp")
6128
+ route: z2.string().default("/mcp").transform(normalizeRoute)
5538
6129
  });
5539
6130
  var localeSchema = z2.strictObject({
5540
6131
  code: z2.string().min(1),
@@ -5751,7 +6342,7 @@ var addRouteSegment = (part, segments, groups) => {
5751
6342
  segments.push(clean);
5752
6343
  };
5753
6344
  var mapRoute = (relativePath) => {
5754
- const withoutExt = relativePath.slice(0, relativePath.length - extname4(relativePath).length);
6345
+ const withoutExt = relativePath.slice(0, relativePath.length - extname5(relativePath).length);
5755
6346
  const rawParts = withoutExt.split("/");
5756
6347
  const segments = [];
5757
6348
  const groups = [];
@@ -5761,41 +6352,78 @@ var mapRoute = (relativePath) => {
5761
6352
  const route = segments.length === 0 ? "/" : `/${segments.join("/")}`;
5762
6353
  return { groups, route, segments };
5763
6354
  };
5764
- var CODE_FENCE2 = /^```/u;
5765
- var ATX_HEADING = /^(?<hashes>#{1,6})\s+(?<text>.+?)(?:\s+#+)?\s*$/u;
5766
- var scanHeadingLine = (line, inFence, slugger, headings) => {
5767
- if (CODE_FENCE2.test(line.trimStart())) {
5768
- return !inFence;
6355
+ var CODE_FENCE2 = /^(?<delimiter>```|~~~)/u;
6356
+ var nextFenceState = (line, fence) => {
6357
+ const delimiter = line.trimStart().match(CODE_FENCE2)?.groups?.delimiter;
6358
+ if (delimiter === undefined) {
6359
+ return fence;
6360
+ }
6361
+ if (fence === null) {
6362
+ return delimiter;
6363
+ }
6364
+ return fence === delimiter ? null : fence;
6365
+ };
6366
+ var ATX_HEADING = /^ {0,3}(?<hashes>#{1,6})\s+(?<text>.+?)(?:\s+#+)?\s*$/u;
6367
+ var SETEXT_UNDERLINE = /^ {0,3}(?<marker>=+|-+)\s*$/u;
6368
+ var PARAGRAPH_INTERRUPT = /^ {0,3}(?:[-+*][ \t]|\d{1,9}[.)][ \t]|>)/u;
6369
+ var THEMATIC_BREAK = /^ {0,3}(?:(?:-[ \t]*){3,}|(?:\*[ \t]*){3,}|(?:_[ \t]*){3,})$/u;
6370
+ var FRONT_MATTER_CLOSE = /^(?:-{3}|\.{3})\s*$/u;
6371
+ var linesWithoutFrontMatter = (body) => {
6372
+ const lines = body.split(`
6373
+ `);
6374
+ if (!/^-{3}\s*$/u.test(lines[0] ?? "")) {
6375
+ return lines;
5769
6376
  }
5770
- if (inFence) {
5771
- return inFence;
6377
+ const close = lines.findIndex((line, index) => index > 0 && FRONT_MATTER_CLOSE.test(line));
6378
+ return close === -1 ? lines : lines.slice(close + 1);
6379
+ };
6380
+ var scanHeadingLine = (line, state, slugger, headings) => {
6381
+ const next = nextFenceState(line, state.fence);
6382
+ if (state.fence !== null || next !== null) {
6383
+ state.fence = next;
6384
+ state.paragraph = [];
6385
+ return;
5772
6386
  }
5773
- const match = line.match(ATX_HEADING);
5774
- if (match?.groups) {
5775
- const depth = match.groups.hashes?.length ?? 1;
5776
- const text = (match.groups.text ?? "").trim();
6387
+ const atx = line.match(ATX_HEADING);
6388
+ if (atx?.groups) {
6389
+ const depth = atx.groups.hashes?.length ?? 1;
6390
+ const text = (atx.groups.text ?? "").trim();
5777
6391
  headings.push({ depth, slug: slugger.slug(text), text });
6392
+ state.paragraph = [];
6393
+ return;
6394
+ }
6395
+ const setext = line.match(SETEXT_UNDERLINE);
6396
+ if (setext?.groups && state.paragraph.length > 0) {
6397
+ const text = state.paragraph.join(" ").trim();
6398
+ headings.push({
6399
+ depth: setext.groups.marker?.startsWith("=") ? 1 : 2,
6400
+ slug: slugger.slug(text),
6401
+ text
6402
+ });
6403
+ state.paragraph = [];
6404
+ return;
5778
6405
  }
5779
- return inFence;
6406
+ if (line.trim() === "" || THEMATIC_BREAK.test(line) || PARAGRAPH_INTERRUPT.test(line)) {
6407
+ state.paragraph = [];
6408
+ return;
6409
+ }
6410
+ state.paragraph.push(line.trim());
5780
6411
  };
5781
6412
  var extractHeadings = (body) => {
5782
6413
  const headings = [];
5783
6414
  const slugger = new GithubSlugger;
5784
- let inFence = false;
5785
- for (const line of body.split(`
5786
- `)) {
5787
- inFence = scanHeadingLine(line, inFence, slugger, headings);
6415
+ const state = { fence: null, paragraph: [] };
6416
+ for (const line of linesWithoutFrontMatter(body)) {
6417
+ scanHeadingLine(line, state, slugger, headings);
5788
6418
  }
5789
6419
  return headings;
5790
6420
  };
5791
6421
  var MD_LINK = /\[[^\]]*\]\((?<target>[^)\s]+)(?:\s+"[^"]*")?\)/gu;
5792
6422
  var INLINE_CODE2 = /`[^`]*`/gu;
5793
- var scanLinkLine = (line, lineNumber, inFence, links) => {
5794
- if (CODE_FENCE2.test(line.trimStart())) {
5795
- return !inFence;
5796
- }
5797
- if (inFence) {
5798
- return inFence;
6423
+ var scanLinkLine = (line, lineNumber, fence, links) => {
6424
+ const next = nextFenceState(line, fence);
6425
+ if (fence !== null || next !== null) {
6426
+ return next;
5799
6427
  }
5800
6428
  const masked = line.replaceAll(INLINE_CODE2, (span) => " ".repeat(span.length));
5801
6429
  for (const match of masked.matchAll(MD_LINK)) {
@@ -5810,27 +6438,25 @@ var scanLinkLine = (line, lineNumber, inFence, links) => {
5810
6438
  target
5811
6439
  });
5812
6440
  }
5813
- return inFence;
6441
+ return next;
5814
6442
  };
5815
- var extractLinks = (body) => {
6443
+ var extractLinks = (body, lineOffset = 0) => {
5816
6444
  const links = [];
5817
- let inFence = false;
5818
- let lineNumber = 0;
6445
+ let fence = null;
6446
+ let lineNumber = lineOffset;
5819
6447
  for (const line of body.split(`
5820
6448
  `)) {
5821
6449
  lineNumber += 1;
5822
- inFence = scanLinkLine(line, lineNumber, inFence, links);
6450
+ fence = scanLinkLine(line, lineNumber, fence, links);
5823
6451
  }
5824
6452
  return links;
5825
6453
  };
5826
6454
  var DOUBLE_QUOTED = /"[^"]*"/gu;
5827
6455
  var JSX_OPEN = /<(?<tag>[A-Z][A-Za-z0-9]*)/gu;
5828
- var scanTagLine = (line, inFence, tags) => {
5829
- if (CODE_FENCE2.test(line.trimStart())) {
5830
- return !inFence;
5831
- }
5832
- if (inFence) {
5833
- return inFence;
6456
+ var scanTagLine = (line, fence, tags) => {
6457
+ const next = nextFenceState(line, fence);
6458
+ if (fence !== null || next !== null) {
6459
+ return next;
5834
6460
  }
5835
6461
  const clean = line.replaceAll(INLINE_CODE2, "").replaceAll(DOUBLE_QUOTED, "");
5836
6462
  for (const match of clean.matchAll(JSX_OPEN)) {
@@ -5839,17 +6465,20 @@ var scanTagLine = (line, inFence, tags) => {
5839
6465
  tags.add(tag);
5840
6466
  }
5841
6467
  }
5842
- return inFence;
6468
+ return next;
5843
6469
  };
5844
6470
  var extractComponentTags = (body) => {
5845
6471
  const tags = new Set;
5846
- let inFence = false;
6472
+ let fence = null;
5847
6473
  for (const line of body.split(`
5848
6474
  `)) {
5849
- inFence = scanTagLine(line, inFence, tags);
6475
+ fence = scanTagLine(line, fence, tags);
5850
6476
  }
5851
6477
  return [...tags];
5852
6478
  };
6479
+ var strippedLineOffset = (raw, body) => raw ? Math.max(0, raw.split(`
6480
+ `).length - body.split(`
6481
+ `).length) : 0;
5853
6482
  var deriveTitle = (meta, headings, id2) => {
5854
6483
  if (meta.title) {
5855
6484
  return meta.title;
@@ -5859,7 +6488,7 @@ var deriveTitle = (meta, headings, id2) => {
5859
6488
  return firstHeading.text;
5860
6489
  }
5861
6490
  const base = id2.split("/").pop() ?? id2;
5862
- return titleCase(stripNumericPrefix(base.replace(extname4(base), "")));
6491
+ return titleCase(stripNumericPrefix(base.replace(extname5(base), "")));
5863
6492
  };
5864
6493
  var trimSlashes = (value) => value.replaceAll(/^\/+|\/+$/gu, "");
5865
6494
  var withPrefix = (prefix, path) => {
@@ -5910,7 +6539,7 @@ var normalizeEntry2 = (entry, ctx) => {
5910
6539
  headings,
5911
6540
  id: `${ctx.source.name}:${entry.ref}`,
5912
6541
  lastModified: meta.lastModified ?? entry.lastModified,
5913
- links: extractLinks(entry.body.text),
6542
+ links: extractLinks(entry.body.text, strippedLineOffset(entry.raw, entry.body.text)),
5914
6543
  meta,
5915
6544
  navPath,
5916
6545
  segments,
@@ -6129,12 +6758,9 @@ ${nested}`;
6129
6758
  return Object.values(page.properties).find((p) => p.type === "title");
6130
6759
  };
6131
6760
  const isDraft = (page) => {
6132
- if (!options.publishedValue) {
6133
- return false;
6134
- }
6135
6761
  const prop = page.properties[props.status ?? "Status"];
6136
6762
  const status = prop?.status?.name ?? prop?.select?.name;
6137
- return Boolean(status && status !== options.publishedValue);
6763
+ return Boolean(status && status !== (options.publishedValue ?? "Published"));
6138
6764
  };
6139
6765
  const orderOf = (page) => {
6140
6766
  const order = page.properties[props.order ?? "Order"]?.number;
@@ -6267,7 +6893,7 @@ var renderSpan = (span, defs) => {
6267
6893
  }
6268
6894
  return link?.href ? `[${text}](${link.href})` : text;
6269
6895
  };
6270
- var renderChildren = (block) => {
6896
+ var renderChildren2 = (block) => {
6271
6897
  const defs = new Map((block.markDefs ?? []).map((def) => [def._key, def]));
6272
6898
  return (block.children ?? []).map((span) => renderSpan(span, defs)).join("");
6273
6899
  };
@@ -6284,7 +6910,7 @@ var renderBlock = (block, options) => {
6284
6910
  if (block._type !== "block") {
6285
6911
  return `<!-- unsupported Portable Text block: ${block._type} -->`;
6286
6912
  }
6287
- const inline = renderChildren(block);
6913
+ const inline = renderChildren2(block);
6288
6914
  if (block.listItem) {
6289
6915
  const indent = " ".repeat(Math.max(0, (block.level ?? 1) - 1));
6290
6916
  const marker = block.listItem === "number" ? "1." : "-";
@@ -6708,7 +7334,7 @@ import { isAbsolute as isAbsolute8, join as join19 } from "pathe";
6708
7334
  // src/astro/templates.ts
6709
7335
  import { existsSync as existsSync11, readFileSync as readFileSync5 } from "node:fs";
6710
7336
  import { pathToFileURL as pathToFileURL2 } from "node:url";
6711
- import { dirname as dirname7, isAbsolute as isAbsolute7, join as join18, relative as relative5 } from "pathe";
7337
+ import { dirname as dirname7, isAbsolute as isAbsolute7, join as join18, relative as relative6 } from "pathe";
6712
7338
  var WORKSPACE_MARKERS = [
6713
7339
  ".git",
6714
7340
  "bun.lock",
@@ -6852,11 +7478,12 @@ var astroConfigTemplate = (options) => {
6852
7478
  const twoslashImport = `import { transformerTwoslash } from "@shikijs/twoslash";
6853
7479
  `;
6854
7480
  const twoslashTransformer = "transformerTwoslash({ explicitTrigger: true }), ";
6855
- const contentLinkBase = normalizeBasePath(deployment.base) + config.basePath;
7481
+ const deployBase = normalizeBasePath(deployment.base);
6856
7482
  const integrations = [
6857
7483
  `mdx({ processor: blumeMdxProcessor(${JSON.stringify({
6858
- basePath: contentLinkBase,
7484
+ basePath: config.basePath,
6859
7485
  codeThemes: config.markdown.codeBlocks.theme,
7486
+ deployBase,
6860
7487
  headingAnchors: config.markdown.headingAnchors
6861
7488
  })}) })`
6862
7489
  ];
@@ -6885,8 +7512,9 @@ export default defineConfig({
6885
7512
  integrations: [${integrations.join(", ")}],
6886
7513
  markdown: {
6887
7514
  processor: blumeMarkdownProcessor(${JSON.stringify({
6888
- basePath: contentLinkBase,
7515
+ basePath: config.basePath,
6889
7516
  codeThemes: config.markdown.codeBlocks.theme,
7517
+ deployBase,
6890
7518
  headingAnchors: config.markdown.headingAnchors
6891
7519
  })}),
6892
7520
  shikiConfig: {
@@ -6953,7 +7581,7 @@ var contentConfigTemplate = (options) => {
6953
7581
  const collectionBase = options.collection?.base ?? context.contentRoot;
6954
7582
  const includeGlobs = options.collection?.include ?? config.content.include;
6955
7583
  const excludeGlobs = options.collection?.exclude ?? config.content.exclude;
6956
- const outDirRel = relative5(collectionBase, context.outDir);
7584
+ const outDirRel = relative6(collectionBase, context.outDir);
6957
7585
  const outDirIgnore = outDirRel && !outDirRel.startsWith("..") && !isAbsolute7(outDirRel) ? [`!${outDirRel}/**`] : [];
6958
7586
  const filesystem = options.filesystem ?? true;
6959
7587
  const docsPattern = filesystem ? [
@@ -7041,6 +7669,21 @@ const ground = createAskContext(askData);
7041
7669
  content: m.content,
7042
7670
  role: m.role,
7043
7671
  }));`;
7672
+ const keyCheck = backend.kind === "gateway" ? ` // The AI Gateway authenticates with an API key or Vercel's OIDC token.
7673
+ if (!(process.env.AI_GATEWAY_API_KEY || process.env.VERCEL_OIDC_TOKEN)) {
7674
+ return new Response(
7675
+ "Ask AI is not configured: set AI_GATEWAY_API_KEY (or deploy on Vercel with OIDC).",
7676
+ { status: 500 }
7677
+ );
7678
+ }` : ` if (!process.env[${JSON.stringify(backend.apiKeyEnv)}]) {
7679
+ return new Response(
7680
+ ${JSON.stringify(`Ask AI is not configured: set ${backend.apiKeyEnv}.`)},
7681
+ { status: 500 }
7682
+ );
7683
+ }`;
7684
+ const onError = ` onError({ error }) {
7685
+ console.error("Ask AI provider error:", error);
7686
+ },`;
7044
7687
  const stream = grounded ? ` const system =
7045
7688
  (await ground(messages, body.page)) ??
7046
7689
  "You are a helpful documentation assistant. Answer using the project's documentation.";
@@ -7048,14 +7691,17 @@ const ground = createAskContext(askData);
7048
7691
  model: ${modelExpr},
7049
7692
  system,
7050
7693
  messages,
7694
+ ${onError}
7051
7695
  });` : ` const result = streamText({
7052
7696
  model: ${modelExpr},
7053
7697
  system:
7054
7698
  "You are a helpful documentation assistant. Answer using the project's documentation.",
7055
7699
  messages,
7700
+ ${onError}
7056
7701
  });`;
7057
7702
  const handler = `export const POST: APIRoute = async ({ request }) => {
7058
7703
  ${validate}
7704
+ ${keyCheck}
7059
7705
  try {
7060
7706
  ${stream}
7061
7707
  return result.toTextStreamResponse();
@@ -7182,7 +7828,7 @@ export const POST: APIRoute = async ({ request }) => {
7182
7828
  });
7183
7829
  };
7184
7830
  `;
7185
- var rawMarkdownEndpointTemplate = () => `// Generated by Blume. Do not edit.
7831
+ var rawMarkdownEndpointTemplate = (kind) => `// Generated by Blume. Do not edit.
7186
7832
  import raw from "../generated/raw-markdown.json";
7187
7833
 
7188
7834
  export const prerender = true;
@@ -7195,7 +7841,8 @@ export function getStaticPaths() {
7195
7841
  }
7196
7842
 
7197
7843
  export function GET({ props }) {
7198
- return new Response(raw[props.route] ?? "", {
7844
+ const entry = raw[props.route];
7845
+ return new Response(entry ? ${kind === "md" ? "(entry.md ?? entry.mdx)" : "entry.mdx"} : "", {
7199
7846
  headers: { "Content-Type": "text/markdown; charset=utf-8" },
7200
7847
  });
7201
7848
  }
@@ -7335,6 +7982,7 @@ const configuration = ${JSON.stringify(options.configuration, null, 2)};
7335
7982
  searchEnabled={data.config.search.enabled}
7336
7983
  site={{ title: data.config.title, description: data.config.description }}
7337
7984
  themeMode={data.config.theme.mode}
7985
+ ui={data.ui}
7338
7986
  >
7339
7987
  <ScalarComponent configuration={configuration} renderMode="client" />
7340
7988
  </ReferenceLayout>
@@ -7615,6 +8263,7 @@ var changelogIndexTemplate = (options) => {
7615
8263
  import { getCollection, render } from "astro:content";
7616
8264
  import RootLayout from "blume/components/layout/RootLayout.astro";
7617
8265
  import Update from "blume/components/content/Update.astro";
8266
+ import { withBase } from "blume/components/islands/base-path.ts";
7618
8267
  import { resolveSlot } from "blume/components/layout/overrides.ts";
7619
8268
  import { layoutOverrides } from "../generated/components.ts";
7620
8269
  ${askImport}import data from "../generated/data.json";
@@ -7695,6 +8344,20 @@ const items = await Promise.all(
7695
8344
  })
7696
8345
  );
7697
8346
 
8347
+ // Repeated labels slug to the same id (e.g. two entries with neither a title
8348
+ // nor a version both falling back to "update"); suffix the later ones -2, -3,
8349
+ // ... so every heading deep-links to its own entry. The first keeps the plain
8350
+ // slug, and the rendered ids stay in lockstep with the \`headings\` list below.
8351
+ const seenIds = new Set();
8352
+ for (const item of items) {
8353
+ let uniqueId = item.id;
8354
+ for (let n = 2; seenIds.has(uniqueId); n += 1) {
8355
+ uniqueId = item.id + "-" + n;
8356
+ }
8357
+ seenIds.add(uniqueId);
8358
+ item.id = uniqueId;
8359
+ }
8360
+
7698
8361
  // A changelog is semver-paginated only when every visible release parses as
7699
8362
  // semver and they span more than one major line. Older majors then collapse
7700
8363
  // into groups the reader reveals one at a time; otherwise the timeline is flat.
@@ -7715,7 +8378,20 @@ const headings = items.map((item) => ({
7715
8378
  }));
7716
8379
 
7717
8380
  const base = data.config.site ? data.config.site.replace(/\\/$/, "") : null;
7718
- const canonical = base ? base + "/changelog" : null;
8381
+ // The canonical URL carries the deployment base (the page is served under it),
8382
+ // matching how the catch-all canonicalizes via \`withBase(route)\`.
8383
+ const basedRoute = withBase("/changelog");
8384
+ const canonical = base ? base + basedRoute : null;
8385
+
8386
+ // The changelog is an unlocalized route, so its chrome renders in the default
8387
+ // locale's dictionary and direction (\`data.ui\` is the default locale's resolved
8388
+ // dictionary), mirroring the catch-all's locale wiring.
8389
+ const i18n = data.config.i18n;
8390
+ const localeMeta = i18n
8391
+ ? i18n.locales.find((l) => l.code === i18n.defaultLocale)
8392
+ : null;
8393
+ const dir = localeMeta?.dir ?? "ltr";
8394
+ const htmlLang = i18n ? i18n.defaultLocale : "en";
7719
8395
 
7720
8396
  const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
7721
8397
  ---
@@ -7732,7 +8408,10 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
7732
8408
  imageZoom={data.config.imageZoom}
7733
8409
  codeWrap={data.config.codeWrap}
7734
8410
  navigation={data.navigation}
7735
- page={{
8411
+ locale={htmlLang}
8412
+ dir={dir}
8413
+ ui={data.ui}
8414
+ page={{
7736
8415
  title: data.config.title + " changelog",
7737
8416
  description: "Product updates and release notes.",
7738
8417
  route: "/changelog",
@@ -7759,7 +8438,10 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
7759
8438
  items.length === 0 ? (
7760
8439
  <p>No changelog entries yet.</p>
7761
8440
  ) : paginate ? (
7762
- <blume-changelog class="not-prose mt-8 block">
8441
+ <blume-changelog
8442
+ class="not-prose mt-8 block"
8443
+ data-i18n-more={data.ui.changelog?.showReleases}
8444
+ >
7763
8445
  {majorGroups[0].items.map(({ Content, href, id, label, date, tags }) => (
7764
8446
  <Update description={date} href={href} id={id} label={label} tags={tags}>
7765
8447
  <Content />
@@ -7808,11 +8490,22 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
7808
8490
  var notFoundPageTemplate = () => `---
7809
8491
  // Generated by Blume. Do not edit. Override by adding \`pages/404.astro\`.
7810
8492
  import PageLayout from "blume/components/layout/PageLayout.astro";
8493
+ import { withBase } from "blume/components/islands/base-path.ts";
7811
8494
  import data from "../generated/data.json";
7812
8495
 
7813
8496
  export const prerender = true;
7814
8497
 
7815
8498
  const nf = data.ui.notFound;
8499
+
8500
+ // The 404 page is an unlocalized route, so its chrome renders in the default
8501
+ // locale's dictionary and direction (\`data.ui\` is the default locale's resolved
8502
+ // dictionary), mirroring the catch-all's locale wiring.
8503
+ const i18n = data.config.i18n;
8504
+ const localeMeta = i18n
8505
+ ? i18n.locales.find((l) => l.code === i18n.defaultLocale)
8506
+ : null;
8507
+ const dir = localeMeta?.dir ?? "ltr";
8508
+ const htmlLang = i18n ? i18n.defaultLocale : "en";
7816
8509
  ---
7817
8510
 
7818
8511
  <PageLayout
@@ -7827,6 +8520,8 @@ const nf = data.ui.notFound;
7827
8520
  themeMode={data.config.theme.mode}
7828
8521
  fontCssVars={data.fontCssVars}
7829
8522
  searchEnabled={data.config.search.enabled}
8523
+ locale={htmlLang}
8524
+ dir={dir}
7830
8525
  ui={data.ui}
7831
8526
  noindex={true}
7832
8527
  >
@@ -7838,7 +8533,7 @@ const nf = data.ui.notFound;
7838
8533
  <p class="text-muted-foreground">{nf.description}</p>
7839
8534
  <a
7840
8535
  class="mt-2 rounded-md bg-accent px-4 py-2 text-sm font-medium text-accent-foreground"
7841
- href="/">{nf.home}</a
8536
+ href={withBase("/")}>{nf.home}</a
7842
8537
  >
7843
8538
  </div>
7844
8539
  </PageLayout>
@@ -8948,7 +9643,7 @@ ${clause}
8948
9643
  <Component ${directiveFor(override)} {...Astro.props}><slot /></Component>
8949
9644
  `;
8950
9645
  };
8951
- var sanitize = (value) => value.replaceAll(/[^A-Za-z0-9]/gu, "_");
9646
+ var sanitize = (value) => value.replaceAll(/[^A-Za-z0-9]/gu, (char) => `_${(char.codePointAt(0) ?? 0).toString(16)}_`);
8952
9647
  var planComponentSlots = (componentsFile, analysis) => {
8953
9648
  const frameworks = new Set;
8954
9649
  if (!componentsFile) {
@@ -9011,13 +9706,13 @@ export const layoutOverrides = { ...(overrides.layout ?? {})${layoutEntries.leng
9011
9706
 
9012
9707
  // src/astro/examples.ts
9013
9708
  import { readFile as readFile10 } from "node:fs/promises";
9014
- import { join as join21, relative as relative6 } from "pathe";
9015
- import { glob as glob3 } from "tinyglobby";
9709
+ import { join as join21, relative as relative7 } from "pathe";
9710
+ import { glob as glob4 } from "tinyglobby";
9016
9711
 
9017
9712
  // src/astro/islands.ts
9018
9713
  import { readFile as readFile9 } from "node:fs/promises";
9019
9714
  import { basename, join as join20 } from "pathe";
9020
- import { glob as glob2 } from "tinyglobby";
9715
+ import { glob as glob3 } from "tinyglobby";
9021
9716
  var DEFAULT_CLIENT = "visible";
9022
9717
  var VALID_MODES = new Set([
9023
9718
  "idle",
@@ -9046,7 +9741,7 @@ var readClientMode = (source, file, warnings) => {
9046
9741
  };
9047
9742
  var discoverIslands = async (root) => {
9048
9743
  const dir = join20(root, "islands");
9049
- const matches = await glob2(["**/*.{jsx,svelte,tsx,vue}"], {
9744
+ const matches = await glob3(["**/*.{jsx,svelte,tsx,vue}"], {
9050
9745
  absolute: true,
9051
9746
  cwd: dir,
9052
9747
  onlyFiles: true
@@ -9109,7 +9804,7 @@ var splitGlobBase = (pattern) => {
9109
9804
  var discoverExamples = async (root, pattern = "examples") => {
9110
9805
  const { base, rest } = GLOB_MAGIC.test(pattern) ? splitGlobBase(pattern) : { base: pattern, rest: DEFAULT_EXAMPLE_GLOB };
9111
9806
  const dir = join21(root, base);
9112
- const matches = await glob3([rest], {
9807
+ const matches = await glob4([rest], {
9113
9808
  absolute: true,
9114
9809
  cwd: dir,
9115
9810
  onlyFiles: true
@@ -9125,7 +9820,7 @@ var discoverExamples = async (root, pattern = "examples") => {
9125
9820
  if (!(ext && framework)) {
9126
9821
  return;
9127
9822
  }
9128
- const path = relative6(dir, file).slice(0, -(ext.length + 1));
9823
+ const path = relative7(dir, file).slice(0, -(ext.length + 1));
9129
9824
  const existing = seen.get(path);
9130
9825
  if (existing) {
9131
9826
  warnings.push(`Two examples both resolve to "${path}" ("${existing}" and "${file}"); ignoring the second. Give them distinct paths.`);
@@ -9147,52 +9842,6 @@ var discoverExamples = async (root, pattern = "examples") => {
9147
9842
  return { examples, warnings };
9148
9843
  };
9149
9844
 
9150
- // src/astro/pages.ts
9151
- import { extname as extname5, relative as relative7 } from "pathe";
9152
- import { glob as glob4 } from "tinyglobby";
9153
- var discoverPages = async (pagesRoot) => {
9154
- const files = await glob4(["**/*.astro"], {
9155
- absolute: true,
9156
- cwd: pagesRoot,
9157
- onlyFiles: true
9158
- });
9159
- files.sort();
9160
- return files.map((file) => {
9161
- const rel = relative7(pagesRoot, file);
9162
- const withoutExt = rel.slice(0, rel.length - extname5(rel).length);
9163
- const parts = withoutExt.split("/");
9164
- if (parts.at(-1) === "index") {
9165
- parts.pop();
9166
- }
9167
- const pattern = parts.length === 0 ? "/" : `/${parts.join("/")}`;
9168
- return { entrypoint: file, pattern };
9169
- });
9170
- };
9171
- var routeIsTaken = (pages, contentPages, route) => pages.some((page) => page.pattern === route) || contentPages.some((page) => page.route === route);
9172
- var PRIVATE_SEGMENT = /^[._]/u;
9173
- var humanizeSegment = (segment) => segment.split(/[-_]/u).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
9174
- var customOgRoutes = (pages, siteTitle) => {
9175
- const seen = new Set;
9176
- const routes = [];
9177
- const collectRoute = (pattern) => {
9178
- const segments = pattern.split("/").filter(Boolean);
9179
- if (segments.some((part) => PRIVATE_SEGMENT.test(part) || part.includes("["))) {
9180
- return;
9181
- }
9182
- const slug = segments.length === 0 ? "index" : segments.join("/");
9183
- if (seen.has(slug)) {
9184
- return;
9185
- }
9186
- seen.add(slug);
9187
- const last = segments.at(-1);
9188
- routes.push({ slug, title: last ? humanizeSegment(last) : siteTitle });
9189
- };
9190
- for (const { pattern } of pages) {
9191
- collectRoute(pattern);
9192
- }
9193
- return routes;
9194
- };
9195
-
9196
9845
  // src/astro/generate.ts
9197
9846
  var BLUME_SRC = join22(packageRoot(), "src");
9198
9847
  var canResolveFrom = (fromDir, spec) => {
@@ -9281,6 +9930,10 @@ var ISLAND_FRAMEWORK_DEPS = {
9281
9930
  svelte: "@astrojs/svelte",
9282
9931
  vue: "@astrojs/vue"
9283
9932
  };
9933
+ var DEPLOYMENT_ADAPTER_DEPS = {
9934
+ cloudflare: "@astrojs/cloudflare",
9935
+ netlify: "@astrojs/netlify"
9936
+ };
9284
9937
  var islandFrameworkWarnings = (frameworks, root) => {
9285
9938
  const warnings = [];
9286
9939
  for (const framework of frameworks) {
@@ -9291,6 +9944,15 @@ var islandFrameworkWarnings = (frameworks, root) => {
9291
9944
  }
9292
9945
  return warnings;
9293
9946
  };
9947
+ var deploymentAdapterWarnings = (deployment, root) => {
9948
+ const dep = deployment.output === "server" && deployment.adapter ? DEPLOYMENT_ADAPTER_DEPS[deployment.adapter] : undefined;
9949
+ if (dep && !(canResolveFrom(root, dep) || canResolveFrom(packageRoot(), dep))) {
9950
+ return [
9951
+ `Deployment adapter "${deployment.adapter}" needs "${dep}", which isn't installed. Run \`npm install ${dep}\` (or your package manager's equivalent).`
9952
+ ];
9953
+ }
9954
+ return [];
9955
+ };
9294
9956
  var examplesCssFile = (root, config) => config.examples.css ? join22(root, config.examples.css) : null;
9295
9957
  var writeExamplesPreview = async (options) => {
9296
9958
  const { config, hasExamples, root, srcDir, write } = options;
@@ -9320,14 +9982,15 @@ var detectNeedsReact = async (root) => {
9320
9982
  });
9321
9983
  return matches.length > 0;
9322
9984
  };
9323
- var detectUsesMath = async (root) => {
9324
- const files = await glob5(["**/*.mdx"], {
9985
+ var containsMath = (content) => content.includes("$$") || content.includes("<Math");
9986
+ var detectUsesMath = async (root, staged = []) => {
9987
+ const files = await glob5(["**/*.{md,mdx}"], {
9325
9988
  cwd: root,
9326
9989
  ignore: ["**/node_modules/**", "**/.blume/**", "**/dist/**"],
9327
9990
  onlyFiles: true
9328
9991
  });
9329
9992
  const contents = await Promise.all(files.map((file) => readOptional(join22(root, file))));
9330
- return contents.some((content) => content.includes("$$"));
9993
+ return [...contents, ...staged].some(containsMath);
9331
9994
  };
9332
9995
  var writeIfChanged = async (path, content) => {
9333
9996
  let existing = null;
@@ -9518,6 +10181,7 @@ var buildRuntimeData = (project) => {
9518
10181
  appleIcon: resolveAppleIcon(project),
9519
10182
  ask: config.ai.ask?.enabled ? { suggestions: config.ai.ask.suggestions } : null,
9520
10183
  banner: resolveBanner(config),
10184
+ basePath: config.basePath,
9521
10185
  codeThemes: config.markdown.codeBlocks.theme,
9522
10186
  codeWrap: config.markdown.code.wrap,
9523
10187
  description: config.description,
@@ -9620,6 +10284,7 @@ var writeMcpFiles = async (project, plan, write) => {
9620
10284
  }
9621
10285
  const data = await buildMcpData(project);
9622
10286
  const discoveryInput = {
10287
+ base: data.base,
9623
10288
  name: data.name,
9624
10289
  route: plan.route,
9625
10290
  site: data.site,
@@ -9651,11 +10316,6 @@ var writeNotFoundPage = async (write, srcDir, pages, contentPages) => {
9651
10316
  }
9652
10317
  await write(join22(srcDir, "pages", "404.astro"), notFoundPageTemplate());
9653
10318
  };
9654
- var shouldGenerateChangelog = (project, userPages) => {
9655
- const hasChangelog = project.graph.pages.some((page) => page.contentType === "changelog" && !(page.meta.draft || page.meta.sidebar.hidden));
9656
- const hasChangelogSource = (project.config.content.sources ?? []).some((source) => source.type === "github-releases");
9657
- return (hasChangelog || hasChangelogSource) && !routeIsTaken(userPages, project.graph.pages, "/changelog");
9658
- };
9659
10319
  var buildComponentSlots = async (componentsFile) => {
9660
10320
  const analysis = componentsFile ? analyzeComponentOverrides(await readFile11(componentsFile, "utf-8"), componentsFile) : null;
9661
10321
  return {
@@ -9683,6 +10343,7 @@ var generateRuntime = async (project) => {
9683
10343
  const askEnabled = config.ai.ask?.enabled ?? false;
9684
10344
  const exportPdf = config.export.pdf;
9685
10345
  const exportEpub = config.export.epub;
10346
+ const staged = collectStaged(project);
9686
10347
  const [
9687
10348
  pages,
9688
10349
  detectedReact,
@@ -9695,7 +10356,7 @@ var generateRuntime = async (project) => {
9695
10356
  ] = await Promise.all([
9696
10357
  context.pagesRoot ? discoverPages(context.pagesRoot) : Promise.resolve([]),
9697
10358
  detectNeedsReact(context.root),
9698
- detectUsesMath(context.root),
10359
+ detectUsesMath(context.root, staged.values()),
9699
10360
  readOptional(context.themeFile),
9700
10361
  readOptional(examplesCssFile(context.root, config)),
9701
10362
  discoverIslands(context.root),
@@ -9719,7 +10380,6 @@ var generateRuntime = async (project) => {
9719
10380
  const ogRoutes = customOgRoutes(pages, config.title);
9720
10381
  const mcp = planMcp(project, srcDir, pages);
9721
10382
  pages.push(...mcp.discoveryPages);
9722
- const staged = collectStaged(project);
9723
10383
  const hasStaged = staged.size > 0;
9724
10384
  const hasFilesystemSource = project.sources.some((source) => !source.staged);
9725
10385
  const [structural] = await Promise.all([
@@ -9785,7 +10445,7 @@ var generateRuntime = async (project) => {
9785
10445
  if (config.seo.og.enabled) {
9786
10446
  await write(join22(srcDir, "pages", "og", "[...slug].png.ts"), ogEndpointTemplate(ogRoutes));
9787
10447
  }
9788
- if (shouldGenerateChangelog(project, pages)) {
10448
+ if (hasGeneratedChangelog(project, pages)) {
9789
10449
  await write(join22(srcDir, "pages", "changelog.astro"), changelogIndexTemplate({
9790
10450
  askEnabled,
9791
10451
  exportEpub,
@@ -9818,8 +10478,8 @@ var generateRuntime = async (project) => {
9818
10478
  await Promise.all([
9819
10479
  write(join22(srcDir, "generated", "raw-markdown.json"), `${JSON.stringify(rawMarkdown)}
9820
10480
  `),
9821
- write(join22(srcDir, "pages", "[...slug].md.ts"), rawMarkdownEndpointTemplate()),
9822
- write(join22(srcDir, "pages", "[...slug].mdx.ts"), rawMarkdownEndpointTemplate())
10481
+ write(join22(srcDir, "pages", "[...slug].md.ts"), rawMarkdownEndpointTemplate("md")),
10482
+ write(join22(srcDir, "pages", "[...slug].mdx.ts"), rawMarkdownEndpointTemplate("mdx"))
9823
10483
  ]);
9824
10484
  const feeds = buildRssFeeds(project);
9825
10485
  if (feeds.length > 0) {
@@ -9844,7 +10504,7 @@ var generateRuntime = async (project) => {
9844
10504
  ...pages.map((page) => page.pattern),
9845
10505
  ...referenceTabs(config).map((tab) => tab.path)
9846
10506
  ]);
9847
- if (shouldGenerateChangelog(project, pages)) {
10507
+ if (hasGeneratedChangelog(project, pages)) {
9848
10508
  navTargetRoutes.add("/changelog");
9849
10509
  }
9850
10510
  warnings.push(...validateNavTargets(project.graph.navigation, navTargetRoutes).map((diagnostic) => diagnostic.suggestion ? `${diagnostic.message} ${diagnostic.suggestion}` : diagnostic.message));
@@ -9858,7 +10518,7 @@ var generateRuntime = async (project) => {
9858
10518
  warnings.push(`Search provider "${config.search.provider}" needs "${dep}", which isn't installed. Run \`npm install ${dep}\` (or your package manager's equivalent).`);
9859
10519
  }
9860
10520
  }
9861
- warnings.push(...islandFrameworkWarnings(frameworks, context.root));
10521
+ warnings.push(...deploymentAdapterWarnings(config.deployment, context.root), ...islandFrameworkWarnings(frameworks, context.root));
9862
10522
  if (hasScalarReferences(config)) {
9863
10523
  const references = await buildReferenceFiles({
9864
10524
  config,
@@ -10005,6 +10665,7 @@ var segmentKey = (raw) => {
10005
10665
  const group = raw.match(GROUP_FOLDER2)?.groups?.label;
10006
10666
  return (group ?? raw).replace(NUMERIC_PREFIX2, "");
10007
10667
  };
10668
+ var isIndexStem = (stem) => stem.replace(NUMERIC_PREFIX2, "") === "index";
10008
10669
  var createGroup = (key, path, label, order) => ({
10009
10670
  children: [],
10010
10671
  index: new Map,
@@ -10029,7 +10690,7 @@ var pageOrder = (page, filename) => {
10029
10690
  if (page.meta.sidebar.order !== undefined) {
10030
10691
  return page.meta.sidebar.order;
10031
10692
  }
10032
- if (filename.replace(extname6(filename), "") === "index") {
10693
+ if (isIndexStem(filename.replace(extname6(filename), ""))) {
10033
10694
  return Number.NEGATIVE_INFINITY;
10034
10695
  }
10035
10696
  if (page.contentType === "changelog") {
@@ -10138,7 +10799,7 @@ var buildFileSystemSidebar = (pages, folderMeta, sharedMeta, metaPrefix, display
10138
10799
  const stem = filename.replace(extname6(filename), "");
10139
10800
  const dirs = parts.slice(0, -1);
10140
10801
  const routeSegments = page.route.split("/").filter(Boolean);
10141
- const folderParts = stem === "index" ? routeSegments : routeSegments.slice(0, -1);
10802
+ const folderParts = isIndexStem(stem) ? routeSegments : routeSegments.slice(0, -1);
10142
10803
  const routeDirCount = dirs.filter((dir) => !GROUP_FOLDER2.test(dir)).length;
10143
10804
  const offset = Math.max(0, folderParts.length - routeDirCount);
10144
10805
  let parent = root;
@@ -10266,7 +10927,7 @@ var buildNavigation = (pages, options) => {
10266
10927
  ...selector,
10267
10928
  items: selector.items.map(rebasePath)
10268
10929
  })) : options.selectors ?? [];
10269
- const tabs = basePath ? (options.tabs ?? []).map((tab) => ({
10930
+ const tabs2 = basePath ? (options.tabs ?? []).map((tab) => ({
10270
10931
  ...tab,
10271
10932
  items: tab.items?.map(rebasePath),
10272
10933
  path: withBasePath(basePath, tab.path)
@@ -10288,14 +10949,15 @@ var buildNavigation = (pages, options) => {
10288
10949
  featured,
10289
10950
  selectors,
10290
10951
  sidebar: buildConfigSidebar(options.sidebar, byRoute, display, basePath),
10291
- tabs
10952
+ tabs: tabs2
10292
10953
  };
10293
10954
  }
10955
+ const rootTabPath = withBasePath(basePath, options.localizedRoot ?? "/");
10294
10956
  return {
10295
10957
  featured,
10296
10958
  selectors,
10297
- sidebar: buildFileSystemSidebar(pages, options.folderMeta, sharedFolderMeta, metaPrefix, display, new Set(tabs.flatMap((tab) => tab.path === "/" ? [] : [tab.path]))),
10298
- tabs
10959
+ sidebar: buildFileSystemSidebar(pages, options.folderMeta, sharedFolderMeta, metaPrefix, display, new Set(tabs2.flatMap((tab) => tab.path === rootTabPath ? [] : [tab.path]))),
10960
+ tabs: tabs2
10299
10961
  };
10300
10962
  };
10301
10963
 
@@ -10337,9 +10999,14 @@ var localePagesFor = (code, real, fallback, fallbackByKey, i18n, basePath) => {
10337
10999
  return [...real, ...filled];
10338
11000
  };
10339
11001
  var buildLocaleNavigation = (code, pages, fallback, fallbackByKey, options, i18n) => {
10340
- const tabs = options.navigation.tabs?.map((tab) => ({
11002
+ const localizePath = (path) => path.startsWith("/") ? localizeRoute(path, code, i18n) : path;
11003
+ const tabs2 = options.navigation.tabs?.map((tab) => ({
10341
11004
  ...tab,
10342
- path: tab.path.startsWith("/") ? localizeRoute(tab.path, code, i18n) : tab.path
11005
+ items: tab.items?.map((item) => ({
11006
+ ...item,
11007
+ path: localizePath(item.path)
11008
+ })),
11009
+ path: localizePath(tab.path)
10343
11010
  }));
10344
11011
  const real = pages.filter((page) => page.locale === code);
10345
11012
  const localePages = localePagesFor(code, real, fallback, fallbackByKey, i18n, options.basePath ?? "");
@@ -10348,12 +11015,13 @@ var buildLocaleNavigation = (code, pages, fallback, fallbackByKey, options, i18n
10348
11015
  display: options.navigation.sidebar.display,
10349
11016
  featured: options.navigation.featured,
10350
11017
  folderMeta: options.folderMeta,
11018
+ localizedRoot: localizeRoute("/", code, i18n),
10351
11019
  metaPrefix: i18n.parser === "dir" && code !== i18n.defaultLocale ? code : "",
10352
11020
  refByLogical: true,
10353
11021
  selectors: options.navigation.selectors,
10354
11022
  sharedFolderMeta: options.sharedFolderMeta,
10355
11023
  sidebar: options.navigation.sidebar.items,
10356
- tabs
11024
+ tabs: tabs2
10357
11025
  });
10358
11026
  };
10359
11027
  var buildI18nNavigation = (pages, options, i18n) => {
@@ -10429,7 +11097,10 @@ var parseGitLog = (output) => {
10429
11097
  }
10430
11098
  return times;
10431
11099
  };
10432
- var gitLastModifiedTimes = (root, contentRoot, sourcePaths) => {
11100
+ var gitLastModifiedTimes = (root, contentRoots, sourcePaths) => {
11101
+ if (sourcePaths.length === 0) {
11102
+ return new Map;
11103
+ }
10433
11104
  try {
10434
11105
  const gitRoot = execFileSync("git", ["-C", root, "rev-parse", "--show-toplevel"], { encoding: "utf-8" }).trim();
10435
11106
  const output = execFileSync("git", [
@@ -10441,7 +11112,7 @@ var gitLastModifiedTimes = (root, contentRoot, sourcePaths) => {
10441
11112
  "--format=%x00%cI",
10442
11113
  "--name-only",
10443
11114
  "--",
10444
- contentRoot
11115
+ ...contentRoots
10445
11116
  ], { encoding: "utf-8", maxBuffer: 256 * 1024 * 1024 });
10446
11117
  const byRepoPath = parseGitLog(output);
10447
11118
  const result = new Map;
@@ -10567,7 +11238,7 @@ var entryIdDiagnostics = (pages, collectionBase) => {
10567
11238
  file: page.sourcePath,
10568
11239
  message: `Content source "${page.source.name}" is rooted outside the docs collection base, so ${page.route} resolves entry id "${entryId}" but the collection would generate "${expected}" — the page would 404 at runtime.`,
10569
11240
  severity: "error",
10570
- suggestion: "Give each filesystem source a root under content.root, or use a single filesystem source so the collection can root at it."
11241
+ suggestion: "Use a single filesystem source (the docs collection roots at it), or root every filesystem source at content.root and partition them with include globs — a root at a subdirectory of content.root still mismatches."
10571
11242
  });
10572
11243
  }
10573
11244
  }
@@ -10579,7 +11250,7 @@ var scanProject = async (root, options = {}) => {
10579
11250
  const configResult = await loadConfig(root, {
10580
11251
  devServerUrl: options.devServerUrl
10581
11252
  });
10582
- const config = applyConfigOverrides(configResult.config, options.overrides);
11253
+ const config = applyDeploymentEnv(applyConfigOverrides(configResult.config, options.overrides));
10583
11254
  const context = resolveProjectContext(root, config, {
10584
11255
  runtimeDir: options.runtimeDir
10585
11256
  });
@@ -10620,7 +11291,8 @@ var scanProject = async (root, options = {}) => {
10620
11291
  const lastModified = resolveLastModifiedConfig(config.lastModified);
10621
11292
  if (lastModified.enabled && lastModified.source === "git") {
10622
11293
  const fsPaths = pages.map((page) => page.sourcePath).filter((path) => path !== undefined);
10623
- const gitTimes = gitLastModifiedTimes(context.root, context.contentRoot, fsPaths);
11294
+ const contentRoots = sources.flatMap((source) => source.staged || !source.contentRoot ? [] : [source.contentRoot]);
11295
+ const gitTimes = gitLastModifiedTimes(context.root, contentRoots, fsPaths);
10624
11296
  for (const page of pages) {
10625
11297
  if (!page.lastModified && page.sourcePath) {
10626
11298
  page.lastModified = gitTimes.get(page.sourcePath);
@@ -10659,11 +11331,18 @@ import { dirname as dirname10, join as join23, resolve as resolve7 } from "pathe
10659
11331
  var ENV_LINE = /^\s*(?:export\s+)?(?<key>[A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?<value>.*?)\s*$/u;
10660
11332
  var DOUBLE_QUOTED2 = /^"(?<body>[\s\S]*)"$/u;
10661
11333
  var SINGLE_QUOTED = /^'(?<body>[\s\S]*)'$/u;
11334
+ var ESCAPE = /\\(?<char>[\\nt"])/gu;
11335
+ var UNESCAPED = {
11336
+ '"': '"',
11337
+ "\\": "\\",
11338
+ n: `
11339
+ `,
11340
+ t: "\t"
11341
+ };
10662
11342
  var unquote = (raw) => {
10663
11343
  const double = raw.match(DOUBLE_QUOTED2)?.groups?.body;
10664
11344
  if (double !== undefined) {
10665
- return double.replaceAll("\\n", `
10666
- `).replaceAll("\\t", "\t").replaceAll("\\\"", '"').replaceAll("\\\\", "\\");
11345
+ return double.replaceAll(ESCAPE, (match, char) => UNESCAPED[char] ?? match);
10667
11346
  }
10668
11347
  const single = raw.match(SINGLE_QUOTED)?.groups?.body;
10669
11348
  if (single !== undefined) {
@@ -10905,6 +11584,47 @@ var enforceBudget = async (distDir, args) => {
10905
11584
  }
10906
11585
  return passed ? "pass" : "fail";
10907
11586
  };
11587
+ var runClientAssetChecks = async (staticDir, args) => {
11588
+ if (args.analyze) {
11589
+ await reportBundleSizes(staticDir);
11590
+ }
11591
+ if (await enforceBudget(staticDir, args) === "fail") {
11592
+ process.exit(1);
11593
+ }
11594
+ };
11595
+ var isolatedStaticDir = (config, context) => {
11596
+ const { adapter, output } = config.deployment;
11597
+ if (output === "server" && adapter === "vercel") {
11598
+ return join24(context.outDir, ".vercel", "output", "static");
11599
+ }
11600
+ const dist = context.distDir ?? join24(context.outDir, "dist");
11601
+ if (output === "server" && adapter === "node") {
11602
+ return join24(dist, "client");
11603
+ }
11604
+ return dist;
11605
+ };
11606
+ var publishLlmsFiles = async (project, distDir) => {
11607
+ const indexPath = join24(distDir, "llms.txt");
11608
+ const fullPath = join24(distDir, "llms-full.txt");
11609
+ const writeIndex = !existsSync15(indexPath);
11610
+ const writeFull = !existsSync15(fullPath);
11611
+ if (!(writeIndex || writeFull)) {
11612
+ return;
11613
+ }
11614
+ const { index, full } = await buildLlmsFiles(project);
11615
+ const writes = [];
11616
+ if (writeIndex) {
11617
+ writes.push(writeFile7(indexPath, index, "utf-8"));
11618
+ }
11619
+ if (writeFull) {
11620
+ writes.push(writeFile7(fullPath, full, "utf-8"));
11621
+ }
11622
+ await Promise.all(writes);
11623
+ logger.success(`Generated ${[
11624
+ writeIndex ? "llms.txt" : null,
11625
+ writeFull ? "llms-full.txt" : null
11626
+ ].filter(Boolean).join(" and ")}`);
11627
+ };
10908
11628
  var publishBuildArtifacts = async (project, distDir, args) => {
10909
11629
  if (project.config.search.provider === "pagefind") {
10910
11630
  logger.start("Building search index");
@@ -10916,13 +11636,8 @@ var publishBuildArtifacts = async (project, distDir, args) => {
10916
11636
  success: (message) => logger.success(message),
10917
11637
  warn: (message) => logger.warn(message)
10918
11638
  });
10919
- if (project.config.ai.llmsTxt) {
10920
- const { index, full } = await buildLlmsFiles(project);
10921
- await Promise.all([
10922
- writeFile7(join24(distDir, "llms.txt"), index, "utf-8"),
10923
- writeFile7(join24(distDir, "llms-full.txt"), full, "utf-8")
10924
- ]);
10925
- logger.success("Generated llms.txt and llms-full.txt");
11639
+ if (project.config.ai.llmsTxt.enabled) {
11640
+ await publishLlmsFiles(project, distDir);
10926
11641
  }
10927
11642
  const sitemap = buildSitemap(project);
10928
11643
  if (sitemap && !existsSync15(join24(distDir, "sitemap.xml"))) {
@@ -10943,25 +11658,21 @@ var publishBuildArtifacts = async (project, distDir, args) => {
10943
11658
  await emitRedirectFiles(project.config, distDir);
10944
11659
  const { config } = project;
10945
11660
  const features = serverFeatures(config);
11661
+ const sitemapNote = config.seo.sitemap ? "no (set deployment.site)" : "no (seo.sitemap is false)";
10946
11662
  logger.box([
10947
11663
  `Output ${config.deployment.output}`,
10948
11664
  `Adapter ${config.deployment.adapter ?? "none"}`,
10949
11665
  `Site ${config.deployment.site ?? "not set"}`,
10950
11666
  `Search ${config.search.provider}`,
10951
11667
  `Redirects ${config.redirects.length}`,
10952
- `Sitemap ${sitemap ? "yes" : "no (set deployment.site)"}`,
11668
+ `Sitemap ${sitemap ? "yes" : sitemapNote}`,
10953
11669
  `Robots ${robots ? "yes" : "no"}`,
10954
11670
  `Agent JSON ${agentReadability ? "yes" : "no"}`,
10955
- `LLM files ${config.ai.llmsTxt ? "yes" : "no"}`,
11671
+ `LLM files ${config.ai.llmsTxt.enabled ? "yes" : "no"}`,
10956
11672
  `Server features ${features.length > 0 ? features.join(", ") : "none"}`
10957
11673
  ].join(`
10958
11674
  `));
10959
- if (args.analyze) {
10960
- await reportBundleSizes(distDir);
10961
- }
10962
- if (await enforceBudget(distDir, args) === "fail") {
10963
- process.exit(1);
10964
- }
11675
+ await runClientAssetChecks(distDir, args);
10965
11676
  logger.success(`Built to ${distDir}`);
10966
11677
  };
10967
11678
  var buildCommand = defineCommand2({
@@ -11007,7 +11718,7 @@ var buildCommand = defineCommand2({
11007
11718
  async run({ args }) {
11008
11719
  const root = process.cwd();
11009
11720
  const runtimeDir = args.isolated ? ".blume-verify" : process.env.BLUME_RUNTIME_DIR;
11010
- refuseIfDevRunning(root, "building", runtimeDir);
11721
+ refuseIfDevRunning(root, "building", { isolatedHint: true, runtimeDir });
11011
11722
  if (args.isolated) {
11012
11723
  await ensureGitignore(root, [".blume-verify/"]);
11013
11724
  }
@@ -11039,6 +11750,7 @@ var buildCommand = defineCommand2({
11039
11750
  });
11040
11751
  const distDir = project.context.distDir ?? join24(root, "dist");
11041
11752
  if (runtimeDir) {
11753
+ await runClientAssetChecks(isolatedStaticDir(project.config, project.context), args);
11042
11754
  logger.success(`Isolated build OK — output at ${distDir} (not published).`);
11043
11755
  return;
11044
11756
  }
@@ -11079,7 +11791,7 @@ var checkCommand = defineCommand3({
11079
11791
  async run({ args }) {
11080
11792
  const root = process.cwd();
11081
11793
  const runtimeDir = args.isolated ? ".blume-verify" : process.env.BLUME_RUNTIME_DIR;
11082
- refuseIfDevRunning(root, "checking", runtimeDir);
11794
+ refuseIfDevRunning(root, "checking", { isolatedHint: true, runtimeDir });
11083
11795
  if (args.isolated) {
11084
11796
  await ensureGitignore(root, [".blume-verify/"]);
11085
11797
  }
@@ -11191,6 +11903,7 @@ var coalescedRunner = (task) => {
11191
11903
  };
11192
11904
 
11193
11905
  // src/cli/commands/dev.ts
11906
+ var normalizeHost = (host) => host === "" ? true : host ?? false;
11194
11907
  var routeSignature = (routes) => routes.map((route) => `${route.path} ${route.entryId}`).toSorted().join(`
11195
11908
  `);
11196
11909
  var devCommand = defineCommand4({
@@ -11246,7 +11959,7 @@ var devCommand = defineCommand4({
11246
11959
  const createServer = (listenPort, open) => dev({
11247
11960
  logLevel: args.debug ? "debug" : "info",
11248
11961
  root: project.context.outDir,
11249
- server: { host: args.host ?? false, open, port: listenPort }
11962
+ server: { host: normalizeHost(args.host), open, port: listenPort }
11250
11963
  });
11251
11964
  let server = await createServer(explicitPort, args.open ?? false);
11252
11965
  const boundPort = server.address.port;
@@ -11266,7 +11979,6 @@ var devCommand = defineCommand4({
11266
11979
  });
11267
11980
  const nextSignature = routeSignature(next.manifest.routes);
11268
11981
  const structural = nextSignature !== lastSignature;
11269
- lastSignature = nextSignature;
11270
11982
  if (structural) {
11271
11983
  await server.stop();
11272
11984
  await generateRuntime(next);
@@ -11274,6 +11986,8 @@ var devCommand = defineCommand4({
11274
11986
  } else {
11275
11987
  await generateRuntime(next);
11276
11988
  }
11989
+ lastSignature = nextSignature;
11990
+ reportDiagnostics(next.diagnostics, root);
11277
11991
  showBlumeErrorOverlay(next.diagnostics);
11278
11992
  } catch (error) {
11279
11993
  logger.error(`Regeneration failed: ${error.message}`);
@@ -11415,15 +12129,27 @@ var doctorCommand = defineCommand5({
11415
12129
  });
11416
12130
 
11417
12131
  // src/cli/commands/eject.ts
11418
- import { readFile as readFile13, writeFile as writeFile9 } from "node:fs/promises";
11419
12132
  import { defineCommand as defineCommand6 } from "citty";
11420
- import { join as join28, relative as relative13 } from "pathe";
12133
+ import { relative as relative14 } from "pathe";
11421
12134
 
11422
12135
  // src/registry/eject.ts
11423
12136
  import { existsSync as existsSync17 } from "node:fs";
11424
12137
  import { cp as cp2, mkdir as mkdir7, readFile as readFile12, rm as rm3, writeFile as writeFile8 } from "node:fs/promises";
11425
12138
  import { join as join27, relative as relative12 } from "pathe";
11426
12139
  var toPosix = (path) => path.split("\\").join("/");
12140
+ var LOCAL_BLUME_SOURCE = "../../node_modules/blume/src/**/*.{astro,ts,tsx}";
12141
+ var blumeSourceGlob = (root, genDir, resolveBlumeRoot = packageRoot) => {
12142
+ if (existsSync17(join27(root, "node_modules", "blume"))) {
12143
+ return LOCAL_BLUME_SOURCE;
12144
+ }
12145
+ try {
12146
+ const src = join27(resolveBlumeRoot(), "src");
12147
+ return `${toPosix(relative12(genDir, src))}/**/*.{astro,ts,tsx}`;
12148
+ } catch {
12149
+ console.warn('blume: could not locate the installed blume package; src/generated/app.css keeps its default `@source "../../node_modules/blume/..."` glob. If blume is hoisted elsewhere, point that glob at its install location or Blume\'s utility classes will be missing.');
12150
+ return LOCAL_BLUME_SOURCE;
12151
+ }
12152
+ };
11427
12153
  var ejectOpenApiData = (project) => {
11428
12154
  const source = project.sources.find(isOpenApiSource);
11429
12155
  return source ? source.openApiData() : {};
@@ -11449,6 +12175,63 @@ var askFiles = async (project, srcDir, genDir) => {
11449
12175
  }
11450
12176
  return files;
11451
12177
  };
12178
+ var hostsMcp = (project, userPages) => project.config.mcp.enabled && !routeIsTaken(userPages, project.graph.pages, project.config.mcp.route);
12179
+ var mcpDiscoveryPages = (project, userPages) => hostsMcp(project, userPages) ? [
12180
+ {
12181
+ entrypoint: "src/blume-mcp/discovery.ts",
12182
+ pattern: "/.well-known/mcp.json"
12183
+ },
12184
+ {
12185
+ entrypoint: "src/blume-mcp/server-card.ts",
12186
+ pattern: "/.well-known/mcp/server-card.json"
12187
+ }
12188
+ ] : [];
12189
+ var mcpFiles = async (project, userPages, srcDir, genDir) => {
12190
+ if (!hostsMcp(project, userPages)) {
12191
+ return [];
12192
+ }
12193
+ const { route } = project.config.mcp;
12194
+ const data = await buildMcpData(project);
12195
+ const discoveryInput = {
12196
+ base: data.base,
12197
+ name: data.name,
12198
+ route,
12199
+ site: data.site,
12200
+ version: data.version
12201
+ };
12202
+ return [
12203
+ {
12204
+ content: `${JSON.stringify(data)}
12205
+ `,
12206
+ path: join27(genDir, "mcp-data.json")
12207
+ },
12208
+ {
12209
+ content: mcpEndpointTemplate(route),
12210
+ path: join27(srcDir, "pages", mcpPageFile(route))
12211
+ },
12212
+ {
12213
+ content: staticJsonEndpointTemplate(buildMcpDiscovery(discoveryInput)),
12214
+ path: join27(srcDir, "blume-mcp", "discovery.ts")
12215
+ },
12216
+ {
12217
+ content: staticJsonEndpointTemplate(buildMcpServerCard(discoveryInput)),
12218
+ path: join27(srcDir, "blume-mcp", "server-card.ts")
12219
+ }
12220
+ ];
12221
+ };
12222
+ var changelogFiles = (project, userPages, srcDir, options) => {
12223
+ const hasChangelog = project.graph.pages.some((page) => page.contentType === "changelog" && !(page.meta.draft || page.meta.sidebar.hidden));
12224
+ const hasChangelogSource = (project.config.content.sources ?? []).some((source) => source.type === "github-releases");
12225
+ if (!(hasChangelog || hasChangelogSource) || routeIsTaken(userPages, project.graph.pages, "/changelog")) {
12226
+ return [];
12227
+ }
12228
+ return [
12229
+ {
12230
+ content: changelogIndexTemplate(options),
12231
+ path: join27(srcDir, "pages", "changelog.astro")
12232
+ }
12233
+ ];
12234
+ };
11452
12235
  var readExamplesCss = (root, css) => css && existsSync17(join27(root, css)) ? readFile12(join27(root, css), "utf-8") : Promise.resolve("");
11453
12236
  var examplesPreviewFiles = (srcDir, basePath, hasExamples) => hasExamples ? [
11454
12237
  {
@@ -11497,10 +12280,13 @@ var eject = async (root) => {
11497
12280
  root: "."
11498
12281
  };
11499
12282
  const componentsImport = context.componentsFile ? `../../${toPosix(relative12(root, context.componentsFile))}` : null;
11500
- const relPages = pages.map((page) => ({
11501
- entrypoint: toPosix(relative12(root, page.entrypoint)),
11502
- pattern: page.pattern
11503
- }));
12283
+ const relPages = [
12284
+ ...pages.map((page) => ({
12285
+ entrypoint: toPosix(relative12(root, page.entrypoint)),
12286
+ pattern: page.pattern
12287
+ })),
12288
+ ...mcpDiscoveryPages(project, pages)
12289
+ ];
11504
12290
  const staged = collectStaged(project);
11505
12291
  const hasStaged = staged.size > 0;
11506
12292
  const stagedDir = "blume-staged";
@@ -11572,7 +12358,7 @@ var eject = async (root) => {
11572
12358
  content: tailwindEntryTemplate({
11573
12359
  configTokens: buildThemeCss(config.theme),
11574
12360
  sources: [
11575
- "../../node_modules/blume/src/**/*.{astro,ts,tsx}",
12361
+ blumeSourceGlob(root, genDir),
11576
12362
  "../../**/*.{astro,mdx,ts,tsx}"
11577
12363
  ],
11578
12364
  twoslashCss: twoslashCss(),
@@ -11592,11 +12378,11 @@ var eject = async (root) => {
11592
12378
  path: join27(genDir, "raw-markdown.json")
11593
12379
  },
11594
12380
  {
11595
- content: rawMarkdownEndpointTemplate(),
12381
+ content: rawMarkdownEndpointTemplate("md"),
11596
12382
  path: join27(srcDir, "pages", "[...slug].md.ts")
11597
12383
  },
11598
12384
  {
11599
- content: rawMarkdownEndpointTemplate(),
12385
+ content: rawMarkdownEndpointTemplate("mdx"),
11600
12386
  path: join27(srcDir, "pages", "[...slug].mdx.ts")
11601
12387
  }
11602
12388
  ];
@@ -11609,6 +12395,13 @@ var eject = async (root) => {
11609
12395
  path: join27(srcDir, "pages", "og", "[...slug].png.ts")
11610
12396
  });
11611
12397
  }
12398
+ files.push(...await mcpFiles(project, pages, srcDir, genDir), ...changelogFiles(project, pages, srcDir, {
12399
+ askEnabled,
12400
+ exportEpub,
12401
+ exportPdf,
12402
+ needsReact,
12403
+ staged: hasStaged
12404
+ }));
11612
12405
  if (!routeIsTaken(pages, project.graph.pages, "/404")) {
11613
12406
  files.push({
11614
12407
  content: notFoundPageTemplate(),
@@ -11648,12 +12441,14 @@ var eject = async (root) => {
11648
12441
  path: join27(srcDir, "pages", "[section]", "rss.xml.ts")
11649
12442
  });
11650
12443
  }
12444
+ const warnings = [];
11651
12445
  if (hasScalarReferences(config)) {
11652
12446
  const references = await buildReferenceFiles({
11653
12447
  config,
11654
12448
  contentRoutes: new Set(project.graph.pages.map((page) => page.route)),
11655
12449
  root
11656
12450
  });
12451
+ warnings.push(...references.warnings);
11657
12452
  for (const file of references.files) {
11658
12453
  files.push({
11659
12454
  content: file.content,
@@ -11683,10 +12478,37 @@ var eject = async (root) => {
11683
12478
  });
11684
12479
  }
11685
12480
  await rm3(context.outDir, { force: true, recursive: true });
11686
- return written.map((file) => file.path);
12481
+ return { files: written.map((file) => file.path), warnings };
11687
12482
  };
11688
12483
 
11689
- // src/cli/commands/eject.ts
12484
+ // src/cli/eject-scripts.ts
12485
+ import { readFile as readFile13, writeFile as writeFile9 } from "node:fs/promises";
12486
+ import { join as join28 } from "pathe";
12487
+ var droppedArtifactNotices = (config) => {
12488
+ const notices = [];
12489
+ if (config.search.provider === "pagefind") {
12490
+ notices.push('the Pagefind search index — the search UI loads it from the built site, so search will break in production. Add a post-build step: `"build": "astro build && pagefind --site dist"` (with `pagefind` installed as a devDependency).');
12491
+ }
12492
+ if (searchProviderMeta(config.search.provider).syncs) {
12493
+ notices.push(`the hosted ${config.search.provider} index sync — new and updated pages stop being pushed; re-upload your search records after each build with the provider's API or CLI.`);
12494
+ }
12495
+ if (config.ai.llmsTxt.enabled) {
12496
+ notices.push("llms.txt and llms-full.txt");
12497
+ }
12498
+ if (config.deployment.site && config.seo.sitemap) {
12499
+ notices.push("sitemap.xml — recreate it with the @astrojs/sitemap integration.");
12500
+ }
12501
+ if (config.seo.robots) {
12502
+ notices.push("robots.txt — recreate it as a public/robots.txt file.");
12503
+ }
12504
+ if (config.seo.agentReadability) {
12505
+ notices.push("agent-readability.json");
12506
+ }
12507
+ if (config.redirects.length > 0 && config.deployment.output === "static") {
12508
+ notices.push("the platform redirect files (_redirects, vercel.json) — your redirects still work as Astro-generated meta-refresh pages.");
12509
+ }
12510
+ return notices;
12511
+ };
11690
12512
  var updatePackageScripts = async (root) => {
11691
12513
  const pkgPath = join28(root, "package.json");
11692
12514
  let pkg;
@@ -11705,47 +12527,21 @@ var updatePackageScripts = async (root) => {
11705
12527
  await writeFile9(pkgPath, `${JSON.stringify(pkg, null, 2)}
11706
12528
  `, "utf-8");
11707
12529
  };
11708
- var ejectCommand = defineCommand6({
11709
- args: {
11710
- yes: { description: "Skip the confirmation prompt.", type: "boolean" }
11711
- },
11712
- meta: {
11713
- description: "Promote the generated runtime into an owned Astro project.",
11714
- name: "eject"
11715
- },
11716
- async run({ args }) {
11717
- const root = process.cwd();
11718
- refuseIfDevRunning(root, "ejecting");
11719
- if (!args.yes) {
11720
- logger.warn("Eject is one-way: it writes astro.config.mjs, src/, and (if absent) tsconfig.json, rewrites your package.json scripts, and removes .blume. An existing tsconfig.json is left untouched.");
11721
- logger.info("Re-run with --yes to proceed.");
11722
- return;
11723
- }
11724
- const files = await eject(root);
11725
- await updatePackageScripts(root);
11726
- logger.success(`Ejected ${files.length} file(s):`);
11727
- for (const file of files) {
11728
- process.stdout.write(` ${relative13(root, file)}
11729
- `);
11730
- }
11731
- logger.box(`Your project is now a standalone Astro app.
11732
-
11733
- bun run dev
11734
- bun run build
11735
12530
 
11736
- The blume package remains importable.`);
11737
- }
11738
- });
11739
-
11740
- // src/cli/commands/init.ts
12531
+ // src/cli/init/scaffold.ts
11741
12532
  import { existsSync as existsSync18 } from "node:fs";
11742
12533
  import { mkdir as mkdir8, writeFile as writeFile10 } from "node:fs/promises";
11743
- import { defineCommand as defineCommand7 } from "citty";
11744
- import { basename as basename4, dirname as dirname11, isAbsolute as isAbsolute9, join as join29, relative as relative14 } from "pathe";
12534
+ import { basename as basename4, dirname as dirname11, isAbsolute as isAbsolute9, join as join29, relative as relative13 } from "pathe";
11745
12535
 
11746
12536
  // src/core/package-json.ts
11747
12537
  var toPackageName = (raw) => raw.toLowerCase().replaceAll(/[^a-z0-9._-]+/gu, "-").replaceAll(/^[-_.]+|[-_.]+$/gu, "") || "docs";
11748
- var blumePackageJson = (name) => `{
12538
+ var blumePackageJson = (name, extraDeps = {}) => {
12539
+ const dependencies = Object.entries({
12540
+ blume: `^${getBlumeVersion()}`,
12541
+ ...extraDeps
12542
+ }).toSorted(([a], [b]) => a < b ? -1 : 1).map(([dep, range]) => ` ${JSON.stringify(dep)}: ${JSON.stringify(range)}`).join(`,
12543
+ `);
12544
+ return `{
11749
12545
  "name": ${JSON.stringify(name)},
11750
12546
  "private": true,
11751
12547
  "type": "module",
@@ -11755,21 +12551,22 @@ var blumePackageJson = (name) => `{
11755
12551
  "doctor": "blume doctor"
11756
12552
  },
11757
12553
  "dependencies": {
11758
- "blume": "^${getBlumeVersion()}"
12554
+ ${dependencies}
11759
12555
  }
11760
12556
  }
11761
12557
  `;
12558
+ };
11762
12559
 
11763
- // src/cli/commands/init.ts
12560
+ // src/cli/init/scaffold.ts
11764
12561
  var TEMPLATES = ["docs", "api", "sdk", "changelog"];
11765
12562
  var PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
11766
- var configFor = (extra) => `import { defineConfig } from "blume";
11767
-
11768
- export default defineConfig({
11769
- title: "My Docs",
11770
- description: "Documentation powered by Blume.",${extra}
11771
- });
11772
- `;
12563
+ var SOURCE_KINDS = [
12564
+ "filesystem",
12565
+ "github-releases",
12566
+ "notion",
12567
+ "sanity",
12568
+ "mdx-remote"
12569
+ ];
11773
12570
  var page = (title, description, body) => `---
11774
12571
  title: ${title}
11775
12572
  description: ${description}
@@ -11779,7 +12576,7 @@ ${body}
11779
12576
  `;
11780
12577
  var STARTERS = {
11781
12578
  api: {
11782
- config: configFor(`
12579
+ configExtra: `
11783
12580
  openapi: {
11784
12581
  enabled: true,
11785
12582
  route: "/api",
@@ -11789,7 +12586,7 @@ var STARTERS = {
11789
12586
  spec: "https://petstore3.swagger.io/api/v3/openapi.json",
11790
12587
  },
11791
12588
  ],
11792
- },`),
12589
+ },`,
11793
12590
  files: (dir) => [
11794
12591
  {
11795
12592
  content: page("API Reference", "Explore the API.", "# API Reference\n\nYour OpenAPI spec renders at [`/api`](/api). Point `openapi.sources` at your own spec in `blume.config.ts`."),
@@ -11798,13 +12595,13 @@ var STARTERS = {
11798
12595
  ]
11799
12596
  },
11800
12597
  changelog: {
11801
- config: configFor(`
12598
+ configExtra: `
11802
12599
  navigation: {
11803
12600
  tabs: [
11804
12601
  { label: "Docs", path: "/" },
11805
12602
  { label: "Changelog", path: "/changelog" },
11806
12603
  ],
11807
- },`),
12604
+ },`,
11808
12605
  files: (dir) => [
11809
12606
  {
11810
12607
  content: page("Introduction", "Welcome to your new Blume docs.", "# Introduction\n\nWrite your docs here, and log releases under `changelog/`."),
@@ -11824,7 +12621,7 @@ The first release. Edit \`${dir}/changelog/v1-0-0.mdx\` or add new entries besid
11824
12621
  ]
11825
12622
  },
11826
12623
  docs: {
11827
- config: configFor(""),
12624
+ configExtra: "",
11828
12625
  files: (dir) => [
11829
12626
  {
11830
12627
  content: page("Introduction", "Welcome to your new Blume docs.", `# Introduction
@@ -11837,7 +12634,7 @@ Edit \`${dir}/index.mdx\` to get started, then run \`blume dev\`.`),
11837
12634
  ]
11838
12635
  },
11839
12636
  sdk: {
11840
- config: configFor(""),
12637
+ configExtra: "",
11841
12638
  files: (dir) => [
11842
12639
  {
11843
12640
  content: page("Introduction", "Get started with the SDK.", `# Introduction
@@ -11853,26 +12650,331 @@ Install the SDK and make your first call. See [Installation](/installation).`),
11853
12650
  }
11854
12651
  };
11855
12652
  var commandsFor = (pm) => ({
12653
+ build: pm === "npm" || pm === "bun" ? `${pm} run build` : `${pm} build`,
11856
12654
  dev: pm === "npm" ? "npm run dev" : `${pm} dev`,
12655
+ exec: { bun: "bunx", npm: "npx", pnpm: "pnpm exec", yarn: "yarn" }[pm],
11857
12656
  install: `${pm} install`
11858
12657
  });
11859
- var writeFileSafe = async (path, content) => {
11860
- if (existsSync18(path)) {
11861
- logger.info(`Skipped existing ${path}`);
12658
+ var detectPackageManager = (userAgent) => {
12659
+ const name = userAgent?.split("/")[0];
12660
+ return name !== undefined && PACKAGE_MANAGERS.includes(name) ? name : "npm";
12661
+ };
12662
+ var validateContentDir = (root, dir) => isAbsolute9(dir) || relative13(root, join29(root, dir)).startsWith("..") ? "Must be a relative path inside the project." : undefined;
12663
+ var titleize = (raw) => {
12664
+ const words = raw.replaceAll(/[-_.]+/gu, " ").split(/\s+/u).filter(Boolean);
12665
+ return words.length === 0 ? "My Docs" : words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
12666
+ };
12667
+ var hasRemoteSource = (sources) => sources.some((source) => source !== "filesystem");
12668
+ var sourceSnippetFor = (kind) => {
12669
+ switch (kind) {
12670
+ case "github-releases": {
12671
+ return ` // Changelog entries from GitHub Releases. Private repos read
12672
+ // GITHUB_TOKEN from the environment.
12673
+ {
12674
+ type: "github-releases",
12675
+ owner: "your-org",
12676
+ repo: "your-repo",
12677
+ prefix: "changelog",
12678
+ },`;
12679
+ }
12680
+ case "notion": {
12681
+ return ` // Pages from a Notion database. Reads NOTION_TOKEN from the environment.
12682
+ {
12683
+ type: "notion",
12684
+ database: "your-database-id",
12685
+ prefix: "notion",
12686
+ },`;
12687
+ }
12688
+ case "sanity": {
12689
+ return ` // Documents from a Sanity dataset. Private datasets read SANITY_TOKEN
12690
+ // from the environment.
12691
+ {
12692
+ type: "sanity",
12693
+ projectId: "your-project-id",
12694
+ dataset: "production",
12695
+ query: \`*[_type == "doc"]\`,
12696
+ prefix: "sanity",
12697
+ },`;
12698
+ }
12699
+ case "mdx-remote": {
12700
+ return ` // MDX fetched from a GitHub repo. Private repos read GITHUB_TOKEN
12701
+ // from the environment.
12702
+ {
12703
+ type: "mdx-remote",
12704
+ github: { owner: "your-org", repo: "your-repo", path: "docs" },
12705
+ prefix: "remote",
12706
+ },`;
12707
+ }
12708
+ default: {
12709
+ return kind;
12710
+ }
12711
+ }
12712
+ };
12713
+ var contentBlockFor = (answers) => {
12714
+ const sources = answers.sources.length === 0 ? ["filesystem"] : answers.sources;
12715
+ if (!hasRemoteSource(sources)) {
12716
+ return answers.contentDir === "docs" ? "" : `
12717
+ content: {
12718
+ root: ${JSON.stringify(answers.contentDir)},
12719
+ },`;
12720
+ }
12721
+ const entries = SOURCE_KINDS.filter((kind) => sources.includes(kind)).map((kind) => kind === "filesystem" ? ` { type: "filesystem", root: ${JSON.stringify(answers.contentDir)} },` : sourceSnippetFor(kind));
12722
+ return `
12723
+ content: {
12724
+ sources: [
12725
+ ${entries.join(`
12726
+ `)}
12727
+ ],
12728
+ },`;
12729
+ };
12730
+ var buildConfig = (answers) => `import { defineConfig } from "blume";
12731
+
12732
+ export default defineConfig({
12733
+ title: ${JSON.stringify(answers.title)},
12734
+ description: "Documentation powered by Blume.",${STARTERS[answers.template].configExtra}${contentBlockFor(answers)}
12735
+ });
12736
+ `;
12737
+ var extraDepsFor = (sources) => ({
12738
+ ...sources.includes("notion") && { "@notionhq/client": "^2.2.15" },
12739
+ ...sources.includes("sanity") && { "@sanity/client": "^6.21.0" }
12740
+ });
12741
+ var buildPlan = (root, answers) => {
12742
+ const files = [
12743
+ {
12744
+ content: blumePackageJson(toPackageName(basename4(root)), extraDepsFor(answers.sources)),
12745
+ path: join29(root, "package.json")
12746
+ },
12747
+ { content: buildConfig(answers), path: join29(root, "blume.config.ts") }
12748
+ ];
12749
+ if (answers.sources.length === 0 || answers.sources.includes("filesystem")) {
12750
+ files.push(...STARTERS[answers.template].files(answers.contentDir).map((file) => ({ ...file, path: join29(root, file.path) })));
12751
+ }
12752
+ return files;
12753
+ };
12754
+ var writeFileSafe = async (file, log) => {
12755
+ if (existsSync18(file.path)) {
12756
+ log.info(`Skipped existing ${file.path}`);
11862
12757
  return false;
11863
12758
  }
11864
- await mkdir8(dirname11(path), { recursive: true });
11865
- await writeFile10(path, content, "utf-8");
11866
- logger.success(`Created ${path}`);
12759
+ await mkdir8(dirname11(file.path), { recursive: true });
12760
+ await writeFile10(file.path, file.content, "utf-8");
12761
+ log.success(`Created ${file.path}`);
11867
12762
  return true;
11868
12763
  };
12764
+ var applyPlan = async (files, log) => {
12765
+ const created = await Promise.all(files.map((file) => writeFileSafe(file, log)));
12766
+ const createdPackage = files.some((file, index) => created[index] && basename4(file.path) === "package.json");
12767
+ return { createdPackage };
12768
+ };
12769
+ var envVarsFor = (sources) => [
12770
+ ["GITHUB_TOKEN", ["github-releases", "mdx-remote"]],
12771
+ ["NOTION_TOKEN", ["notion"]],
12772
+ ["SANITY_TOKEN", ["sanity"]]
12773
+ ].filter(([, kinds]) => kinds.some((kind) => sources.includes(kind))).map(([envVar]) => envVar);
12774
+ var nextSteps = (answers, createdPackage) => {
12775
+ const commands = commandsFor(answers.packageManager);
12776
+ const lines = [];
12777
+ if (answers.directory !== ".") {
12778
+ lines.push(`cd ${answers.directory}`);
12779
+ }
12780
+ if (createdPackage) {
12781
+ lines.push(commands.install);
12782
+ }
12783
+ lines.push(commands.dev);
12784
+ const envVars = envVarsFor(answers.sources);
12785
+ const auth = envVars.length > 0 ? `
12786
+ Set ${envVars.join(" and ")} in .env.local so your sources can authenticate.
12787
+ ` : "";
12788
+ return `Next steps:
12789
+
12790
+ ${lines.join(`
12791
+ `)}
12792
+ ${auth}`;
12793
+ };
12794
+
12795
+ // src/cli/commands/eject.ts
12796
+ var reportDroppedArtifacts = (notices) => {
12797
+ if (notices.length === 0) {
12798
+ return;
12799
+ }
12800
+ logger.warn([
12801
+ "The ejected build script runs plain `astro build`, which stops producing these `blume build` artifacts:",
12802
+ ...notices.map((notice) => ` - ${notice}`)
12803
+ ].join(`
12804
+ `));
12805
+ };
12806
+ var ejectCommand = defineCommand6({
12807
+ args: {
12808
+ yes: { description: "Skip the confirmation prompt.", type: "boolean" }
12809
+ },
12810
+ meta: {
12811
+ description: "Promote the generated runtime into an owned Astro project.",
12812
+ name: "eject"
12813
+ },
12814
+ async run({ args }) {
12815
+ const root = process.cwd();
12816
+ refuseIfDevRunning(root, "ejecting");
12817
+ let notices = [];
12818
+ try {
12819
+ const { config } = await loadConfig(root);
12820
+ notices = droppedArtifactNotices(config);
12821
+ } catch {}
12822
+ if (!args.yes) {
12823
+ logger.warn("Eject is one-way: it writes astro.config.mjs, src/, and (if absent) tsconfig.json, rewrites your package.json scripts, and removes .blume. An existing tsconfig.json is left untouched.");
12824
+ reportDroppedArtifacts(notices);
12825
+ logger.info("Re-run with --yes to proceed.");
12826
+ return;
12827
+ }
12828
+ const { files, warnings } = await eject(root);
12829
+ await updatePackageScripts(root);
12830
+ for (const warning of warnings) {
12831
+ logger.warn(warning);
12832
+ }
12833
+ logger.success(`Ejected ${files.length} file(s):`);
12834
+ for (const file of files) {
12835
+ process.stdout.write(` ${relative14(root, file)}
12836
+ `);
12837
+ }
12838
+ reportDroppedArtifacts(notices);
12839
+ const pm = detectPackageManager(process.env.npm_config_user_agent);
12840
+ const { build: build2, dev: dev2 } = commandsFor(pm);
12841
+ logger.box(`Your project is now a standalone Astro app.
12842
+
12843
+ ${dev2}
12844
+ ${build2}
12845
+
12846
+ The blume package remains importable.`);
12847
+ }
12848
+ });
12849
+
12850
+ // src/cli/commands/init.ts
12851
+ import * as clack from "@clack/prompts";
12852
+ import { defineCommand as defineCommand7 } from "citty";
12853
+ import { resolve as resolve9 } from "pathe";
12854
+
12855
+ // src/cli/init/questions.ts
12856
+ import { basename as basename5, resolve as resolve8 } from "pathe";
12857
+ var cancelled = (value) => typeof value === "symbol";
12858
+ var collectAnswers = async (prompter, flags, defaults) => {
12859
+ const directory = flags.directory ?? await prompter.text({
12860
+ defaultValue: ".",
12861
+ message: "Where should we create your project?",
12862
+ placeholder: "./my-docs"
12863
+ });
12864
+ if (cancelled(directory)) {
12865
+ return null;
12866
+ }
12867
+ const root = resolve8(defaults.cwd, directory);
12868
+ const title = await prompter.text({
12869
+ initialValue: titleize(basename5(root)),
12870
+ message: "What's your docs site called?",
12871
+ validate: (value) => value?.trim() ? undefined : "Give your docs site a name."
12872
+ });
12873
+ if (cancelled(title)) {
12874
+ return null;
12875
+ }
12876
+ const template = flags.template ?? await prompter.select({
12877
+ message: "Which template?",
12878
+ options: [
12879
+ { hint: "Markdown docs site", label: "docs", value: "docs" },
12880
+ { hint: "OpenAPI reference at /api", label: "api", value: "api" },
12881
+ { hint: "SDK docs with an install page", label: "sdk", value: "sdk" },
12882
+ {
12883
+ hint: "Docs plus a changelog tab",
12884
+ label: "changelog",
12885
+ value: "changelog"
12886
+ }
12887
+ ]
12888
+ });
12889
+ if (cancelled(template)) {
12890
+ return null;
12891
+ }
12892
+ const picked = await prompter.multiselect({
12893
+ initialValues: ["filesystem"],
12894
+ message: "Where does your content live?",
12895
+ options: [
12896
+ { hint: "Local .mdx files", label: "filesystem", value: "filesystem" },
12897
+ {
12898
+ hint: "Changelog from GitHub Releases",
12899
+ label: "github-releases",
12900
+ value: "github-releases"
12901
+ },
12902
+ { hint: "A Notion database", label: "notion", value: "notion" },
12903
+ { hint: "A Sanity dataset", label: "sanity", value: "sanity" },
12904
+ {
12905
+ hint: "MDX fetched from a GitHub repo",
12906
+ label: "mdx-remote",
12907
+ value: "mdx-remote"
12908
+ }
12909
+ ],
12910
+ required: false
12911
+ });
12912
+ if (cancelled(picked)) {
12913
+ return null;
12914
+ }
12915
+ const sources = picked.length === 0 ? ["filesystem"] : picked;
12916
+ let contentDir = flags.contentDir ?? "docs";
12917
+ if (flags.contentDir === undefined && sources.includes("filesystem")) {
12918
+ const answer = await prompter.text({
12919
+ defaultValue: "docs",
12920
+ message: "Content directory?",
12921
+ placeholder: "docs",
12922
+ validate: (value) => validateContentDir(root, value || "docs")
12923
+ });
12924
+ if (cancelled(answer)) {
12925
+ return null;
12926
+ }
12927
+ contentDir = answer;
12928
+ }
12929
+ return {
12930
+ contentDir,
12931
+ directory,
12932
+ packageManager: flags.packageManager ?? detectPackageManager(defaults.userAgent),
12933
+ sources,
12934
+ template,
12935
+ title
12936
+ };
12937
+ };
12938
+
12939
+ // src/cli/commands/init.ts
12940
+ var ejectScaffold = async (root, answers) => {
12941
+ const commands = commandsFor(answers.packageManager);
12942
+ const cd = answers.directory === "." ? [] : [`cd ${answers.directory}`];
12943
+ try {
12944
+ await eject(root);
12945
+ await updatePackageScripts(root);
12946
+ logger.success("Ejected to a standalone Astro project.");
12947
+ const steps2 = [...cd, commands.install, commands.dev];
12948
+ logger.box(`Next steps:
12949
+
12950
+ ${steps2.join(`
12951
+ `)}
12952
+ `);
12953
+ } catch (error) {
12954
+ logger.warn(`Scaffolded, but eject needs the project's dependencies installed to load blume.config.ts: ${error.message}`);
12955
+ const steps2 = [
12956
+ ...cd,
12957
+ commands.install,
12958
+ `${commands.exec} blume eject --yes`
12959
+ ];
12960
+ logger.box(`Next steps:
12961
+
12962
+ ${steps2.join(`
12963
+ `)}
12964
+ `);
12965
+ }
12966
+ };
11869
12967
  var initCommand = defineCommand7({
11870
12968
  args: {
11871
12969
  "content-dir": {
11872
- default: "docs",
11873
12970
  description: "Content directory.",
11874
12971
  type: "string"
11875
12972
  },
12973
+ dir: {
12974
+ description: "Directory to scaffold into (default: current directory).",
12975
+ required: false,
12976
+ type: "positional"
12977
+ },
11876
12978
  eject: {
11877
12979
  description: "Eject to a standalone Astro project after scaffolding.",
11878
12980
  type: "boolean"
@@ -11885,66 +12987,74 @@ var initCommand = defineCommand7({
11885
12987
  description: "Starter template: docs | api | sdk | changelog.",
11886
12988
  type: "string"
11887
12989
  },
11888
- yes: { description: "Skip prompts.", type: "boolean" }
12990
+ yes: {
12991
+ description: "Skip prompts and scaffold with defaults.",
12992
+ type: "boolean"
12993
+ }
11889
12994
  },
11890
12995
  meta: {
11891
12996
  description: "Scaffold a minimal Blume project.",
11892
12997
  name: "init"
11893
12998
  },
11894
12999
  async run({ args }) {
11895
- const root = process.cwd();
11896
- const contentDir = args["content-dir"] ?? "docs";
11897
- if (isAbsolute9(contentDir) || relative14(root, join29(root, contentDir)).startsWith("..")) {
11898
- logger.error(`Invalid --content-dir "${contentDir}" (must be a path inside the project).`);
11899
- process.exit(1);
11900
- }
11901
- const template = args.template ?? "docs";
11902
- if (!TEMPLATES.includes(template)) {
13000
+ const cwd = process.cwd();
13001
+ const template = args.template;
13002
+ if (template !== undefined && !TEMPLATES.includes(template)) {
11903
13003
  logger.error(`Unknown template "${args.template}" (use ${TEMPLATES.join(" | ")}).`);
11904
13004
  process.exit(1);
11905
13005
  }
11906
- const pm = args["package-manager"] ?? "npm";
11907
- if (!PACKAGE_MANAGERS.includes(pm)) {
13006
+ const pm = args["package-manager"];
13007
+ if (pm !== undefined && !PACKAGE_MANAGERS.includes(pm)) {
11908
13008
  logger.error(`Unknown package manager "${args["package-manager"]}" (use ${PACKAGE_MANAGERS.join(" | ")}).`);
11909
13009
  process.exit(1);
11910
13010
  }
11911
- const starter = STARTERS[template];
11912
- const createdPackage = await writeFileSafe(join29(root, "package.json"), blumePackageJson(toPackageName(basename4(root))));
11913
- await writeFileSafe(join29(root, "blume.config.ts"), starter.config);
11914
- await Promise.all(starter.files(contentDir).map((file) => writeFileSafe(join29(root, file.path), file.content)));
13011
+ const interactive = !args.yes && process.stdin.isTTY === true && clack.isTTY(process.stdout) && !clack.isCI();
13012
+ let answers;
13013
+ if (interactive) {
13014
+ clack.intro("blume init");
13015
+ const collected = await collectAnswers(clack, {
13016
+ contentDir: args["content-dir"],
13017
+ directory: args.dir,
13018
+ packageManager: pm,
13019
+ template
13020
+ }, { cwd, userAgent: process.env.npm_config_user_agent });
13021
+ if (collected === null) {
13022
+ clack.cancel("Cancelled — nothing was written.");
13023
+ process.exit(0);
13024
+ }
13025
+ answers = collected;
13026
+ } else {
13027
+ answers = {
13028
+ contentDir: args["content-dir"] ?? "docs",
13029
+ directory: args.dir ?? ".",
13030
+ packageManager: pm ?? detectPackageManager(process.env.npm_config_user_agent),
13031
+ sources: ["filesystem"],
13032
+ template: template ?? "docs",
13033
+ title: "My Docs"
13034
+ };
13035
+ }
13036
+ const root = resolve9(cwd, answers.directory);
13037
+ if (validateContentDir(root, answers.contentDir) !== undefined) {
13038
+ logger.error(`Invalid --content-dir "${answers.contentDir}" (must be a path inside the project).`);
13039
+ process.exit(1);
13040
+ }
13041
+ const sink = interactive ? clack.log : logger;
13042
+ const { createdPackage } = await applyPlan(buildPlan(root, answers), sink);
11915
13043
  const ignored = await ensureGitignore(root, [".blume/", "dist/"]);
11916
13044
  if (ignored.length > 0) {
11917
- logger.success(`Added ${ignored.join(", ")} to .gitignore`);
13045
+ sink.success(`Added ${ignored.join(", ")} to .gitignore`);
11918
13046
  }
11919
- const commands = commandsFor(pm);
11920
13047
  if (args.eject) {
11921
- try {
11922
- await eject(root);
11923
- logger.success("Ejected to a standalone Astro project.");
11924
- logger.box(`Next steps:
11925
-
11926
- ${commands.install}
11927
- npx astro dev
11928
- `);
11929
- } catch (error) {
11930
- logger.warn(`Scaffolded, but eject failed: ${error.message}`);
11931
- logger.box(`Next steps:
11932
-
11933
- ${commands.install}
11934
- blume eject --yes
11935
- `);
11936
- }
13048
+ await ejectScaffold(root, answers);
11937
13049
  return;
11938
13050
  }
11939
- const nextSteps = createdPackage ? `Next steps:
11940
-
11941
- ${commands.install}
11942
- ${commands.dev}
11943
- ` : `Next steps:
11944
-
11945
- ${commands.dev}
11946
- `;
11947
- logger.box(nextSteps);
13051
+ const steps2 = nextSteps(answers, createdPackage);
13052
+ if (interactive) {
13053
+ clack.note(steps2.trimEnd());
13054
+ clack.outro("You're all set.");
13055
+ } else {
13056
+ logger.box(steps2);
13057
+ }
11948
13058
  }
11949
13059
  });
11950
13060
 
@@ -11974,7 +13084,7 @@ var previewCommand = defineCommand8({
11974
13084
  logLevel: "info",
11975
13085
  root: context.outDir,
11976
13086
  server: {
11977
- host: args.host ?? false,
13087
+ host: normalizeHost(args.host),
11978
13088
  port: parsePort(args.port)
11979
13089
  }
11980
13090
  });
@@ -12030,7 +13140,7 @@ import { join as join33 } from "pathe";
12030
13140
 
12031
13141
  // src/core/links.ts
12032
13142
  import { existsSync as existsSync20 } from "node:fs";
12033
- import { basename as basename5, join as join32 } from "pathe";
13143
+ import { basename as basename6, join as join32 } from "pathe";
12034
13144
  var HTTP = /^https?:\/\//iu;
12035
13145
  var PROTOCOL_RELATIVE = /^\/\//u;
12036
13146
  var SCHEME = /^[a-z][a-z0-9+.-]*:/iu;
@@ -12050,10 +13160,8 @@ var STATUS_GONE = 410;
12050
13160
  var STATUS_METHOD_NOT_ALLOWED = 405;
12051
13161
  var STATUS_NOT_IMPLEMENTED = 501;
12052
13162
  var assetIsPresent = (resolved, ctx) => ctx.publicDir !== null && existsSync20(join32(ctx.publicDir, resolved));
12053
- var isIndexPage = (page2) => {
12054
- const ref = page2.source?.ref ?? page2.sourcePath ?? "";
12055
- return /^index\.(?:md|mdx)$/iu.test(basename5(ref));
12056
- };
13163
+ var NUMERIC_PREFIX3 = /^\d+[-_.]/u;
13164
+ var isIndexPage = (page2) => /^index\.(?:md|mdx)$/iu.test(basename6(page2.navPath).replace(NUMERIC_PREFIX3, ""));
12057
13165
  var applyRelativePart = (segments, part) => {
12058
13166
  if (part === "" || part === ".") {
12059
13167
  return;
@@ -12107,6 +13215,9 @@ var checkPathLink = (resolved, fragment, target, site, ctx) => {
12107
13215
  if (ctx.routes.has(route)) {
12108
13216
  return fragment ? checkAnchor(route, fragment, site, ctx) : null;
12109
13217
  }
13218
+ if (ctx.extraRoutes.has(route)) {
13219
+ return null;
13220
+ }
12110
13221
  if (ctx.redirects.has(route)) {
12111
13222
  return null;
12112
13223
  }
@@ -12244,6 +13355,7 @@ var validateLinks = async (graph, options) => {
12244
13355
  const ctx = {
12245
13356
  anchors: buildAnchorIndex(graph.pages),
12246
13357
  basePath,
13358
+ extraRoutes: new Set((options.extraRoutes ?? []).map(toRoute)),
12247
13359
  publicDir: options.publicDir,
12248
13360
  redirects: new Set((options.redirects ?? []).map((redirect) => toRoute(withBasePath(basePath, redirect.from)))),
12249
13361
  routes: new Set(graph.routes.keys())
@@ -12300,10 +13412,24 @@ var validateCommand = defineCommand10({
12300
13412
  try {
12301
13413
  const project = await scanProject(root, { mode: "build" });
12302
13414
  diagnostics.push(...project.diagnostics);
13415
+ const userPages = project.context.pagesRoot ? await discoverPages(project.context.pagesRoot) : [];
13416
+ const extraRoutes = customStaticRoutes(userPages);
13417
+ if (hasGeneratedChangelog(project, userPages)) {
13418
+ extraRoutes.push("/changelog");
13419
+ }
13420
+ if (project.config.i18n) {
13421
+ const manifest = buildManifest({
13422
+ config: project.config,
13423
+ context: project.context,
13424
+ graph: project.graph
13425
+ });
13426
+ extraRoutes.push(...manifest.routes.flatMap((route) => route.fallback ? [route.path] : []));
13427
+ }
12303
13428
  const publicDir = join33(root, "public");
12304
13429
  diagnostics.push(...await validateLinks(project.graph, {
12305
13430
  basePath: project.config.basePath,
12306
13431
  checkExternal: Boolean(args.external),
13432
+ extraRoutes,
12307
13433
  publicDir: existsSync21(publicDir) ? publicDir : null,
12308
13434
  redirects: project.config.redirects
12309
13435
  }));
@@ -12315,9 +13441,10 @@ var validateCommand = defineCommand10({
12315
13441
  process.exit(1);
12316
13442
  }
12317
13443
  }
13444
+ const strictFailure = Boolean(args.strict) && diagnostics.some((diagnostic) => diagnostic.severity !== "info");
12318
13445
  if (args.json) {
12319
13446
  const hadErrors2 = reportDiagnosticsJson(diagnostics, root);
12320
- if (hadErrors2 || Boolean(args.strict) && diagnostics.length > 0) {
13447
+ if (hadErrors2 || strictFailure) {
12321
13448
  await flushStdout();
12322
13449
  process.exit(1);
12323
13450
  }
@@ -12327,7 +13454,7 @@ var validateCommand = defineCommand10({
12327
13454
  if (diagnostics.length === 0) {
12328
13455
  logger.success("No broken links found.");
12329
13456
  }
12330
- if (hadErrors || Boolean(args.strict) && diagnostics.length > 0) {
13457
+ if (hadErrors || strictFailure) {
12331
13458
  process.exit(1);
12332
13459
  }
12333
13460
  }
@@ -12364,5 +13491,5 @@ process.on("unhandledRejection", (error) => {
12364
13491
  });
12365
13492
  runMain(main);
12366
13493
 
12367
- //# debugId=8E29DA45BB5B37C564756E2164756E21
13494
+ //# debugId=0106CF055BC5BCA264756E2164756E21
12368
13495
  //# sourceMappingURL=index.js.map