blume 1.0.3 → 1.1.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 (126) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/dist/cli/index.js +13784 -10579
  3. package/dist/cli/index.js.map +93 -61
  4. package/dist/types/core/config-input.d.ts +87 -8
  5. package/dist/types/core/data.d.ts +21 -0
  6. package/dist/types/core/deployment-env.d.ts +6 -0
  7. package/dist/types/core/diagnostics.d.ts +23 -0
  8. package/dist/types/core/i18n-ui.d.ts +140 -140
  9. package/dist/types/core/schema.d.ts +549 -370
  10. package/dist/types/core/sources/types.d.ts +3 -1
  11. package/dist/types/core/standard-schema.d.ts +41 -0
  12. package/dist/types/core/types.d.ts +23 -0
  13. package/dist/types/og/card.d.ts +63 -0
  14. package/dist/types/og/dimensions.d.ts +12 -0
  15. package/dist/types/openapi/references.d.ts +12 -7
  16. package/docs/01-quickstart.mdx +1 -1
  17. package/docs/02-deployment.mdx +9 -1
  18. package/docs/advanced/api-reference.mdx +22 -3
  19. package/docs/advanced/changelog.mdx +1 -1
  20. package/docs/advanced/skills.mdx +1 -1
  21. package/docs/configuration/ai.mdx +1 -1
  22. package/docs/configuration/customization.mdx +1 -1
  23. package/docs/configuration/export.mdx +1 -1
  24. package/docs/configuration/index.mdx +21 -1
  25. package/docs/configuration/search.mdx +28 -1
  26. package/docs/configuration/seo.mdx +40 -2
  27. package/docs/configuration/theming.mdx +1 -1
  28. package/docs/content/components.mdx +15 -2
  29. package/docs/content/index.mdx +1 -1
  30. package/docs/content/meta.mdx +1 -1
  31. package/docs/content/navigation.mdx +11 -1
  32. package/docs/content/sources.mdx +1 -1
  33. package/docs/content/syntax.mdx +116 -4
  34. package/docs/reference/cli.mdx +79 -1
  35. package/docs/reference/frontmatter.mdx +29 -1
  36. package/package.json +3 -3
  37. package/skills/blume-migrate/SKILL.md +170 -0
  38. package/skills/blume-migrate/assets/oxfmt@0.55.0.patch +20 -0
  39. package/skills/blume-migrate/references/docusaurus.md +95 -0
  40. package/skills/blume-migrate/references/fumadocs.md +95 -0
  41. package/skills/blume-migrate/references/mintlify.md +156 -0
  42. package/skills/blume-migrate/references/monorepo.md +224 -0
  43. package/skills/blume-migrate/references/nextra.md +76 -0
  44. package/skills/blume-migrate/references/starlight.md +116 -0
  45. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +478 -0
  46. package/src/ai/llms.ts +15 -0
  47. package/src/astro/adapter-root.ts +70 -0
  48. package/src/astro/component-slots.ts +3 -2
  49. package/src/astro/generate.ts +132 -42
  50. package/src/astro/index.ts +1 -0
  51. package/src/astro/pages.ts +18 -3
  52. package/src/astro/templates.ts +158 -56
  53. package/src/audit/agent.ts +114 -0
  54. package/src/audit/catalog.ts +826 -0
  55. package/src/audit/checks/assets.ts +177 -0
  56. package/src/audit/checks/content.ts +231 -0
  57. package/src/audit/checks/duplicates.ts +131 -0
  58. package/src/audit/checks/i18n.ts +246 -0
  59. package/src/audit/checks/indexability.ts +213 -0
  60. package/src/audit/checks/links.ts +223 -0
  61. package/src/audit/checks/llms.ts +135 -0
  62. package/src/audit/checks/network.ts +272 -0
  63. package/src/audit/checks/og-image.ts +113 -0
  64. package/src/audit/checks/redirects.ts +87 -0
  65. package/src/audit/checks/robots.ts +114 -0
  66. package/src/audit/checks/sitemap.ts +229 -0
  67. package/src/audit/checks/social.ts +238 -0
  68. package/src/audit/crawl.ts +259 -0
  69. package/src/audit/graph.ts +74 -0
  70. package/src/audit/html.ts +54 -0
  71. package/src/audit/image-size.ts +63 -0
  72. package/src/audit/locate.ts +33 -0
  73. package/src/audit/redirects.ts +74 -0
  74. package/src/audit/report.ts +278 -0
  75. package/src/audit/run.ts +198 -0
  76. package/src/audit/snapshot.ts +189 -0
  77. package/src/audit/types.ts +214 -0
  78. package/src/audit/url.ts +103 -0
  79. package/src/cli/commands/audit.ts +205 -0
  80. package/src/cli/commands/build.ts +51 -12
  81. package/src/cli/index.ts +2 -0
  82. package/src/components/content/Callout.astro +8 -2
  83. package/src/components/content/Prompt.astro +25 -13
  84. package/src/components/content/Tabs.astro +98 -15
  85. package/src/components/layout/Breadcrumbs.astro +1 -1
  86. package/src/components/layout/Header.astro +5 -8
  87. package/src/components/layout/Logo.astro +13 -1
  88. package/src/components/layout/PageFeedback.astro +2 -2
  89. package/src/components/layout/PageLayout.astro +9 -9
  90. package/src/components/layout/Pagination.astro +7 -7
  91. package/src/components/layout/RootLayout.astro +9 -11
  92. package/src/components/layout/Search.astro +36 -7
  93. package/src/components/layout/TableOfContents.astro +1 -1
  94. package/src/components/layout/nav-utils.ts +9 -7
  95. package/src/components/openapi/Authorization.astro +80 -0
  96. package/src/components/openapi/Operation.astro +19 -1
  97. package/src/components/openapi/ParametersTable.astro +1 -1
  98. package/src/components/openapi/security.ts +201 -0
  99. package/src/components/openapi/snippets.ts +42 -13
  100. package/src/core/config-input.ts +94 -8
  101. package/src/core/data.ts +18 -2
  102. package/src/core/deployment-env.ts +9 -0
  103. package/src/core/diagnostics.ts +59 -12
  104. package/src/core/links.ts +2 -91
  105. package/src/core/nav-diagnostics.ts +48 -4
  106. package/src/core/navigation.ts +55 -13
  107. package/src/core/probe.ts +136 -0
  108. package/src/core/project-graph.ts +8 -0
  109. package/src/core/schema.ts +100 -1
  110. package/src/core/sources/normalize.ts +198 -25
  111. package/src/core/sources/types.ts +3 -1
  112. package/src/core/sources/watch.ts +5 -0
  113. package/src/core/standard-schema.ts +54 -0
  114. package/src/core/types.ts +23 -0
  115. package/src/deploy/adapter-output.ts +27 -15
  116. package/src/deploy/headers.ts +66 -0
  117. package/src/deploy/redirects.ts +49 -9
  118. package/src/markdown/index.ts +2 -0
  119. package/src/markdown/language-icon.ts +2 -1
  120. package/src/markdown/table-wrap.ts +43 -0
  121. package/src/og/card.ts +128 -36
  122. package/src/og/index.ts +1 -1
  123. package/src/og/logo.ts +21 -0
  124. package/src/openapi/references.ts +19 -16
  125. package/src/search/popular.ts +33 -0
  126. package/src/theme/entry.ts +56 -6
