blume 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/cli/index.js +496 -295
  3. package/dist/cli/index.js.map +18 -17
  4. package/dist/types/core/config-input.d.ts +44 -22
  5. package/dist/types/core/config.d.ts +3 -3
  6. package/dist/types/core/data.d.ts +12 -0
  7. package/dist/types/core/i18n-ui.d.ts +136 -136
  8. package/dist/types/core/schema.d.ts +502 -384
  9. package/dist/types/core/types.d.ts +10 -0
  10. package/dist/types/openapi/references.d.ts +12 -7
  11. package/docs/advanced/api-reference.mdx +11 -3
  12. package/docs/advanced/custom-pages.mdx +2 -0
  13. package/docs/configuration/ai.mdx +6 -4
  14. package/docs/configuration/index.mdx +6 -8
  15. package/docs/configuration/seo.mdx +20 -1
  16. package/docs/content/components.mdx +26 -5
  17. package/docs/content/navigation.mdx +10 -0
  18. package/docs/content/syntax.mdx +116 -4
  19. package/package.json +1 -1
  20. package/skills/blume-migrate/SKILL.md +170 -0
  21. package/skills/blume-migrate/assets/oxfmt@0.55.0.patch +20 -0
  22. package/skills/blume-migrate/references/docusaurus.md +95 -0
  23. package/skills/blume-migrate/references/fumadocs.md +95 -0
  24. package/skills/blume-migrate/references/mintlify.md +155 -0
  25. package/skills/blume-migrate/references/monorepo.md +224 -0
  26. package/skills/blume-migrate/references/nextra.md +76 -0
  27. package/skills/blume-migrate/references/starlight.md +116 -0
  28. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +466 -0
  29. package/src/ai/agent-readability.ts +3 -3
  30. package/src/ai/mcp/data.ts +2 -2
  31. package/src/astro/component-slots.ts +3 -2
  32. package/src/astro/generate.ts +97 -30
  33. package/src/astro/templates.ts +140 -53
  34. package/src/blume-modules.d.ts +6 -0
  35. package/src/components/content/Callout.astro +8 -2
  36. package/src/components/content/Prompt.astro +25 -13
  37. package/src/components/layout/Header.astro +19 -10
  38. package/src/components/layout/Logo.astro +13 -1
  39. package/src/components/layout/PageFeedback.astro +1 -1
  40. package/src/components/layout/PageLayout.astro +11 -11
  41. package/src/components/layout/Pagination.astro +6 -6
  42. package/src/components/layout/ReferenceLayout.astro +1 -0
  43. package/src/components/layout/RootLayout.astro +10 -11
  44. package/src/components/layout/Search.astro +1 -1
  45. package/src/components/layout/nav-utils.ts +9 -7
  46. package/src/core/config-input.ts +47 -27
  47. package/src/core/config.ts +3 -3
  48. package/src/core/data.ts +9 -1
  49. package/src/core/navigation.ts +55 -13
  50. package/src/core/schema.ts +32 -15
  51. package/src/core/server-features.ts +1 -1
  52. package/src/core/sources/watch.ts +5 -0
  53. package/src/core/types.ts +10 -0
  54. package/src/deploy/adapter-output.ts +11 -1
  55. package/src/markdown/index.ts +2 -0
  56. package/src/markdown/language-icon.ts +2 -1
  57. package/src/markdown/table-wrap.ts +43 -0
  58. package/src/og/card.ts +39 -12
  59. package/src/og/index.ts +1 -1
  60. package/src/og/logo.ts +21 -0
  61. package/src/openapi/references.ts +19 -16
  62. package/src/registry/eject.ts +11 -5
  63. package/src/theme/entry.ts +50 -5
@@ -62,10 +62,10 @@ export const buildAgentReadability = (
62
62
  artifacts.llmsFullTxt = abs("/llms-full.txt");
63
63
  artifacts.llmsTxt = abs("/llms.txt");
64
64
  }
65
- if (config.mcp.enabled) {
65
+ if (config.ai.mcp.enabled) {
66
66
  artifacts.mcp = {
67
67
  discovery: abs("/.well-known/mcp.json"),
68
- url: abs(config.mcp.route),
68
+ url: abs(config.ai.mcp.route),
69
69
  };
70
70
  }
71
71
  if (config.ai.ask?.enabled) {
@@ -87,7 +87,7 @@ export const buildAgentReadability = (
87
87
  artifacts,
88
88
  description: config.description,
89
89
  generator: version ? `blume@${version}` : undefined,
90
- name: config.mcp.name ?? config.title,
90
+ name: config.ai.mcp.name ?? config.title,
91
91
  site,
92
92
  };
93
93
 
@@ -88,8 +88,8 @@ export const buildMcpData = async (project: BlumeProject): Promise<McpData> => {
88
88
  route: doc.route,
89
89
  title: doc.title,
90
90
  })),
