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.
- package/CHANGELOG.md +37 -0
- package/dist/cli/index.js +496 -295
- package/dist/cli/index.js.map +18 -17
- package/dist/types/core/config-input.d.ts +44 -22
- package/dist/types/core/config.d.ts +3 -3
- package/dist/types/core/data.d.ts +12 -0
- package/dist/types/core/i18n-ui.d.ts +136 -136
- package/dist/types/core/schema.d.ts +502 -384
- package/dist/types/core/types.d.ts +10 -0
- package/dist/types/openapi/references.d.ts +12 -7
- package/docs/advanced/api-reference.mdx +11 -3
- package/docs/advanced/custom-pages.mdx +2 -0
- package/docs/configuration/ai.mdx +6 -4
- package/docs/configuration/index.mdx +6 -8
- package/docs/configuration/seo.mdx +20 -1
- package/docs/content/components.mdx +26 -5
- package/docs/content/navigation.mdx +10 -0
- package/docs/content/syntax.mdx +116 -4
- package/package.json +1 -1
- package/skills/blume-migrate/SKILL.md +170 -0
- package/skills/blume-migrate/assets/oxfmt@0.55.0.patch +20 -0
- package/skills/blume-migrate/references/docusaurus.md +95 -0
- package/skills/blume-migrate/references/fumadocs.md +95 -0
- package/skills/blume-migrate/references/mintlify.md +155 -0
- package/skills/blume-migrate/references/monorepo.md +224 -0
- package/skills/blume-migrate/references/nextra.md +76 -0
- package/skills/blume-migrate/references/starlight.md +116 -0
- package/skills/blume-migrate/scripts/mintlify-codemod.mjs +466 -0
- package/src/ai/agent-readability.ts +3 -3
- package/src/ai/mcp/data.ts +2 -2
- package/src/astro/component-slots.ts +3 -2
- package/src/astro/generate.ts +97 -30
- package/src/astro/templates.ts +140 -53
- package/src/blume-modules.d.ts +6 -0
- package/src/components/content/Callout.astro +8 -2
- package/src/components/content/Prompt.astro +25 -13
- package/src/components/layout/Header.astro +19 -10
- package/src/components/layout/Logo.astro +13 -1
- package/src/components/layout/PageFeedback.astro +1 -1
- package/src/components/layout/PageLayout.astro +11 -11
- package/src/components/layout/Pagination.astro +6 -6
- package/src/components/layout/ReferenceLayout.astro +1 -0
- package/src/components/layout/RootLayout.astro +10 -11
- package/src/components/layout/Search.astro +1 -1
- package/src/components/layout/nav-utils.ts +9 -7
- package/src/core/config-input.ts +47 -27
- package/src/core/config.ts +3 -3
- package/src/core/data.ts +9 -1
- package/src/core/navigation.ts +55 -13
- package/src/core/schema.ts +32 -15
- package/src/core/server-features.ts +1 -1
- package/src/core/sources/watch.ts +5 -0
- package/src/core/types.ts +10 -0
- package/src/deploy/adapter-output.ts +11 -1
- package/src/markdown/index.ts +2 -0
- package/src/markdown/language-icon.ts +2 -1
- package/src/markdown/table-wrap.ts +43 -0
- package/src/og/card.ts +39 -12
- package/src/og/index.ts +1 -1
- package/src/og/logo.ts +21 -0
- package/src/openapi/references.ts +19 -16
- package/src/registry/eject.ts +11 -5
- 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
|
|
package/src/ai/mcp/data.ts
CHANGED
|
@@ -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
|
-
|
|
35
|
-
export const
|
|
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
|
package/src/astro/generate.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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 &&
|
|
651
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
794
|
-
//
|
|
795
|
-
//
|
|
796
|
-
const
|
|
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
|
-
|
|
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
|
|
868
|
-
mcp: config.mcp.enabled
|
|
869
|
-
? {
|
|
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: {
|
|
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:
|
|
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
|
|
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
|
-
...
|
|
1507
|
+
...referenceRoutes(config),
|
|
1441
1508
|
]);
|
|
1442
1509
|
if (hasGeneratedChangelog(project, pages)) {
|
|
1443
1510
|
navTargetRoutes.add("/changelog");
|