@@ -0,0 +1,70 @@
1
+ import { pathToFileURL } from "node:url";
2
+
3
+ import type { AstroIntegration } from "astro";
4
+
5
+ /**
6
+ * Present a deploy adapter with `root` pointed at the real project root rather
7
+ * than the hidden `.blume` runtime.
8
+ *
9
+ * Astro's `root` and `outDir` normally sit together (`outDir` defaults to
10
+ * `<root>/dist`), and `@astrojs/vercel` leans on that: it writes its Build
11
+ * Output tree to `<root>/.vercel/output` and — the part that bites — traces the
12
+ * function's dependency closure with `@vercel/nft` using a base derived from
13
+ * `root`, silently dropping every traced file that falls outside it.
14
+ *
15
+ * Blume splits the two: `root` is `<project>/.blume`, so Astro resolves the
16
+ * runtime's own `package.json` and its `node_modules` junction, while `outDir`
17
+ * stays at `<project>/dist` so the build lands where users expect. That puts
18
+ * `build.server` (`<outDir>/server`) *outside* `root`, so nft's base excludes
19
+ * the server bundle entirely: the traced file list collapses to `entry.mjs`
20
+ * alone, and the deployed function dies on its first import with
21
+ * ERR_MODULE_NOT_FOUND — missing its chunks, its virtual middleware, and every
22
+ * npm dependency.
23
+ *
24
+ * A project inside a workspace accidentally escapes this, because nft's base
25
+ * search climbs past `.blume` to the workspace root, which does contain both
26
+ * `dist/` and `node_modules` — which is why the bug only ever surfaced in
27
+ * standalone projects.
28
+ *
29
+ * Handing the adapter the root its own `outDir` assumption implies restores the
30
+ * invariant without moving Astro's real root: the trace covers `dist/server`
31
+ * and `node_modules`, and the Build Output tree lands at the project root
32
+ * natively, where `vercel deploy --prebuilt` looks for it.
33
+ *
34
+ * `astro:config:setup` and `astro:config:done` are the only hooks handed a
35
+ * `config`; an adapter reads `root` from one or both and closes over it for its
36
+ * later build hooks, so overriding it there covers the whole adapter.
37
+ */
38
+ const stripTrailingSlashes = (value: string): string => {
39
+ let end = value.length;
40
+
41
+ while (end > 0 && value[end - 1] === "/") {
42
+ end -= 1;
43
+ }
44
+
45
+ return value.slice(0, end);
46
+ };
47
+
48
+ export const withAdapterRoot = (
49
+ integration: AstroIntegration,
50
+ root: string
51
+ ): AstroIntegration => {
52
+ const rootUrl = pathToFileURL(`${stripTrailingSlashes(root)}/`);
53
+ const setup = integration.hooks["astro:config:setup"];
54
+ const done = integration.hooks["astro:config:done"];
55
+
56
+ return {
57
+ ...integration,
58
+ hooks: {
59
+ ...integration.hooks,
60
+ ...(setup && {
61
+ "astro:config:setup": (options) =>
62
+ setup({ ...options, config: { ...options.config, root: rootUrl } }),
63
+ }),
64
+ ...(done && {
65
+ "astro:config:done": (options) =>
66
+ done({ ...options, config: { ...options.config, root: rootUrl } }),
67
+ }),
68
+ },
69
+ };
70
+ };
@@ -31,8 +31,9 @@ export interface ComponentSlotPlan {
31
31
  }