91
- instructions: config.mcp.instructions,
92
- name: config.mcp.name ?? config.title,
91
+ instructions: config.ai.mcp.instructions,
92
+ name: config.ai.mcp.name ?? config.title,
93
93
  navigation: graph.navigation,
94
94
  pages,
95
95
  routes,
@@ -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
@@ -37,7 +37,8 @@ import { resolveDocsCollection } from "../core/sources/resolve.ts";
37
37
  import { resolveTsconfigAliases } from "../core/tsconfig-aliases.ts";
38
38
  import type { Navigation } from "../core/types.ts";
39
39
  import { buildRssFeeds, renderRssFeed } from "../deploy/rss.ts";
40
- import { hasScalarReferences, referenceTabs } from "../openapi/references.ts";
40
+ import { resolveOgLogo } from "../og/logo.ts";
41
+ import { hasScalarReferences, referenceRoutes } from "../openapi/references.ts";
41
42
  import { buildReferenceFiles } from "../openapi/scalar.ts";
42
43
  import { isOpenApiSource } from "../openapi/source.ts";
43
44
  import { registry } from "../registry/registry.ts";
@@ -61,6 +62,7 @@ import {
61
62
  routeIsTaken,
62
63
  } from "./pages.ts";
63
64
  import {
65
+ askComponentTemplate,
64
66
  askEndpointTemplate,
65
67
  astroConfigTemplate,
66
68
  catchAllPageTemplate,
@@ -625,7 +627,56 @@ const writeStagedContent = async (
625
627
  }
626
628
  };
627
629
 
