blume 1.6.0 → 1.6.2
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 +21 -0
- package/dist/cli/index.js +1318 -270
- package/dist/cli/index.js.map +68 -61
- package/dist/types/core/config-input.d.ts +9 -0
- package/dist/types/core/data.d.ts +12 -1
- package/dist/types/core/i18n-ui.d.ts +4 -0
- package/dist/types/core/schema.d.ts +7 -0
- package/dist/types/core/types.d.ts +6 -0
- package/dist/types/openapi/references.d.ts +5 -0
- package/docs/07-faq.mdx +9 -9
- package/docs/advanced/api-reference.mdx +10 -1
- package/docs/advanced/custom-pages.mdx +3 -1
- package/docs/advanced/graphql.mdx +1 -1
- package/docs/configuration/ai.mdx +76 -7
- package/docs/configuration/seo.mdx +3 -3
- package/docs/configuration/theming.mdx +6 -0
- package/docs/content/components.mdx +8 -1
- package/docs/index.mdx +2 -2
- package/package.json +53 -53
- package/skills/blume/SKILL.md +2 -2
- package/src/ai/agent-readability.ts +60 -17
- package/src/ai/api/handlers.ts +273 -0
- package/src/ai/api/paths.ts +14 -0
- package/src/ai/api/problem.ts +63 -0
- package/src/ai/api/spec.ts +681 -0
- package/src/ai/api-catalog.ts +11 -1
- package/src/ai/link-headers.ts +12 -3
- package/src/ai/llms.ts +9 -2
- package/src/ai/mcp/query.ts +390 -0
- package/src/ai/mcp/server.ts +32 -352
- package/src/astro/examples.ts +29 -2
- package/src/astro/generate.ts +256 -64
- package/src/astro/index.ts +7 -0
- package/src/astro/markdown-negotiation.ts +1 -1
- package/src/astro/runtime-modules.ts +196 -0
- package/src/astro/templates.ts +398 -38
- package/src/cli/commands/build.ts +9 -1
- package/src/cli/commands/dev.ts +6 -3
- package/src/cli/host-args.ts +18 -0
- package/src/cli/index.ts +2 -1
- package/src/components/copy-feedback.ts +93 -9
- package/src/components/islands/ask-ai.tsx +4 -1
- package/src/components/islands/hooks.ts +3 -1
- package/src/components/layout/PageActions.astro +25 -14
- package/src/core/config-input.ts +9 -0
- package/src/core/data.ts +17 -2
- package/src/core/define-components.ts +2 -0
- package/src/core/i18n-ui.ts +3 -0
- package/src/core/includes.ts +2 -1
- package/src/core/manifest.ts +10 -0
- package/src/core/schema.ts +20 -5
- package/src/core/types.ts +6 -0
- package/src/core/ui-packs/ar.ts +1 -0
- package/src/core/ui-packs/bg.ts +1 -0
- package/src/core/ui-packs/bn.ts +1 -0
- package/src/core/ui-packs/ca.ts +1 -0
- package/src/core/ui-packs/cs.ts +1 -0
- package/src/core/ui-packs/da.ts +1 -0
- package/src/core/ui-packs/de.ts +1 -0
- package/src/core/ui-packs/el.ts +1 -0
- package/src/core/ui-packs/es.ts +1 -0
- package/src/core/ui-packs/fa.ts +1 -0
- package/src/core/ui-packs/fi.ts +1 -0
- package/src/core/ui-packs/fr.ts +1 -0
- package/src/core/ui-packs/he.ts +1 -0
- package/src/core/ui-packs/hi.ts +1 -0
- package/src/core/ui-packs/hr.ts +1 -0
- package/src/core/ui-packs/hu.ts +1 -0
- package/src/core/ui-packs/id.ts +1 -0
- package/src/core/ui-packs/it.ts +1 -0
- package/src/core/ui-packs/ja.ts +1 -0
- package/src/core/ui-packs/ko.ts +1 -0
- package/src/core/ui-packs/nl.ts +1 -0
- package/src/core/ui-packs/no.ts +1 -0
- package/src/core/ui-packs/pl.ts +1 -0
- package/src/core/ui-packs/pt-br.ts +1 -0
- package/src/core/ui-packs/pt.ts +1 -0
- package/src/core/ui-packs/ro.ts +1 -0
- package/src/core/ui-packs/ru.ts +1 -0
- package/src/core/ui-packs/sk.ts +1 -0
- package/src/core/ui-packs/sr.ts +1 -0
- package/src/core/ui-packs/sv.ts +1 -0
- package/src/core/ui-packs/th.ts +1 -0
- package/src/core/ui-packs/tr.ts +1 -0
- package/src/core/ui-packs/uk.ts +1 -0
- package/src/core/ui-packs/vi.ts +1 -0
- package/src/core/ui-packs/zh-tw.ts +1 -0
- package/src/core/ui-packs/zh.ts +1 -0
- package/src/core/version-cut.ts +5 -3
- package/src/deploy/vercel-negotiation.ts +97 -6
- package/src/og/card.ts +1 -1
- package/src/openapi/references.ts +8 -0
- package/src/openapi/render-mdx.ts +18 -4
- package/src/openapi/scalar.ts +0 -4
- package/src/registry/eject.ts +36 -17
- package/src/theme/entry.ts +2 -2
- package/src/theme/sources.ts +49 -0
package/src/astro/generate.ts
CHANGED
|
@@ -25,10 +25,13 @@ import {
|
|
|
25
25
|
} from "pathe";
|
|
26
26
|
import { glob } from "tinyglobby";
|
|
27
27
|
|
|
28
|
+
import { OPENAPI_PATH } from "../ai/api/paths.ts";
|
|
29
|
+
import { buildApiSpec } from "../ai/api/spec.ts";
|
|
28
30
|
import { buildAskData } from "../ai/ask-data.ts";
|
|
29
31
|
import { askBackendRuntimeDep, resolveAskBackend } from "../ai/ask.ts";
|
|
30
32
|
import { buildRawMarkdown, markdownRoutePaths } from "../ai/markdown.ts";
|
|
31
33
|
import { buildMcpData } from "../ai/mcp/data.ts";
|
|
34
|
+
import type { McpData } from "../ai/mcp/data.ts";
|
|
32
35
|
import { buildMcpDiscovery, buildMcpServerCard } from "../ai/mcp/discovery.ts";
|
|
33
36
|
import { normalizeBasePath } from "../core/base-path.ts";
|
|
34
37
|
import { validateUsedComponents } from "../core/component-diagnostics.ts";
|
|
@@ -91,10 +94,16 @@ import {
|
|
|
91
94
|
fontLocaleCodes,
|
|
92
95
|
} from "../theme/fonts.ts";
|
|
93
96
|
import { buildThemeCss } from "../theme/palette.ts";
|
|
97
|
+
import { rebaseSourceDirectives } from "../theme/sources.ts";
|
|
94
98
|
import { twoslashCss } from "../theme/twoslash.ts";
|
|
95
99
|
import { planComponentSlots } from "./component-slots.ts";
|
|
96
100
|
import type { ComponentSlotPlan } from "./component-slots.ts";
|
|
97
|
-
import {
|
|
101
|
+
import {
|
|
102
|
+
EXAMPLE_SCAN_GLOB,
|
|
103
|
+
discoverExamples,
|
|
104
|
+
exampleMarkdownLookup,
|
|
105
|
+
exampleScanRoots,
|
|
106
|
+
} from "./examples.ts";
|
|
98
107
|
import { discoverIslands } from "./islands.ts";
|
|
99
108
|
import {
|
|
100
109
|
customOgRoutes,
|
|
@@ -102,6 +111,8 @@ import {
|
|
|
102
111
|
hasGeneratedChangelog,
|
|
103
112
|
routeIsTaken,
|
|
104
113
|
} from "./pages.ts";
|
|
114
|
+
import { publishRuntimeModules } from "./runtime-modules.ts";
|
|
115
|
+
import type { RuntimeModuleId } from "./runtime-modules.ts";
|
|
105
116
|
import {
|
|
106
117
|
askComponentTemplate,
|
|
107
118
|
askEndpointTemplate,
|
|
@@ -117,9 +128,16 @@ import {
|
|
|
117
128
|
exampleSlug,
|
|
118
129
|
islandMapTemplate,
|
|
119
130
|
islandWrapperTemplate,
|
|
131
|
+
apiNavigationTemplate,
|
|
132
|
+
apiNotFoundTemplate,
|
|
133
|
+
apiPageTemplate,
|
|
134
|
+
apiPagesIndexTemplate,
|
|
135
|
+
apiSearchTemplate,
|
|
120
136
|
mcpEndpointTemplate,
|
|
121
137
|
mcpPageFile,
|
|
122
138
|
mixedbreadSearchEndpointTemplate,
|
|
139
|
+
notFoundJsonTemplate,
|
|
140
|
+
notFoundMarkdownTemplate,
|
|
123
141
|
notFoundPageTemplate,
|
|
124
142
|
ogEndpointTemplate,
|
|
125
143
|
playgroundProxyTemplate,
|
|
@@ -714,6 +732,21 @@ const readOptional = async (path: string | null): Promise<string> => {
|
|
|
714
732
|
}
|
|
715
733
|
};
|
|
716
734
|
|
|
735
|
+
/**
|
|
736
|
+
* Read a user stylesheet (`theme.css`, `examples.css`) that gets inlined into
|
|
737
|
+
* a generated Tailwind entry under `outputDir`, re-rooting its relative
|
|
738
|
+
* `@source` paths so they still resolve from where the author wrote them.
|
|
739
|
+
*/
|
|
740
|
+
const readUserCss = async (
|
|
741
|
+
file: string | null,
|
|
742
|
+
outputDir: string
|
|
743
|
+
): Promise<string> => {
|
|
744
|
+
const css = await readOptional(file);
|
|
745
|
+
return file
|
|
746
|
+
? rebaseSourceDirectives(css, { from: file, to: outputDir })
|
|
747
|
+
: css;
|
|
748
|
+
};
|
|
749
|
+
|
|
717
750
|
/** Heuristically detect whether the project uses React islands. */
|
|
718
751
|
export const detectNeedsReact = async (root: string): Promise<boolean> => {
|
|
719
752
|
const matches = await glob(["**/*.{tsx,jsx}"], {
|
|
@@ -1169,7 +1202,11 @@ const resolveOgSite = (config: ResolvedConfig): string | undefined => {
|
|
|
1169
1202
|
: undefined;
|
|
1170
1203
|
};
|
|
1171
1204
|
|
|
1172
|
-
/**
|
|
1205
|
+
/**
|
|
1206
|
+
* The OG card's site-wide subtitle: `seo.og.description` (`false` omits it)
|
|
1207
|
+
* over the site description. The generated endpoint prefers a page's own
|
|
1208
|
+
* description and falls back to this.
|
|
1209
|
+
*/
|
|
1173
1210
|
const resolveOgDescription = (config: ResolvedConfig): string | undefined => {
|
|
1174
1211
|
const configured = config.seo.og.description;
|
|
1175
1212
|
if (configured === false) {
|
|
@@ -1291,6 +1328,7 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
1291
1328
|
description: config.description,
|
|
1292
1329
|
discovery: {
|
|
1293
1330
|
agentReadability: config.seo.agentReadability,
|
|
1331
|
+
api: config.ai.api,
|
|
1294
1332
|
llmsTxt: config.ai.llmsTxt.enabled,
|
|
1295
1333
|
// Mirrors `buildSitemapFiles`: no site, no sitemap.
|
|
1296
1334
|
sitemap: config.seo.sitemap && Boolean(config.deployment.site),
|
|
@@ -1390,6 +1428,7 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
1390
1428
|
routes: manifest.routes.map((route) => ({
|
|
1391
1429
|
alternates: route.alternates,
|
|
1392
1430
|
collection: route.collection,
|
|
1431
|
+
description: route.description ?? null,
|
|
1393
1432
|
draft: route.draft,
|
|
1394
1433
|
editUrl: route.editUrl ?? editUrlFor(route.sourcePath),
|
|
1395
1434
|
entryId: route.entryId,
|
|
@@ -1472,16 +1511,36 @@ const planMcp = (
|
|
|
1472
1511
|
};
|
|
1473
1512
|
};
|
|
1474
1513
|
|
|
1475
|
-
/**
|
|
1476
|
-
|
|
1514
|
+
/** A pass's runtime data modules, published together once every writer ran. */
|
|
1515
|
+
type RuntimeModules = Map<RuntimeModuleId, string>;
|
|
1516
|
+
|
|
1517
|
+
/**
|
|
1518
|
+
* The agent data snapshot (`blume:mcp-data`) behind the MCP server and the
|
|
1519
|
+
* JSON docs API: built once per pass when either is on, published to the
|
|
1520
|
+
* runtime modules, and handed to both writers. Null when neither needs it.
|
|
1521
|
+
*/
|
|
1522
|
+
const publishAgentData = async (
|
|
1477
1523
|
project: BlumeProject,
|
|
1524
|
+
plans: { api: ApiPlan; mcp: McpPlan },
|
|
1525
|
+
modules: RuntimeModules
|
|
1526
|
+
): Promise<McpData | null> => {
|
|
1527
|
+
if (!(plans.mcp.enabled || plans.api.enabled)) {
|
|
1528
|
+
return null;
|
|
1529
|
+
}
|
|
1530
|
+
const data = await buildMcpData(project);
|
|
1531
|
+
modules.set("blume:mcp-data", JSON.stringify(data));
|
|
1532
|
+
return data;
|
|
1533
|
+
};
|
|
1534
|
+
|
|
1535
|
+
/** Write the MCP server endpoint and discovery documents. */
|
|
1536
|
+
const writeMcpFiles = async (
|
|
1478
1537
|
plan: McpPlan,
|
|
1479
|
-
write: (path: string, content: string) => Promise<boolean
|
|
1538
|
+
write: (path: string, content: string) => Promise<boolean>,
|
|
1539
|
+
data: McpData | null
|
|
1480
1540
|
): Promise<void> => {
|
|
1481
|
-
if (!plan.enabled) {
|
|
1541
|
+
if (!(plan.enabled && data)) {
|
|
1482
1542
|
return;
|
|
1483
1543
|
}
|
|
1484
|
-
const data = await buildMcpData(project);
|
|
1485
1544
|
const discoveryInput = {
|
|
1486
1545
|
base: data.base,
|
|
1487
1546
|
name: data.name,
|
|
@@ -1490,13 +1549,9 @@ const writeMcpFiles = async (
|
|
|
1490
1549
|
version: data.version,
|
|
1491
1550
|
};
|
|
1492
1551
|
await Promise.all([
|
|
1493
|
-
write(
|
|
1494
|
-
join(plan.srcDir, "generated", "mcp-data.json"),
|
|
1495
|
-
`${JSON.stringify(data)}\n`
|
|
1496
|
-
),
|
|
1497
1552
|
write(
|
|
1498
1553
|
join(plan.srcDir, "pages", mcpPageFile(plan.route)),
|
|
1499
|
-
mcpEndpointTemplate(
|
|
1554
|
+
mcpEndpointTemplate()
|
|
1500
1555
|
),
|
|
1501
1556
|
write(
|
|
1502
1557
|
join(plan.dir, "discovery.ts"),
|
|
@@ -1509,6 +1564,129 @@ const writeMcpFiles = async (
|
|
|
1509
1564
|
]);
|
|
1510
1565
|
};
|
|
1511
1566
|
|
|
1567
|
+
/** The resolved plan for the JSON docs API within a single generate pass. */
|
|
1568
|
+
interface ApiPlan {
|
|
1569
|
+
/**
|
|
1570
|
+
* Whether the `/api/` catch-all (JSON 404s) is written: server output, no
|
|
1571
|
+
* user page owning a rest route under `/api/`, and no content page served
|
|
1572
|
+
* from the `/api` namespace (the catch-all would outrank those pages).
|
|
1573
|
+
*/
|
|
1574
|
+
catchAll: boolean;
|
|
1575
|
+
enabled: boolean;
|
|
1576
|
+
/** Whether the live endpoints (search) are written — server output only. */
|
|
1577
|
+
server: boolean;
|
|
1578
|
+
/**
|
|
1579
|
+
* Whether `/openapi.json` is generated: skipped when a `public/openapi.json`
|
|
1580
|
+
* or a user page owns the route, so a site can publish its own description.
|
|
1581
|
+
*/
|
|
1582
|
+
spec: boolean;
|
|
1583
|
+
srcDir: string;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
/** Whether a user page pattern is a rest route under `/api/` (`/api/[...x]`). */
|
|
1587
|
+
const ownsApiRest = (page: { pattern: string }): boolean =>
|
|
1588
|
+
page.pattern.startsWith("/api/[");
|
|
1589
|
+
|
|
1590
|
+
/**
|
|
1591
|
+
* Whether a content route lives in the `/api` namespace — a docs section
|
|
1592
|
+
* about an API commonly does (`content/api/overview.md` → `/api/overview`).
|
|
1593
|
+
* Astro ranks `/api/[...path]` above the content catch-all (`/[...slug]`),
|
|
1594
|
+
* so the JSON 404 route would shadow those pages wherever routing decides
|
|
1595
|
+
* (the dev server, adapters that don't serve prerendered files first).
|
|
1596
|
+
*/
|
|
1597
|
+
const contentUnderApi = (page: { route: string }): boolean =>
|
|
1598
|
+
page.route === "/api" || page.route.startsWith("/api/");
|
|
1599
|
+
|
|
1600
|
+
/**
|
|
1601
|
+
* Decide what the JSON docs API generates. The prerendered endpoints always
|
|
1602
|
+
* ride along when the feature is on (they live under Blume's own `/api/docs/`
|
|
1603
|
+
* namespace); the live ones need server output; the OpenAPI description yields
|
|
1604
|
+
* to one the project ships itself.
|
|
1605
|
+
*/
|
|
1606
|
+
const planApi = (
|
|
1607
|
+
project: BlumeProject,
|
|
1608
|
+
srcDir: string,
|
|
1609
|
+
userPages: { pattern: string }[]
|
|
1610
|
+
): ApiPlan => {
|
|
1611
|
+
const { config, context } = project;
|
|
1612
|
+
const server = config.deployment.output === "server";
|
|
1613
|
+
return {
|
|
1614
|
+
catchAll:
|
|
1615
|
+
server &&
|
|
1616
|
+
!userPages.some(ownsApiRest) &&
|
|
1617
|
+
!project.graph.pages.some(contentUnderApi),
|
|
1618
|
+
enabled: config.ai.api,
|
|
1619
|
+
server,
|
|
1620
|
+
spec:
|
|
1621
|
+
!routeIsTaken(userPages, project.graph.pages, OPENAPI_PATH) &&
|
|
1622
|
+
!existsSync(join(context.root, "public", "openapi.json")),
|
|
1623
|
+
srcDir,
|
|
1624
|
+
};
|
|
1625
|
+
};
|
|
1626
|
+
|
|
1627
|
+
/**
|
|
1628
|
+
* Write the JSON docs API: the prerendered page index, per-page documents, and
|
|
1629
|
+
* navigation; on server output the search endpoint and the `/api/` catch-all;
|
|
1630
|
+
* and the OpenAPI description of the whole agent-facing surface. The MCP
|
|
1631
|
+
* route reaches the description only when the server was actually planned (a
|
|
1632
|
+
* collision can disable it), so it never advertises an endpoint that isn't
|
|
1633
|
+
* there.
|
|
1634
|
+
*/
|
|
1635
|
+
const writeApiFiles = async (
|
|
1636
|
+
project: BlumeProject,
|
|
1637
|
+
plan: ApiPlan,
|
|
1638
|
+
write: (path: string, content: string) => Promise<boolean>,
|
|
1639
|
+
data: McpData | null,
|
|
1640
|
+
mcp: McpPlan
|
|
1641
|
+
): Promise<void> => {
|
|
1642
|
+
if (!(plan.enabled && data)) {
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
const { config } = project;
|
|
1646
|
+
const mcpRoute = mcp.enabled ? mcp.route : null;
|
|
1647
|
+
const apiDir = join(plan.srcDir, "pages", "api");
|
|
1648
|
+
const writes = [
|
|
1649
|
+
write(join(apiDir, "docs", "pages.json.ts"), apiPagesIndexTemplate()),
|
|
1650
|
+
write(
|
|
1651
|
+
join(apiDir, "docs", "pages", "[...route].json.ts"),
|
|
1652
|
+
apiPageTemplate()
|
|
1653
|
+
),
|
|
1654
|
+
write(join(apiDir, "docs", "navigation.json.ts"), apiNavigationTemplate()),
|
|
1655
|
+
];
|
|
1656
|
+
if (plan.server) {
|
|
1657
|
+
writes.push(write(join(apiDir, "docs", "search.ts"), apiSearchTemplate()));
|
|
1658
|
+
}
|
|
1659
|
+
if (plan.catchAll) {
|
|
1660
|
+
writes.push(
|
|
1661
|
+
write(
|
|
1662
|
+
join(apiDir, "[...path].ts"),
|
|
1663
|
+
apiNotFoundTemplate({ base: data.base, site: data.site })
|
|
1664
|
+
)
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
if (plan.spec) {
|
|
1668
|
+
writes.push(
|
|
1669
|
+
write(
|
|
1670
|
+
join(plan.srcDir, "pages", "openapi.json.ts"),
|
|
1671
|
+
staticJsonEndpointTemplate(
|
|
1672
|
+
buildApiSpec({
|
|
1673
|
+
agentReadability: config.seo.agentReadability,
|
|
1674
|
+
base: data.base,
|
|
1675
|
+
description: config.description,
|
|
1676
|
+
llmsTxt: config.ai.llmsTxt.enabled,
|
|
1677
|
+
mcpRoute,
|
|
1678
|
+
name: config.title,
|
|
1679
|
+
search: plan.server,
|
|
1680
|
+
site: data.site,
|
|
1681
|
+
version: data.version,
|
|
1682
|
+
})
|
|
1683
|
+
)
|
|
1684
|
+
)
|
|
1685
|
+
);
|
|
1686
|
+
}
|
|
1687
|
+
await Promise.all(writes);
|
|
1688
|
+
};
|
|
1689
|
+
|
|
1512
1690
|
/**
|
|
1513
1691
|
* Decide whether to generate the playground's built-in CORS proxy endpoint.
|
|
1514
1692
|
* Only the Blume renderer's playground with `proxy: true` needs it — a proxy
|
|
@@ -1605,7 +1783,8 @@ const proxyAllowlistWarnings = (
|
|
|
1605
1783
|
const writeAskFiles = async (
|
|
1606
1784
|
project: BlumeProject,
|
|
1607
1785
|
srcDir: string,
|
|
1608
|
-
write: (path: string, content: string) => Promise<boolean
|
|
1786
|
+
write: (path: string, content: string) => Promise<boolean>,
|
|
1787
|
+
modules: RuntimeModules
|
|
1609
1788
|
): Promise<void> => {
|
|
1610
1789
|
const { ask } = project.config.ai;
|
|
1611
1790
|
if (!(ask?.enabled && !ask.endpoint)) {
|
|
@@ -1613,10 +1792,7 @@ const writeAskFiles = async (
|
|
|
1613
1792
|
}
|
|
1614
1793
|
const grounded = ask.provider !== "inkeep";
|
|
1615
1794
|
if (grounded) {
|
|
1616
|
-
await
|
|
1617
|
-
join(srcDir, "generated", "ask-data.json"),
|
|
1618
|
-
`${JSON.stringify(await buildAskData(project))}\n`
|
|
1619
|
-
);
|
|
1795
|
+
modules.set("blume:ask-data", JSON.stringify(await buildAskData(project)));
|
|
1620
1796
|
}
|
|
1621
1797
|
await write(
|
|
1622
1798
|
join(srcDir, "pages", "api", "ask.ts"),
|
|
@@ -1629,10 +1805,13 @@ const writeAskFiles = async (
|
|
|
1629
1805
|
|
|
1630
1806
|
/**
|
|
1631
1807
|
* Write the default 404 page at Astro's reserved `src/pages/404.astro` path so
|
|
1632
|
-
* static builds emit `dist/404.html
|
|
1633
|
-
*
|
|
1634
|
-
*
|
|
1635
|
-
*
|
|
1808
|
+
* static builds emit `dist/404.html`, plus its Markdown twin at `404.md.ts`
|
|
1809
|
+
* (`dist/404.md`) for agents that ask a missing URL for Markdown and its JSON
|
|
1810
|
+
* twin at `404.json.ts` (`dist/404.json`, RFC 9457 problem details) for those
|
|
1811
|
+
* that ask for JSON. All three are skipped when the project already owns
|
|
1812
|
+
* `/404` (a custom `pages/404.astro` or a `404.md` content page), letting it
|
|
1813
|
+
* be fully overridden without a route collision; `pruneOrphans` then removes
|
|
1814
|
+
* any previously-generated copies.
|
|
1636
1815
|
*/
|
|
1637
1816
|
const writeNotFoundPage = async (
|
|
1638
1817
|
write: (path: string, content: string) => Promise<boolean>,
|
|
@@ -1643,7 +1822,11 @@ const writeNotFoundPage = async (
|
|
|
1643
1822
|
if (routeIsTaken(pages, contentPages, "/404")) {
|
|
1644
1823
|
return;
|
|
1645
1824
|
}
|
|
1646
|
-
await
|
|
1825
|
+
await Promise.all([
|
|
1826
|
+
write(join(srcDir, "pages", "404.astro"), notFoundPageTemplate()),
|
|
1827
|
+
write(join(srcDir, "pages", "404.md.ts"), notFoundMarkdownTemplate()),
|
|
1828
|
+
write(join(srcDir, "pages", "404.json.ts"), notFoundJsonTemplate()),
|
|
1829
|
+
]);
|
|
1647
1830
|
};
|
|
1648
1831
|
|
|
1649
1832
|
/**
|
|
@@ -1733,13 +1916,12 @@ export const generateRuntime = async (
|
|
|
1733
1916
|
assertFontFilesExist(project);
|
|
1734
1917
|
const out = context.outDir;
|
|
1735
1918
|
const srcDir = join(out, "src");
|
|
1919
|
+
const generatedDir = join(srcDir, "generated");
|
|
1736
1920
|
const askPath = join(srcDir, "generated", "Ask.astro");
|
|
1737
|
-
const dataPath = join(srcDir, "generated", "data.json");
|
|
1738
1921
|
const themePath = join(srcDir, "generated", "app.css");
|
|
1739
1922
|
const searchClientPath = join(srcDir, "generated", "search-client.ts");
|
|
1740
1923
|
const examplesPath = join(srcDir, "generated", "examples.ts");
|
|
1741
1924
|
const examplesThemePath = join(srcDir, "generated", "examples.css");
|
|
1742
|
-
const openapiPath = join(srcDir, "generated", "openapi.json");
|
|
1743
1925
|
|
|
1744
1926
|
// Record every file this pass writes so orphans (from a now-disabled feature)
|
|
1745
1927
|
// can be pruned afterwards. `write` wraps the atomic writer and tracks paths.
|
|
@@ -1748,6 +1930,11 @@ export const generateRuntime = async (
|
|
|
1748
1930
|
written.add(normalize(path));
|
|
1749
1931
|
return writeIfChanged(path, content);
|
|
1750
1932
|
};
|
|
1933
|
+
// The data snapshots the generated pages import (`blume:data`, the search
|
|
1934
|
+
// index, …) are collected here and published in memory at the end of the
|
|
1935
|
+
// pass — see `runtime-modules.ts`. An id a feature leaves unset is
|
|
1936
|
+
// unpublished, the in-memory counterpart of `pruneOrphans`.
|
|
1937
|
+
const modules: RuntimeModules = new Map();
|
|
1751
1938
|
|
|
1752
1939
|
const depsLinkWarning = await ensureDepsLink(out);
|
|
1753
1940
|
|
|
@@ -1776,8 +1963,8 @@ export const generateRuntime = async (
|
|
|
1776
1963
|
context.pagesRoot ? discoverPages(context.pagesRoot) : Promise.resolve([]),
|
|
1777
1964
|
detectNeedsReact(context.root),
|
|
1778
1965
|
detectUsesMath(context.root, staged.values()),
|
|
1779
|
-
|
|
1780
|
-
|
|
1966
|
+
readUserCss(context.themeFile, generatedDir),
|
|
1967
|
+
readUserCss(examplesCssFile(context.root, config), generatedDir),
|
|
1781
1968
|
loadIntegrationBridge(config, context),
|
|
1782
1969
|
discoverIslands(context.root),
|
|
1783
1970
|
discoverExamples(context.root, config.examples.source),
|
|
@@ -1827,6 +2014,11 @@ export const generateRuntime = async (
|
|
|
1827
2014
|
const mcp = planMcp(project, srcDir, pages);
|
|
1828
2015
|
pages.push(...mcp.discoveryPages);
|
|
1829
2016
|
|
|
2017
|
+
// The JSON docs API shares the MCP server's snapshot; build it once when
|
|
2018
|
+
// either is on.
|
|
2019
|
+
const api = planApi(project, srcDir, pages);
|
|
2020
|
+
const agentData = await publishAgentData(project, { api, mcp }, modules);
|
|
2021
|
+
|
|
1830
2022
|
// The parsed OpenAPI specs behind the `blume:openapi` alias, also the source
|
|
1831
2023
|
// of the proxy's origin allowlist below. The source parsed them during the
|
|
1832
2024
|
// scan, so reading them here is free.
|
|
@@ -1870,14 +2062,12 @@ export const generateRuntime = async (
|
|
|
1870
2062
|
contentRoot: docsCollection.base,
|
|
1871
2063
|
contentRoutes: markdownRoutePaths(project),
|
|
1872
2064
|
context,
|
|
1873
|
-
dataPath,
|
|
1874
2065
|
examplesPath,
|
|
1875
2066
|
examplesThemePath,
|
|
1876
2067
|
integrationBridge,
|
|
1877
2068
|
needsReact,
|
|
1878
2069
|
needsSvelte,
|
|
1879
2070
|
needsVue,
|
|
1880
|
-
openapiPath,
|
|
1881
2071
|
pages,
|
|
1882
2072
|
reactCompilerPath,
|
|
1883
2073
|
searchClientPath,
|
|
@@ -1925,13 +2115,16 @@ export const generateRuntime = async (
|
|
|
1925
2115
|
exampleMapTemplate(exampleDiscovery.examples, config.basePath)
|
|
1926
2116
|
),
|
|
1927
2117
|
// The isolated Tailwind entry for `<Component />` preview frames: only
|
|
1928
|
-
//
|
|
1929
|
-
//
|
|
2118
|
+
// the project (and an out-of-root examples directory) is scanned, so the
|
|
2119
|
+
// docs theme never reaches a preview. Anything further afield — a sibling
|
|
2120
|
+
// package the examples import — is the user's `@source` in examples.css.
|
|
1930
2121
|
write(
|
|
1931
2122
|
examplesThemePath,
|
|
1932
2123
|
examplesEntryTemplate({
|
|
1933
2124
|
configTokens: buildThemeCss(config.theme),
|
|
1934
|
-
sources:
|
|
2125
|
+
sources: exampleScanRoots(context.root, exampleDiscovery.dir).map(
|
|
2126
|
+
(dir) => `${dir}/${EXAMPLE_SCAN_GLOB}`
|
|
2127
|
+
),
|
|
1935
2128
|
userCss: userExamplesCss,
|
|
1936
2129
|
})
|
|
1937
2130
|
),
|
|
@@ -1986,8 +2179,9 @@ export const generateRuntime = async (
|
|
|
1986
2179
|
)
|
|
1987
2180
|
)
|
|
1988
2181
|
),
|
|
1989
|
-
writeAskFiles(project, srcDir, write),
|
|
1990
|
-
writeMcpFiles(
|
|
2182
|
+
writeAskFiles(project, srcDir, write, modules),
|
|
2183
|
+
writeMcpFiles(mcp, write, agentData),
|
|
2184
|
+
writeApiFiles(project, api, write, agentData, mcp),
|
|
1991
2185
|
playgroundProxy.enabled
|
|
1992
2186
|
? write(playgroundProxy.entrypoint, playgroundProxyTemplate(proxyOrigins))
|
|
1993
2187
|
: Promise.resolve(false),
|
|
@@ -1996,7 +2190,14 @@ export const generateRuntime = async (
|
|
|
1996
2190
|
if (config.seo.og.enabled) {
|
|
1997
2191
|
await write(
|
|
1998
2192
|
join(srcDir, "pages", "og", "[...slug].png.ts"),
|
|
1999
|
-
ogEndpointTemplate(
|
|
2193
|
+
ogEndpointTemplate(
|
|
2194
|
+
ogRoutes,
|
|
2195
|
+
{
|
|
2196
|
+
...projectOgFonts(project),
|
|
2197
|
+
pageDescriptions: config.seo.og.description !== false,
|
|
2198
|
+
},
|
|
2199
|
+
changelogIndex
|
|
2200
|
+
)
|
|
2000
2201
|
);
|
|
2001
2202
|
}
|
|
2002
2203
|
|
|
@@ -2034,10 +2235,7 @@ export const generateRuntime = async (
|
|
|
2034
2235
|
// Client-loaded providers (orama, flexsearch) ship a static index + endpoint.
|
|
2035
2236
|
if (servesStaticIndex(config.search.provider)) {
|
|
2036
2237
|
const documents = await buildSearchDocuments(project);
|
|
2037
|
-
|
|
2038
|
-
join(srcDir, "generated", "search.json"),
|
|
2039
|
-
`${JSON.stringify(documents)}\n`
|
|
2040
|
-
);
|
|
2238
|
+
modules.set("blume:search-index", JSON.stringify(documents));
|
|
2041
2239
|
await write(
|
|
2042
2240
|
join(srcDir, "pages", "blume-search.json.ts"),
|
|
2043
2241
|
searchEndpointTemplate()
|
|
@@ -2061,15 +2259,13 @@ export const generateRuntime = async (
|
|
|
2061
2259
|
);
|
|
2062
2260
|
|
|
2063
2261
|
const rawMarkdown = await buildRawMarkdown(project);
|
|
2262
|
+
modules.set("blume:raw-markdown", JSON.stringify(rawMarkdown));
|
|
2064
2263
|
// The originals behind the rewritten `/blume-assets/content/…` references in
|
|
2065
2264
|
// the agent-facing Markdown, plus the endpoint that serves them (and the
|
|
2066
2265
|
// remote-source assets materialized under `.blume/public/blume-assets`).
|
|
2067
2266
|
const contentAssets = await collectContentAssets(project);
|
|
2267
|
+
modules.set("blume:content-assets", JSON.stringify(contentAssets));
|
|
2068
2268
|
await Promise.all([
|
|
2069
|
-
write(
|
|
2070
|
-
join(srcDir, "generated", "raw-markdown.json"),
|
|
2071
|
-
`${JSON.stringify(rawMarkdown)}\n`
|
|
2072
|
-
),
|
|
2073
2269
|
write(
|
|
2074
2270
|
join(srcDir, "pages", "[...slug].md.ts"),
|
|
2075
2271
|
rawMarkdownEndpointTemplate("md")
|
|
@@ -2078,10 +2274,6 @@ export const generateRuntime = async (
|
|
|
2078
2274
|
join(srcDir, "pages", "[...slug].mdx.ts"),
|
|
2079
2275
|
rawMarkdownEndpointTemplate("mdx")
|
|
2080
2276
|
),
|
|
2081
|
-
write(
|
|
2082
|
-
join(srcDir, "generated", "content-assets.json"),
|
|
2083
|
-
`${JSON.stringify(contentAssets)}\n`
|
|
2084
|
-
),
|
|
2085
2277
|
write(
|
|
2086
2278
|
join(srcDir, "pages", "blume-assets", "[...asset].ts"),
|
|
2087
2279
|
contentAssetsEndpointTemplate(
|
|
@@ -2097,16 +2289,11 @@ export const generateRuntime = async (
|
|
|
2097
2289
|
const feedXml = Object.fromEntries(
|
|
2098
2290
|
feeds.map((feed) => [feed.type, renderRssFeed(feed)])
|
|
2099
2291
|
);
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
write(
|
|
2106
|
-
join(srcDir, "pages", "[section]", "rss.xml.ts"),
|
|
2107
|
-
rssEndpointTemplate()
|
|
2108
|
-
),
|
|
2109
|
-
]);
|
|
2292
|
+
modules.set("blume:rss", JSON.stringify(feedXml));
|
|
2293
|
+
await write(
|
|
2294
|
+
join(srcDir, "pages", "[section]", "rss.xml.ts"),
|
|
2295
|
+
rssEndpointTemplate()
|
|
2296
|
+
);
|
|
2110
2297
|
}
|
|
2111
2298
|
|
|
2112
2299
|
// Scalar-rendered API/AsyncAPI reference pages (`renderer: "scalar"`). One
|
|
@@ -2181,16 +2368,16 @@ export const generateRuntime = async (
|
|
|
2181
2368
|
);
|
|
2182
2369
|
}
|
|
2183
2370
|
|
|
2184
|
-
// `
|
|
2185
|
-
//
|
|
2186
|
-
//
|
|
2371
|
+
// `blume:openapi` is always published — even as `{}` — so the import resolves
|
|
2372
|
+
// whether or not a reference is enabled; the specs were parsed during the
|
|
2373
|
+
// scan, so this is just serialization. `blume:data` is the page data every
|
|
2374
|
+
// layout reads. Neither is "structural" for Astro; publishing hot-reloads.
|
|
2375
|
+
modules.set("blume:data", buildRuntimeData(project));
|
|
2376
|
+
modules.set("blume:openapi", JSON.stringify(openApiData));
|
|
2187
2377
|
// These write to distinct trees and never read one another, so they batch.
|
|
2188
|
-
// `
|
|
2189
|
-
//
|
|
2190
|
-
// own pruning), outside `.blume/src`, so a removed remote entry doesn't linger.
|
|
2378
|
+
// `writeStagedContent` owns the `.blume/content` tree (its own pruning),
|
|
2379
|
+
// outside `.blume/src`, so a removed remote entry doesn't linger.
|
|
2191
2380
|
await Promise.all([
|
|
2192
|
-
write(join(srcDir, "generated", "data.json"), buildRuntimeData(project)),
|
|
2193
|
-
write(openapiPath, `${JSON.stringify(openApiData)}\n`),
|
|
2194
2381
|
write(
|
|
2195
2382
|
join(out, "blume.manifest.json"),
|
|
2196
2383
|
`${JSON.stringify(project.manifest, null, 2)}\n`
|
|
@@ -2202,5 +2389,10 @@ export const generateRuntime = async (
|
|
|
2202
2389
|
// endpoint left behind after the feature was switched off.
|
|
2203
2390
|
await pruneOrphans(srcDir, written);
|
|
2204
2391
|
|
|
2392
|
+
// Publish last, once every page that imports a module is on disk: a live
|
|
2393
|
+
// dev server invalidates the changed modules and reloads the browser against
|
|
2394
|
+
// the finished tree, never a half-written one.
|
|
2395
|
+
publishRuntimeModules(modules);
|
|
2396
|
+
|
|
2205
2397
|
return { structuralChange: structural.some(Boolean), warnings };
|
|
2206
2398
|
};
|
package/src/astro/index.ts
CHANGED
|
@@ -5,3 +5,10 @@ export { withIncludeRefresh } from "./include-refresh.ts";
|
|
|
5
5
|
export type { GenerateResult } from "./generate.ts";
|
|
6
6
|
export { blumeIntegration } from "./integration.ts";
|
|
7
7
|
export type { BlumeIntegrationOptions, BlumePageRoute } from "./integration.ts";
|
|
8
|
+
export {
|
|
9
|
+
publishRuntimeModules,
|
|
10
|
+
readRuntimeModule,
|
|
11
|
+
RUNTIME_MODULE_FILES,
|
|
12
|
+
runtimeModulesPlugin,
|
|
13
|
+
} from "./runtime-modules.ts";
|
|
14
|
+
export type { RuntimeModuleId } from "./runtime-modules.ts";
|
|
@@ -29,7 +29,7 @@ const parseAccept = (accept: string): AcceptEntry[] =>
|
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Whether the client explicitly prefers Markdown over HTML. Browsers never send
|
|
32
|
-
* `text/markdown`, so an ordinary page request (`text/html
|
|
32
|
+
* `text/markdown`, so an ordinary page request (`text/html` or the catch-all wildcard) is false.
|
|
33
33
|
*/
|
|
34
34
|
export const prefersMarkdown = (accept: string | null | undefined): boolean => {
|
|
35
35
|
if (!accept) {
|