32
32
 
33
33
  const EMPTY_MODULE = `// Generated by Blume. Do not edit.
34
- export const mdxComponents = {};
35
- export const layoutOverrides = {};
34
+ import type { ComponentOverride } from "blume/core/define-components.ts";
35
+ export const mdxComponents: Record<string, ComponentOverride> = {};
36
+ export const layoutOverrides: Record<string, ComponentOverride> = {};
36
37
  `;
37
38
 
38
39
  // A user-supplied attribute value interpolated into a generated .astro tag: a
@@ -29,7 +29,10 @@ import type {
29
29
  } from "../core/data.ts";
30
30
  import { EN_UI, resolveUIStrings } from "../core/i18n-ui.ts";
31
31
  import { resolveFallbackLocale } from "../core/i18n.ts";
32
- import { validateNavTargets } from "../core/nav-diagnostics.ts";
32
+ import {
33
+ validateNavTargets,
34
+ validateSearchPopularIcons,
35
+ } from "../core/nav-diagnostics.ts";
33
36
  import { packageRoot } from "../core/package-root.ts";
34
37
  import type { BlumeProject } from "../core/project-graph.ts";
35
38
  import type { ResolvedConfig } from "../core/schema.ts";
@@ -37,11 +40,13 @@ import { resolveDocsCollection } from "../core/sources/resolve.ts";
37
40
  import { resolveTsconfigAliases } from "../core/tsconfig-aliases.ts";
38
41
  import type { Navigation } from "../core/types.ts";
39
42
  import { buildRssFeeds, renderRssFeed } from "../deploy/rss.ts";