628
- /** The logo shape the runtime consumes: an inline SVG or image URL(s). */
630
+ interface LogoDimensions {
631
+ height: number;
632
+ width: number;
633
+ }
634
+
635
+ const SVG_ROOT = /<svg\b(?<attributes>[^>]*)>/u;
636
+ const SVG_WIDTH = /\bwidth\s*=\s*["'](?<value>[^"']+)["']/u;
637
+ const SVG_HEIGHT = /\bheight\s*=\s*["'](?<value>[^"']+)["']/u;
638
+ const SVG_LENGTH = /^\s*(?<value>[\d.]+)(?:px)?\s*$/u;
639
+ const SVG_VIEW_BOX =
640
+ /\bviewBox\s*=\s*["'][\d.-]+[\s,]+[\d.-]+[\s,]+(?<width>[\d.]+)[\s,]+(?<height>[\d.]+)["']/u;
641
+
642
+ const parseSvgLength = (value: string | undefined): number | undefined => {
643
+ const length = Number(value?.match(SVG_LENGTH)?.groups?.value);
644
+ return length > 0 ? length : undefined;
645
+ };
646
+
647
+ /** Read dimensions from an SVG's explicit size or its view box. */
648
+ const svgDimensions = (svg: string | undefined): LogoDimensions | undefined => {
649
+ const attributes = svg?.match(SVG_ROOT)?.groups?.attributes;
650
+ const width = parseSvgLength(attributes?.match(SVG_WIDTH)?.groups?.value);
651
+ const height = parseSvgLength(attributes?.match(SVG_HEIGHT)?.groups?.value);
652
+ if (width && height) {
653
+ return { height, width };
654
+ }
655
+
656
+ const viewBox = attributes?.match(SVG_VIEW_BOX);
657
+ const viewBoxWidth = Number(viewBox?.groups?.width);
658
+ const viewBoxHeight = Number(viewBox?.groups?.height);
659
+ return viewBoxWidth > 0 && viewBoxHeight > 0
660
+ ? { height: viewBoxHeight, width: viewBoxWidth }
661
+ : undefined;
662
+ };
663
+
664
+ /** Read a local SVG logo from the project root or public directory. */
665
+ const readLogoSvg = (
666
+ project: BlumeProject,
667
+ source: string | undefined
668
+ ): string | undefined => {
669
+ if (!source?.toLowerCase().endsWith(".svg")) {
670
+ return;
671
+ }
672
+ const rel = source.replace(/^\//u, "");
673
+ const file = [
674
+ join(project.context.root, "public", rel),
675
+ join(project.context.root, rel),
676
+ ].find((path) => existsSync(path));
677
+ return file ? readFileSync(file, "utf-8") : undefined;
678
+ };
679
+
629
680
  /**
630
681
  * Resolve the configured logo. A single SVG is read and inlined so a
631
682
  * `currentColor` logo follows the theme; other images keep their URL for an
@@ -646,18 +697,20 @@ const resolveLogo = (project: BlumeProject): BlumeLogo | null => {
646
697
  const dark = image?.dark ?? image?.light;
647
698
  const alt = image?.alt ?? "";
648
699
  const brandHref = href ?? "/";
700
+ const lightSvg = readLogoSvg(project, light);
701
+ const darkSvg = dark === light ? lightSvg : readLogoSvg(project, dark);
649
702
 
650
- if (light && light === dark && light.toLowerCase().endsWith(".svg")) {
651
- const rel = light.replace(/^\//u, "");
652
- const file = [
653
- join(project.context.root, "public", rel),
654
- join(project.context.root, rel),
655
- ].find((path) => existsSync(path));
656
- if (file) {
657
- return { alt, href: brandHref, svg: readFileSync(file, "utf-8"), text };
658
- }
703
+ if (light && light === dark && lightSvg) {
704
+ return { alt, href: brandHref, svg: lightSvg, text };
659
705
  }
660
- return { alt, dark, href: brandHref, light, text };
706
+
707
+ const lightDimensions = svgDimensions(lightSvg);
708
+ const darkDimensions = svgDimensions(darkSvg);
709
+ const dimensions =
710
+ lightDimensions || darkDimensions
711
+ ? { dark: darkDimensions, light: lightDimensions }
712
+ : undefined;
713
+ return { alt, dark, dimensions, href: brandHref, light, text };
661
714
  };
662
715
 
663
716
  /**
@@ -778,6 +831,10 @@ export const buildRuntimeData = (project: BlumeProject): string => {
778
831
  ? `https://github.com/${github.owner}/${github.repo}`
779
832
  : null;
780
833
  const editBase = github ? `${repoUrl}/edit/${github.branch}` : null;
834
+ const logo = resolveLogo(project);
835
+ const ogLogo = config.seo.og.logo
836
+ ? resolveOgLogo(project, config.seo.og.logo)
837
+ : logo?.svg;
781
838
 
782
839
  const editUrlFor = (sourcePath?: string): string | null => {
783
840
  if (!(editBase && sourcePath)) {
@@ -790,13 +847,12 @@ export const buildRuntimeData = (project: BlumeProject): string => {
790
847
 
791
848
  const { i18n } = config;
792
849
 
793
- // API reference routes surface as header tabs alongside the content-derived
794
- // ones (Blume-rendered references also own a tab-scoped sidebar of operations),
795
- // so the reference stays discoverable in every locale.
796
- const withReferenceTabs = (nav: Navigation): Navigation => ({
850
+ // Resolve the header repo link per locale. API references no longer add a tab
851
+ // automatically authors point a `navigation.tabs` entry at the reference
852
+ // route to surface it (see `referenceRoutes`).
853
+ const withRepoUrl = (nav: Navigation): Navigation => ({
797
854
  ...nav,
798
855
  repoUrl: config.navigation.repo && repoUrl ? repoUrl : null,
799
- tabs: [...nav.tabs, ...referenceTabs(config)],
800
856
  });
801
857
 
802
858
  // Resolved UI dictionaries: one per locale under i18n, English baseline
@@ -823,7 +879,7 @@ export const buildRuntimeData = (project: BlumeProject): string => {
823
879
  ? Object.fromEntries(
824
880
  i18n.locales.map(({ code }) => [
825
881
  code,
826
- withReferenceTabs(
882
+ withRepoUrl(
827
883
  graph.navigationByLocale[code] ?? {
828
884
  featured: [],
829
885
  selectors: [],
@@ -864,13 +920,20 @@ export const buildRuntimeData = (project: BlumeProject): string => {
864
920
  }
865
921
  : null,
866
922
  imageZoom: config.markdown.imageZoom,
867
- logo: resolveLogo(project),
868
- mcp: config.mcp.enabled
869
- ? { name: config.mcp.name ?? config.title, route: config.mcp.route }
923
+ logo,
924
+ mcp: config.ai.mcp.enabled
925
+ ? {
926
+ name: config.ai.mcp.name ?? config.title,
927
+ route: config.ai.mcp.route,
928
+ }
870
929
  : null,
871
930
  // `og.enabled` is resolved to a definite boolean in `loadConfig`; coerce
872
931
  // the optional schema type so the serialized shape stays `boolean`.
873
- og: { enabled: config.seo.og.enabled ?? false },
932
+ og: {
933
+ enabled: config.seo.og.enabled ?? false,
934
+ logo: ogLogo,
935
+ palette: config.seo.og.palette,
936
+ },
874
937
  repoUrl,
875
938
  search: {
876
939
  enabled: config.search.provider !== "none",
@@ -890,7 +953,7 @@ export const buildRuntimeData = (project: BlumeProject): string => {
890
953
  // CSS variables for Astro's <Font> component; matches the astro.config
891
954
  // `fonts:` entries derived from the same theme.fonts config.
892
955
  fontCssVars: configuredCssVars(config.theme.fonts),
893
- navigation: withReferenceTabs(graph.navigation),
956
+ navigation: withRepoUrl(graph.navigation),
894
957
  // Per-locale navigation; the catch-all selects the active locale's tree.
895
958
  navigationByLocale,
896
959
  routes: manifest.routes.map((route) => ({
@@ -939,7 +1002,7 @@ const planMcp = (
939
1002
  userPages: { pattern: string }[]
940
1003
  ): McpPlan => {
941
1004
  const { config } = project;
942
- const { route } = config.mcp;
1005
+ const { route } = config.ai.mcp;
943
1006
  const dir = join(srcDir, "blume-mcp");
944
1007
  const base: McpPlan = {
945
1008
  dir,
@@ -949,14 +1012,14 @@ const planMcp = (
949
1012
  srcDir,
950
1013
  warnings: [],
951
1014
  };
952
- if (!config.mcp.enabled) {
1015
+ if (!config.ai.mcp.enabled) {
953
1016
  return base;
954
1017
  }
955
1018
  if (routeIsTaken(userPages, project.graph.pages, route)) {
956
1019
  return {
957
1020
  ...base,
958
1021
  warnings: [
959
- `MCP server route "${route}" is already used by a content or custom page; the MCP server was not generated. Set a different "mcp.route" in blume.config.ts.`,
1022
+ `MCP server route "${route}" is already used by a content or custom page; the MCP server was not generated. Set a different "ai.mcp.route" in blume.config.ts.`,
960
1023
  ],
961
1024
  };
962
1025
  }
@@ -1105,6 +1168,7 @@ export const generateRuntime = async (
1105
1168
  const { context, config } = project;
1106
1169
  const out = context.outDir;
1107
1170
  const srcDir = join(out, "src");
1171
+ const askPath = join(srcDir, "generated", "Ask.astro");
1108
1172
  const dataPath = join(srcDir, "generated", "data.json");
1109
1173
  const themePath = join(srcDir, "generated", "app.css");
1110
1174
  const searchClientPath = join(srcDir, "generated", "search-client.ts");
@@ -1205,6 +1269,7 @@ export const generateRuntime = async (
1205
1269
  join(out, "astro.config.mjs"),
1206
1270
  astroConfigTemplate({
1207
1271
  aliases: resolveTsconfigAliases(context.root),
1272
+ askPath,
1208
1273
  config,
1209
1274
  contentRoutes: project.manifest.routes.map((route) => route.path),
1210
1275
  context,
@@ -1242,13 +1307,16 @@ export const generateRuntime = async (
1242
1307
  write(
1243
1308
  join(srcDir, "pages", "[...slug].astro"),
1244
1309
  catchAllPageTemplate({
1245
- askEnabled,
1246
1310
  exportEpub,
1247
1311
  exportPdf,
1248
1312
  mathEnabled: usesMath,
1249
1313
  needsReact,
1250
1314
  })
1251
1315
  ),
1316
+ // The header's Ask trigger, behind the `blume:ask` alias. Always written
1317
+ // (even when Ask is off, as a component that renders nothing) so the alias
1318
+ // resolves — the same contract as `blume:search-client`.
1319
+ write(askPath, askComponentTemplate(askEnabled)),
1252
1320
  write(join(srcDir, "generated", "components.ts"), slotPlan.module),
1253
1321
  write(
1254
1322
  join(srcDir, "generated", "islands.ts"),
@@ -1336,7 +1404,6 @@ export const generateRuntime = async (
1336
1404
  await write(
1337
1405
  join(srcDir, "pages", "changelog.astro"),
1338
1406
  changelogIndexTemplate({
1339
- askEnabled,
1340
1407
  exportEpub,
1341
1408
  exportPdf,
1342
1409
  needsReact,
@@ -1433,11 +1500,11 @@ export const generateRuntime = async (
1433
1500
 
1434
1501
  // Missing-navigation-target check, now that every servable route is known:
1435
1502
  // content routes, custom `.astro` pages, the generated changelog, and any
1436
- // OpenAPI reference tabs.
1503
+ // OpenAPI reference routes (so a tab an author points at one still validates).
1437
1504
  const navTargetRoutes = new Set<string>([
1438
1505
  ...project.graph.routes.keys(),
1439
1506
  ...pages.map((page) => page.pattern),
1440
- ...referenceTabs(config).map((tab) => tab.path),
1507
+ ...referenceRoutes(config),
1441
1508
  ]);
1442
1509
  if (hasGeneratedChangelog(project, pages)) {
1443
1510
  navTargetRoutes.add("/changelog");