40
- import { hasScalarReferences, referenceTabs } from "../openapi/references.ts";
43
+ import { resolveOgLogo } from "../og/logo.ts";
44
+ import { hasScalarReferences, referenceRoutes } from "../openapi/references.ts";
41
45
  import { buildReferenceFiles } from "../openapi/scalar.ts";
42
46
  import { isOpenApiSource } from "../openapi/source.ts";
43
47
  import { registry } from "../registry/registry.ts";
44
48
  import { buildSearchDocuments } from "../search/documents.ts";
49
+ import { resolveSearchPopular } from "../search/popular.ts";
45
50
  import { searchProviderMeta, servesStaticIndex } from "../search/providers.ts";
46
51
  import {
47
52
  examplesEntryTemplate,
@@ -144,23 +149,42 @@ const reactCompilerWarnings = (
144
149
  ]
145
150
  : [];
146
151
 
147
- /**
148
- * Realpath of the `astro` package node resolves from a directory, or null when
149
- * none resolves. Comparing this for `.blume/` against Blume's own deps tells
150
- * whether the runtime would bind to the *same* astro Blume uses or a different
151
- * one shadowing it (the hoisted-conflict failure mode).
152
- */
153
- const resolvedAstroPath = (fromDir: string): string | null => {
152
+ /** Resolve Astro's package.json directly inside a node_modules directory. */
153
+ const resolveAstroPackageJson = (modulesDir: string): string | null => {
154
154
  try {
155
- const pkg = createRequire(
156
- pathToFileURL(join(fromDir, "_.js")).href
157
- ).resolve("astro/package.json");
158
- return realpathSync(pkg);
155
+ return realpathSync(join(modulesDir, "astro", "package.json"));
159
156
  } catch {
160
157
  return null;
161
158
  }
162
159
  };
163
160
 
161
+ /**
162
+ * Realpath of the `astro` package reachable through the normal node_modules
163
+ * ancestor walk from a generated runtime, or null when none resolves.
164
+ *
165
+ * This deliberately does not use `createRequire().resolve()`. pnpm's generated
166
+ * bin shim adds Blume's virtual-store dependencies to `NODE_PATH`, which
167
+ * CommonJS resolution honors but ESM package resolution ignores. The generated
168
+ * Astro config uses ESM imports, so treating a NODE_PATH-only result as
169
+ * reachable skips the dependency link and makes `import "astro/config"` fail.
170
+ * Walking the physical node_modules ancestors mirrors the lookup that config
171
+ * actually gets.
172
+ */
173
+ const resolvedAstroPath = (fromDir: string): string | null => {
174
+ let dir = normalize(fromDir);
175
+ while (true) {
176
+ const resolved = resolveAstroPackageJson(join(dir, "node_modules"));
177
+ if (resolved) {
178
+ return resolved;
179
+ }
180
+ const parent = dirname(dir);
181
+ if (parent === dir) {
182
+ return null;
183
+ }
184
+ dir = parent;
185
+ }
186
+ };
187
+
164
188
  /**
165
189
  * Locate the directory that holds Blume's installed dependencies (Astro and its
166
190
  * integrations).
@@ -277,7 +301,7 @@ export const ensureDepsLink = async (
277
301
  }
278
302
  // Already correct when `.blume/` resolves the very same astro Blume's deps
279
303
  // provide — the clean hoisted case, nothing to do.
280
- const blumeAstro = resolvedAstroPath(depsDir);
304
+ const blumeAstro = resolveAstroPackageJson(depsDir);
281
305
  const outDirAstro = resolvedAstroPath(outDir);
282
306
  if (blumeAstro && outDirAstro === blumeAstro) {
283
307
  return null;
@@ -298,7 +322,7 @@ export const ensureDepsLink = async (
298
322
 
299
323
  /**
300
324
  * Vite plugin that makes Blume's externalized runtime deps (zod, shiki, sharp,
301
- * `@takumi-rs/core`, …) resolvable when Astro executes the static prerender
325
+ * `takumi-js`, …) resolvable when Astro executes the static prerender
302
326
  * bundle under an isolated linker (Bun's `isolated` mode, pnpm).
303
327
  *
304
328
  * Astro's static build emits a self-contained SSR bundle to
@@ -626,7 +650,56 @@ const writeStagedContent = async (
626
650
  }
627
651
  };
628
652
 
629
- /** The logo shape the runtime consumes: an inline SVG or image URL(s). */
653
+ interface LogoDimensions {
654
+ height: number;
655
+ width: number;
656
+ }
657
+
658
+ const SVG_ROOT = /<svg\b(?<attributes>[^>]*)>/u;
659
+ const SVG_WIDTH = /\bwidth\s*=\s*["'](?<value>[^"']+)["']/u;
660
+ const SVG_HEIGHT = /\bheight\s*=\s*["'](?<value>[^"']+)["']/u;
661
+ const SVG_LENGTH = /^\s*(?<value>[\d.]+)(?:px)?\s*$/u;
662
+ const SVG_VIEW_BOX =
663
+ /\bviewBox\s*=\s*["'][\d.-]+[\s,]+[\d.-]+[\s,]+(?<width>[\d.]+)[\s,]+(?<height>[\d.]+)["']/u;
664
+
665
+ const parseSvgLength = (value: string | undefined): number | undefined => {
666
+ const length = Number(value?.match(SVG_LENGTH)?.groups?.value);
667
+ return length > 0 ? length : undefined;
668
+ };
669
+
670
+ /** Read dimensions from an SVG's explicit size or its view box. */
671
+ const svgDimensions = (svg: string | undefined): LogoDimensions | undefined => {
672
+ const attributes = svg?.match(SVG_ROOT)?.groups?.attributes;
673
+ const width = parseSvgLength(attributes?.match(SVG_WIDTH)?.groups?.value);
674
+ const height = parseSvgLength(attributes?.match(SVG_HEIGHT)?.groups?.value);
675
+ if (width && height) {
676
+ return { height, width };
677
+ }
678
+
679
+ const viewBox = attributes?.match(SVG_VIEW_BOX);
680
+ const viewBoxWidth = Number(viewBox?.groups?.width);
681
+ const viewBoxHeight = Number(viewBox?.groups?.height);
682
+ return viewBoxWidth > 0 && viewBoxHeight > 0
683
+ ? { height: viewBoxHeight, width: viewBoxWidth }
684
+ : undefined;
685
+ };
686
+
687
+ /** Read a local SVG logo from the project root or public directory. */
688
+ const readLogoSvg = (
689
+ project: BlumeProject,
690
+ source: string | undefined
691
+ ): string | undefined => {
692
+ if (!source?.toLowerCase().endsWith(".svg")) {
693
+ return;
694
+ }
695
+ const rel = source.replace(/^\//u, "");
696
+ const file = [
697
+ join(project.context.root, "public", rel),
698
+ join(project.context.root, rel),
699
+ ].find((path) => existsSync(path));
700
+ return file ? readFileSync(file, "utf-8") : undefined;
701
+ };
702
+
630
703
  /**
631
704
  * Resolve the configured logo. A single SVG is read and inlined so a
632
705
  * `currentColor` logo follows the theme; other images keep their URL for an
@@ -647,18 +720,20 @@ const resolveLogo = (project: BlumeProject): BlumeLogo | null => {
647
720
  const dark = image?.dark ?? image?.light;
648
721
  const alt = image?.alt ?? "";
649
722
  const brandHref = href ?? "/";
723
+ const lightSvg = readLogoSvg(project, light);
724
+ const darkSvg = dark === light ? lightSvg : readLogoSvg(project, dark);
650
725
 
651
- if (light && light === dark && light.toLowerCase().endsWith(".svg")) {
652
- const rel = light.replace(/^\//u, "");
653
- const file = [
654
- join(project.context.root, "public", rel),
655
- join(project.context.root, rel),
656
- ].find((path) => existsSync(path));
657
- if (file) {
658
- return { alt, href: brandHref, svg: readFileSync(file, "utf-8"), text };
659
- }
726
+ if (light && light === dark && lightSvg) {
727
+ return { alt, href: brandHref, svg: lightSvg, text };
660
728
  }
661
- return { alt, dark, href: brandHref, light, text };
729
+
730
+ const lightDimensions = svgDimensions(lightSvg);
731
+ const darkDimensions = svgDimensions(darkSvg);
732
+ const dimensions =
733
+ lightDimensions || darkDimensions
734
+ ? { dark: darkDimensions, light: lightDimensions }
735
+ : undefined;
736
+ return { alt, dark, dimensions, href: brandHref, light, text };
662
737
  };
663
738
 
664
739
  /**
@@ -779,6 +854,10 @@ export const buildRuntimeData = (project: BlumeProject): string => {
779
854
  ? `https://github.com/${github.owner}/${github.repo}`
780
855
  : null;
781
856
  const editBase = github ? `${repoUrl}/edit/${github.branch}` : null;
857
+ const logo = resolveLogo(project);
858
+ const ogLogo = config.seo.og.logo
859
+ ? resolveOgLogo(project, config.seo.og.logo)
860
+ : logo?.svg;
782
861
 
783
862
  const editUrlFor = (sourcePath?: string): string | null => {
784
863
  if (!(editBase && sourcePath)) {
@@ -791,13 +870,12 @@ export const buildRuntimeData = (project: BlumeProject): string => {
791
870
 
792
871
  const { i18n } = config;
793
872
 
794
- // API reference routes surface as header tabs alongside the content-derived
795
- // ones (Blume-rendered references also own a tab-scoped sidebar of operations),
796
- // so the reference stays discoverable in every locale.
797
- const withReferenceTabs = (nav: Navigation): Navigation => ({
873
+ // Resolve the header repo link per locale. API references no longer add a tab
874
+ // automatically — authors point a `navigation.tabs` entry at the reference
875
+ // route to surface it (see `referenceRoutes`).
876
+ const withRepoUrl = (nav: Navigation): Navigation => ({
798
877
  ...nav,
799
878
  repoUrl: config.navigation.repo && repoUrl ? repoUrl : null,
800
- tabs: [...nav.tabs, ...referenceTabs(config)],
801
879
  });
802
880
 
803
881
  // Resolved UI dictionaries: one per locale under i18n, English baseline
@@ -824,7 +902,7 @@ export const buildRuntimeData = (project: BlumeProject): string => {
824
902
  ? Object.fromEntries(
825
903
  i18n.locales.map(({ code }) => [
826
904
  code,
827
- withReferenceTabs(
905
+ withRepoUrl(
828
906
  graph.navigationByLocale[code] ?? {
829
907
  featured: [],
830
908
  selectors: [],
@@ -865,7 +943,7 @@ export const buildRuntimeData = (project: BlumeProject): string => {
865
943
  }
866
944
  : null,
867
945
  imageZoom: config.markdown.imageZoom,
868
- logo: resolveLogo(project),
946
+ logo,
869
947
  mcp: config.ai.mcp.enabled
870
948
  ? {
871
949
  name: config.ai.mcp.name ?? config.title,
@@ -874,10 +952,16 @@ export const buildRuntimeData = (project: BlumeProject): string => {
874
952
  : null,
875
953
  // `og.enabled` is resolved to a definite boolean in `loadConfig`; coerce
876
954
  // the optional schema type so the serialized shape stays `boolean`.
877
- og: { enabled: config.seo.og.enabled ?? false },
955
+ og: {
956
+ enabled: config.seo.og.enabled ?? false,
957
+ fonts: config.seo.og.fonts ?? [],
958
+ logo: ogLogo,
959
+ palette: config.seo.og.palette,
960
+ },
878
961
  repoUrl,
879
962
  search: {
880
963
  enabled: config.search.provider !== "none",
964
+ popular: resolveSearchPopular(config.search.popular, config.basePath),
881
965
  provider: config.search.provider,
882
966
  },
883
967
  site: config.deployment.site ?? null,
@@ -894,7 +978,7 @@ export const buildRuntimeData = (project: BlumeProject): string => {
894
978
  // CSS variables for Astro's <Font> component; matches the astro.config
895
979
  // `fonts:` entries derived from the same theme.fonts config.
896
980
  fontCssVars: configuredCssVars(config.theme.fonts),
897
- navigation: withReferenceTabs(graph.navigation),
981
+ navigation: withRepoUrl(graph.navigation),
898
982
  // Per-locale navigation; the catch-all selects the active locale's tree.
899
983
  navigationByLocale,
900
984
  routes: manifest.routes.map((route) => ({
@@ -1441,21 +1525,27 @@ export const generateRuntime = async (
1441
1525
 
1442
1526
  // Missing-navigation-target check, now that every servable route is known:
1443
1527
  // content routes, custom `.astro` pages, the generated changelog, and any
1444
- // OpenAPI reference tabs.
1528
+ // OpenAPI reference routes (so a tab an author points at one still validates).
1445
1529
  const navTargetRoutes = new Set<string>([
1446
1530
  ...project.graph.routes.keys(),
1447
1531
  ...pages.map((page) => page.pattern),
1448
- ...referenceTabs(config).map((tab) => tab.path),
1532
+ ...referenceRoutes(config),
1449
1533
  ]);
1450
1534
  if (hasGeneratedChangelog(project, pages)) {
1451
1535
  navTargetRoutes.add("/changelog");
1452
1536
  }
1537
+ // Curated `search.popular` icons live outside the navigation model, so they
1538
+ // miss `validateNavIcons` in the graph build — they're checked here too,
1539
+ // where the search config is known. A typo otherwise just renders the
1540
+ // default glyph.
1453
1541
  warnings.push(
1454
- ...validateNavTargets(project.graph.navigation, navTargetRoutes).map(
1455
- (diagnostic) =>
1456
- diagnostic.suggestion
1457
- ? `${diagnostic.message} ${diagnostic.suggestion}`
1458
- : diagnostic.message
1542
+ ...[
1543
+ ...validateNavTargets(project.graph.navigation, navTargetRoutes),
1544
+ ...validateSearchPopularIcons(config.search.popular),
1545
+ ].map((diagnostic) =>
1546
+ diagnostic.suggestion
1547
+ ? `${diagnostic.message} ${diagnostic.suggestion}`
1548
+ : diagnostic.message
1459
1549
  )
1460
1550
  );
1461
1551
 
@@ -1,3 +1,4 @@
1
+ export { withAdapterRoot } from "./adapter-root.ts";
1
2
  export {
2
3
  generateRuntime,
3
4
  prerenderDepsPlugin,
@@ -6,11 +6,25 @@ import type { BlumePageRoute } from "./integration.ts";
6
6
 
7
7
  const PAGE_GLOB = ["**/*.astro"];
8
8
 
9
+ /**
10
+ * Astro's routing convention: a file or folder whose name starts with `_` is a
11
+ * private partial — importable (shared layouts, home-page sections), but never
12
+ * built into a route. Blume injects pages itself, so it must reproduce the same
13
+ * exclusion or every `pages/_home/Hero.astro`-style component ships as an HTML
14
+ * page.
15
+ */
16
+ const isPrivatePage = (rel: string): boolean =>
17
+ rel.split("/").some((segment) => segment.startsWith("_"));
18
+
9
19
  /** Map discovered page files to routes; shared by the async/sync discoverers. */
10
20
  const toPageRoutes = (pagesRoot: string, files: string[]): BlumePageRoute[] => {
11
21
  files.sort();
12
- return files.map((file) => {
22
+ const routes: BlumePageRoute[] = [];
23
+ for (const file of files) {
13
24
  const rel = relative(pagesRoot, file);
25
+ if (isPrivatePage(rel)) {
26
+ continue;
27
+ }
14
28
  const withoutExt = rel.slice(0, rel.length - extname(rel).length);
15
29
  const parts = withoutExt.split("/");
16
30
  // Only a trailing `index` maps to its parent dir; a folder literally named
@@ -19,8 +33,9 @@ const toPageRoutes = (pagesRoot: string, files: string[]): BlumePageRoute[] => {
19
33
  parts.pop();
20
34
  }
21
35
  const pattern = parts.length === 0 ? "/" : `/${parts.join("/")}`;
22
- return { entrypoint: file, pattern };
23
- });
36
+ routes.push({ entrypoint: file, pattern });
37
+ }
38
+ return routes;
24
39
  };
25
40
 
26
41
  /**