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/templates.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type { ExampleSpec } from "./examples.ts";
|
|
|
22
22
|
import type { BlumePageRoute } from "./integration.ts";
|
|
23
23
|
import type { IslandSpec } from "./islands.ts";
|
|
24
24
|
import type { OgCustomRoute } from "./pages.ts";
|
|
25
|
+
import { RUNTIME_MODULE_FILES } from "./runtime-modules.ts";
|
|
25
26
|
|
|
26
27
|
const WORKSPACE_MARKERS = [
|
|
27
28
|
".git",
|
|
@@ -290,9 +291,38 @@ const adapterRoot = (context: ProjectContext): string =>
|
|
|
290
291
|
* re-optimization. A blanket `/node_modules/` exclude would instead switch the
|
|
291
292
|
* React Compiler off for Blume's own components in published installs (they
|
|
292
293
|
* resolve under `node_modules/blume/src`, and exclude beats include in the
|
|
293
|
-
* plugin's filter), so only the pre-bundle cache is excluded.
|
|
294
|
+
* plugin's filter), so only the pre-bundle cache is excluded. The hidden runtime
|
|
295
|
+
* relocates that cache to `<runtime>/.cache/vite` (see `cacheOptions`), so both
|
|
296
|
+
* the default and the relocated path are excluded.
|
|
294
297
|
*/
|
|
295
|
-
const REACT_EXCLUDE = String.raw`exclude: [/\/node_modules\/\.vite\//]`;
|
|
298
|
+
const REACT_EXCLUDE = String.raw`exclude: [/\/node_modules\/\.vite\//, /\/\.cache\/vite\//]`;
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* The `cacheDir` entries for the generated config's top level and its `vite`
|
|
302
|
+
* block. The hidden runtime's `node_modules` is a junction into Blume's own
|
|
303
|
+
* package directory, and Astro (`node_modules/.astro`: the content data store,
|
|
304
|
+
* the fonts cache) and Vite (`node_modules/.vite`: pre-bundled deps) both
|
|
305
|
+
* default their caches under the project's `node_modules`. Two Blume projects
|
|
306
|
+
* that resolve the same package (a monorepo building docs and a sandbox in
|
|
307
|
+
* parallel) would therefore share one data store, and each build would serve
|
|
308
|
+
* the other's content — or 404 on entries the other cleared. Keep every cache
|
|
309
|
+
* inside the runtime dir instead. An ejected project (`generatedModulesDir`
|
|
310
|
+
* set) has real `node_modules`, so it keeps the defaults.
|
|
311
|
+
*/
|
|
312
|
+
const runtimeCacheOptions = (
|
|
313
|
+
context: ProjectContext,
|
|
314
|
+
generatedModulesDir: string | undefined
|
|
315
|
+
) => {
|
|
316
|
+
if (generatedModulesDir !== undefined) {
|
|
317
|
+
return { astro: "", vite: "" };
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
astro: `
|
|
321
|
+
cacheDir: ${JSON.stringify(`${context.outDir}/.cache/astro`)},`,
|
|
322
|
+
vite: `
|
|
323
|
+
cacheDir: ${JSON.stringify(`${context.outDir}/.cache/vite`)},`,
|
|
324
|
+
};
|
|
325
|
+
};
|
|
296
326
|
|
|
297
327
|
/**
|
|
298
328
|
* The `react()` integration call. When `compilerPath` is set (the resolved
|
|
@@ -394,6 +424,40 @@ const resolveOptimizeDeps = (options: {
|
|
|
394
424
|
return { optimizeDepsEntries, optimizeDepsInclude };
|
|
395
425
|
};
|
|
396
426
|
|
|
427
|
+
/**
|
|
428
|
+
* How the generated config reaches the runtime data modules (`blume:data`,
|
|
429
|
+
* the search index, …): served from memory by `runtimeModulesPlugin` in the
|
|
430
|
+
* hidden runtime, or aliased to JSON files under `generatedModulesDir` for an
|
|
431
|
+
* ejected project, which has no CLI to publish them (see `runtime-modules.ts`).
|
|
432
|
+
*/
|
|
433
|
+
interface RuntimeModuleWiring {
|
|
434
|
+
/** `resolve.alias` entries (one per module), empty in the in-memory form. */
|
|
435
|
+
aliasLines: string;
|
|
436
|
+
/** Extra `blume/astro` imports the wiring needs. */
|
|
437
|
+
imports: string[];
|
|
438
|
+
/** Leading `vite.plugins` entry, empty in the file-alias form. */
|
|
439
|
+
pluginEntry: string;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const renderRuntimeModuleWiring = (
|
|
443
|
+
generatedModulesDir: string | undefined
|
|
444
|
+
): RuntimeModuleWiring => {
|
|
445
|
+
if (generatedModulesDir === undefined) {
|
|
446
|
+
return {
|
|
447
|
+
aliasLines: "",
|
|
448
|
+
imports: ["runtimeModulesPlugin"],
|
|
449
|
+
pluginEntry: "runtimeModulesPlugin(), ",
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
const aliasLines = [...RUNTIME_MODULE_FILES]
|
|
453
|
+
.map(
|
|
454
|
+
([id, file]) =>
|
|
455
|
+
`\n ${JSON.stringify(id)}: ${JSON.stringify(`${generatedModulesDir}/${file}`)},`
|
|
456
|
+
)
|
|
457
|
+
.join("");
|
|
458
|
+
return { aliasLines, imports: [], pluginEntry: "" };
|
|
459
|
+
};
|
|
460
|
+
|
|
397
461
|
export const astroConfigTemplate = (options: {
|
|
398
462
|
context: ProjectContext;
|
|
399
463
|
config: ResolvedConfig;
|
|
@@ -404,13 +468,18 @@ export const astroConfigTemplate = (options: {
|
|
|
404
468
|
contentRoutes: string[];
|
|
405
469
|
/** The generated Ask trigger (`blume:ask`); renders nothing when Ask is off. */
|
|
406
470
|
askPath: string;
|
|
407
|
-
dataPath: string;
|
|
408
471
|
examplesPath: string;
|
|
409
472
|
/** The example-preview Tailwind entry (`blume:examples-theme`). */
|
|
410
473
|
examplesThemePath: string;
|
|
411
474
|
themePath: string;
|
|
412
475
|
searchClientPath: string;
|
|
413
|
-
|
|
476
|
+
/**
|
|
477
|
+
* Where the runtime data modules (`blume:data`, the search index, …) live as
|
|
478
|
+
* JSON files, for a project with no CLI to publish them in memory (eject):
|
|
479
|
+
* each id is aliased to its file under this directory. Absent, the modules
|
|
480
|
+
* are served from memory by `runtimeModulesPlugin` — the hidden runtime.
|
|
481
|
+
*/
|
|
482
|
+
generatedModulesDir?: string;
|
|
414
483
|
/**
|
|
415
484
|
* Absolute path to `babel-plugin-react-compiler` when the React Compiler is
|
|
416
485
|
* enabled (resolved from Blume's package root by the caller); null/absent
|
|
@@ -427,17 +496,27 @@ export const astroConfigTemplate = (options: {
|
|
|
427
496
|
/** Bridge used to load configured integrations without serializing them. */
|
|
428
497
|
integrationBridge?: IntegrationBridgeOptions;
|
|
429
498
|
}): string => {
|
|
430
|
-
const { context, config, needsReact, pages,
|
|
499
|
+
const { context, config, needsReact, pages, themePath } = options;
|
|
500
|
+
|
|
501
|
+
const { astro: cacheOptions, vite: viteCacheOption } = runtimeCacheOptions(
|
|
502
|
+
context,
|
|
503
|
+
options.generatedModulesDir
|
|
504
|
+
);
|
|
431
505
|
const {
|
|
432
506
|
askPath,
|
|
433
507
|
contentRoutes,
|
|
434
508
|
examplesPath,
|
|
435
509
|
examplesThemePath,
|
|
510
|
+
generatedModulesDir,
|
|
436
511
|
needsSvelte,
|
|
437
512
|
needsVue,
|
|
438
|
-
openapiPath,
|
|
439
513
|
searchClientPath,
|
|
440
514
|
} = options;
|
|
515
|
+
const {
|
|
516
|
+
aliasLines: runtimeModuleAliasLines,
|
|
517
|
+
imports: runtimeModuleImports,
|
|
518
|
+
pluginEntry: runtimeModulesPluginEntry,
|
|
519
|
+
} = renderRuntimeModuleWiring(generatedModulesDir);
|
|
441
520
|
const { deployment } = config;
|
|
442
521
|
const userAliasLines = renderUserAliases(options.aliases);
|
|
443
522
|
const server = deployment.output === "server";
|
|
@@ -588,6 +667,7 @@ export const astroConfigTemplate = (options: {
|
|
|
588
667
|
"blumeIntegration",
|
|
589
668
|
"includeHmrPlugin",
|
|
590
669
|
"prerenderDepsPlugin",
|
|
670
|
+
...runtimeModuleImports,
|
|
591
671
|
...(adapterOption.includes("withAdapterRoot") ? ["withAdapterRoot"] : []),
|
|
592
672
|
];
|
|
593
673
|
const blumeImport = `import { ${blumeImports.join(", ")} } from "blume/astro";\n`;
|
|
@@ -655,7 +735,7 @@ ${userConfigSetup}export default defineConfig({
|
|
|
655
735
|
root: ${JSON.stringify(context.outDir)},
|
|
656
736
|
srcDir: ${JSON.stringify(`${context.outDir}/src`)},
|
|
657
737
|
outDir: ${JSON.stringify(astroOutDir(context))},
|
|
658
|
-
publicDir: ${JSON.stringify(`${context.root}/public`)}
|
|
738
|
+
publicDir: ${JSON.stringify(`${context.root}/public`)},${cacheOptions}
|
|
659
739
|
output: ${JSON.stringify(deployment.output)},${adapterOption}${sessionOption}${siteOption}${baseOption}${imageOption}${redirectsOption}${i18nOption}${fontsOption}
|
|
660
740
|
integrations: [${integrations.join(", ")}${userIntegrationSpread}],
|
|
661
741
|
markdown: {
|
|
@@ -683,8 +763,8 @@ ${userConfigSetup}export default defineConfig({
|
|
|
683
763
|
// request latency behind the user's intent, so most navigations swap
|
|
684
764
|
// instantly.
|
|
685
765
|
prefetch: { prefetchAll: true },
|
|
686
|
-
vite: {
|
|
687
|
-
plugins: [tailwindcss(), includeHmrPlugin(${JSON.stringify(
|
|
766
|
+
vite: {${viteCacheOption}
|
|
767
|
+
plugins: [${runtimeModulesPluginEntry}tailwindcss(), includeHmrPlugin(${JSON.stringify(
|
|
688
768
|
`${context.outDir}/src/generated/includes.json`
|
|
689
769
|
)}), prerenderDepsPlugin()],
|
|
690
770
|
// Everything hydration can reach must be part of the dev dep optimizer's
|
|
@@ -733,12 +813,10 @@ ${userConfigSetup}export default defineConfig({
|
|
|
733
813
|
resolve: {
|
|
734
814
|
alias: {
|
|
735
815
|
"blume:ask": ${JSON.stringify(askPath)},
|
|
736
|
-
"blume:data": ${JSON.stringify(dataPath)},
|
|
737
816
|
"blume:examples": ${JSON.stringify(examplesPath)},
|
|
738
817
|
"blume:examples-theme": ${JSON.stringify(examplesThemePath)},
|
|
739
|
-
"blume:openapi": ${JSON.stringify(openapiPath)},
|
|
740
818
|
"blume:search-client": ${JSON.stringify(searchClientPath)},
|
|
741
|
-
"blume:theme": ${JSON.stringify(themePath)},${userAliasLines}
|
|
819
|
+
"blume:theme": ${JSON.stringify(themePath)},${runtimeModuleAliasLines}${userAliasLines}
|
|
742
820
|
},
|
|
743
821
|
},
|
|
744
822
|
server: {
|
|
@@ -932,7 +1010,7 @@ export const askEndpointTemplate = (
|
|
|
932
1010
|
if (grounded) {
|
|
933
1011
|
imports.push(
|
|
934
1012
|
'import { createAskContext } from "blume/ai/ask-context.ts";',
|
|
935
|
-
'import askData from "
|
|
1013
|
+
'import askData from "blume:ask-data";'
|
|
936
1014
|
);
|
|
937
1015
|
const groundFields: string[] = [];
|
|
938
1016
|
if (instructions) {
|
|
@@ -1080,7 +1158,7 @@ const { strings } = Astro.props;
|
|
|
1080
1158
|
/** Generate the static search index endpoint (`/blume-search.json`). */
|
|
1081
1159
|
export const searchEndpointTemplate = (): string =>
|
|
1082
1160
|
`// Generated by Blume. Do not edit.
|
|
1083
|
-
import documents from "
|
|
1161
|
+
import documents from "blume:search-index";
|
|
1084
1162
|
|
|
1085
1163
|
export const prerender = true;
|
|
1086
1164
|
|
|
@@ -1258,7 +1336,7 @@ export const POST: APIRoute = async ({ request }) => {
|
|
|
1258
1336
|
*/
|
|
1259
1337
|
export const rawMarkdownEndpointTemplate = (kind: "md" | "mdx"): string =>
|
|
1260
1338
|
`// Generated by Blume. Do not edit.
|
|
1261
|
-
import raw from "
|
|
1339
|
+
import raw from "blume:raw-markdown";
|
|
1262
1340
|
|
|
1263
1341
|
export const prerender = true;
|
|
1264
1342
|
|
|
@@ -1308,7 +1386,7 @@ import { existsSync } from "node:fs";
|
|
|
1308
1386
|
import { readdir, readFile } from "node:fs/promises";
|
|
1309
1387
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
1310
1388
|
import type { APIRoute } from "astro";
|
|
1311
|
-
import assets from "
|
|
1389
|
+
import assets from "blume:content-assets";
|
|
1312
1390
|
|
|
1313
1391
|
export const prerender = true;
|
|
1314
1392
|
|
|
@@ -1400,22 +1478,18 @@ export const mcpPageFile = (route: string): string =>
|
|
|
1400
1478
|
* generated data snapshot. Runs server-side (no prerender) so agents can query
|
|
1401
1479
|
* the docs over Streamable HTTP.
|
|
1402
1480
|
*/
|
|
1403
|
-
export const mcpEndpointTemplate = (
|
|
1404
|
-
|
|
1405
|
-
const up = "../".repeat(clean.split("/").length);
|
|
1406
|
-
return `// Generated by Blume. Do not edit.
|
|
1481
|
+
export const mcpEndpointTemplate = (): string =>
|
|
1482
|
+
`// Generated by Blume. Do not edit.
|
|
1407
1483
|
import type { APIRoute } from "astro";
|
|
1408
1484
|
import { createMcpFetchHandler } from "blume/ai/mcp/server.ts";
|
|
1409
|
-
import
|
|
1410
|
-
import data from "${up}generated/mcp-data.json";
|
|
1485
|
+
import data from "blume:mcp-data";
|
|
1411
1486
|
|
|
1412
1487
|
export const prerender = false;
|
|
1413
1488
|
|
|
1414
|
-
const handler = createMcpFetchHandler(data
|
|
1489
|
+
const handler = createMcpFetchHandler(data);
|
|
1415
1490
|
|
|
1416
1491
|
export const ALL: APIRoute = ({ request }) => handler(request);
|
|
1417
1492
|
`;
|
|
1418
|
-
};
|
|
1419
1493
|
|
|
1420
1494
|
/**
|
|
1421
1495
|
* Generate the playground's CORS proxy endpoint
|
|
@@ -1464,7 +1538,7 @@ export function GET() {
|
|
|
1464
1538
|
*/
|
|
1465
1539
|
export const rssEndpointTemplate = (): string =>
|
|
1466
1540
|
`// Generated by Blume. Do not edit.
|
|
1467
|
-
import feeds from "
|
|
1541
|
+
import feeds from "blume:rss";
|
|
1468
1542
|
|
|
1469
1543
|
export const prerender = true;
|
|
1470
1544
|
|
|
@@ -1486,7 +1560,16 @@ export function GET({ props }: { props: { section: string } }) {
|
|
|
1486
1560
|
/** Generate the OG image endpoint (`.blume/src/pages/og/[...slug].png.ts`). */
|
|
1487
1561
|
export const ogEndpointTemplate = (
|
|
1488
1562
|
customRoutes: OgCustomRoute[] = [],
|
|
1489
|
-
og: {
|
|
1563
|
+
og: {
|
|
1564
|
+
families?: OgFontFamilies;
|
|
1565
|
+
fonts?: OgFont[];
|
|
1566
|
+
/**
|
|
1567
|
+
* Whether a page's own description may replace the site-wide subtitle.
|
|
1568
|
+
* `false` when `seo.og.description` is `false`, which hides the subtitle
|
|
1569
|
+
* on every card, page text included. Defaults to `true`.
|
|
1570
|
+
*/
|
|
1571
|
+
pageDescriptions?: boolean;
|
|
1572
|
+
} = {},
|
|
1490
1573
|
includeChangelog = false
|
|
1491
1574
|
): string =>
|
|
1492
1575
|
`// Generated by Blume. Do not edit.
|
|
@@ -1510,28 +1593,52 @@ const families: OgFontFamilies | undefined = ${
|
|
|
1510
1593
|
og.families ? JSON.stringify(og.families) : "undefined"
|
|
1511
1594
|
};
|
|
1512
1595
|
|
|
1596
|
+
// A page's own description (its \`seo.description\`, else \`description\`) is
|
|
1597
|
+
// the card subtitle, so the image says what the page's og:description says.
|
|
1598
|
+
// Pages without one fall back to the site-wide subtitle at render time.
|
|
1599
|
+
// \`seo.og.description: false\` hides the subtitle on every card, page text
|
|
1600
|
+
// included, which is what switches this off.
|
|
1601
|
+
const pageDescriptions = ${og.pageDescriptions !== false};
|
|
1602
|
+
|
|
1603
|
+
interface CardProps {
|
|
1604
|
+
title: string;
|
|
1605
|
+
description: string | null;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1513
1608
|
export function getStaticPaths() {
|
|
1514
1609
|
const seen = new Set<string>();
|
|
1515
|
-
const paths: { params: { slug: string }; props:
|
|
1516
|
-
const add = (slug: string, title: string) => {
|
|
1610
|
+
const paths: { params: { slug: string }; props: CardProps }[] = [];
|
|
1611
|
+
const add = (slug: string, title: string, description: string | null) => {
|
|
1517
1612
|
if (seen.has(slug)) {
|
|
1518
1613
|
return;
|
|
1519
1614
|
}
|
|
1520
1615
|
seen.add(slug);
|
|
1521
|
-
paths.push({
|
|
1616
|
+
paths.push({
|
|
1617
|
+
params: { slug },
|
|
1618
|
+
props: { title, description: pageDescriptions ? description : null },
|
|
1619
|
+
});
|
|
1522
1620
|
};
|
|
1523
1621
|
// A custom page wins over a content route sharing its path, so add it first.
|
|
1622
|
+
// Its description is unknown at generate time, so it takes the site subtitle.
|
|
1524
1623
|
for (const route of customRoutes) {
|
|
1525
|
-
add(route.slug, route.title);
|
|
1624
|
+
add(route.slug, route.title, null);
|
|
1526
1625
|
}
|
|
1527
1626
|
for (const route of data.routes) {
|
|
1528
|
-
add(
|
|
1627
|
+
add(
|
|
1628
|
+
route.path === "/" ? "index" : route.path.slice(1),
|
|
1629
|
+
route.title,
|
|
1630
|
+
route.description
|
|
1631
|
+
);
|
|
1529
1632
|
}${
|
|
1530
1633
|
includeChangelog
|
|
1531
1634
|
? `
|
|
1532
1635
|
// The generated changelog index is not a content route, so it needs its own
|
|
1533
1636
|
// card. Added last: a custom page or content route owning /changelog wins.
|
|
1534
|
-
add(
|
|
1637
|
+
add(
|
|
1638
|
+
"changelog",
|
|
1639
|
+
data.ui.changelog?.title ?? "Changelog",
|
|
1640
|
+
data.ui.changelog?.description ?? null
|
|
1641
|
+
);`
|
|
1535
1642
|
: ""
|
|
1536
1643
|
}
|
|
1537
1644
|
return paths;
|
|
@@ -1544,11 +1651,11 @@ const repoSlug = data.config.github
|
|
|
1544
1651
|
? \`\${data.config.github.owner}/\${data.config.github.repo}\`
|
|
1545
1652
|
: undefined;
|
|
1546
1653
|
|
|
1547
|
-
export async function GET({ props }: { props:
|
|
1654
|
+
export async function GET({ props }: { props: CardProps }) {
|
|
1548
1655
|
const png = await renderOgImage({
|
|
1549
1656
|
accent: data.config.og.palette?.accent ?? data.config.theme.accent.light,
|
|
1550
1657
|
brand: data.config.title,
|
|
1551
|
-
description: data.config.og.description,
|
|
1658
|
+
description: props.description ?? data.config.og.description,
|
|
1552
1659
|
families,
|
|
1553
1660
|
fonts,
|
|
1554
1661
|
logo: data.config.og.logo,
|
|
@@ -1572,13 +1679,11 @@ export async function GET({ props }: { props: { title: string } }) {
|
|
|
1572
1679
|
* but mounted inside Blume's {@link ReferenceLayout} so the page keeps Blume's
|
|
1573
1680
|
* navbar on top. `renderMode: "client"` mounts the reference into a container
|
|
1574
1681
|
* element (rather than emitting a full HTML document), which is what lets it
|
|
1575
|
-
* live inside our shell.
|
|
1576
|
-
* the generated data module the layout reads.
|
|
1682
|
+
* live inside our shell.
|
|
1577
1683
|
*/
|
|
1578
1684
|
export const scalarReferenceTemplate = <Configuration extends object>(options: {
|
|
1579
1685
|
/** Scalar options forwarded verbatim (spec/theme config plus the author's `scalar` escape hatch). */
|
|
1580
1686
|
configuration: Configuration;
|
|
1581
|
-
dataImport: string;
|
|
1582
1687
|
noindex?: boolean;
|
|
1583
1688
|
route: string;
|
|
1584
1689
|
title: string;
|
|
@@ -1587,7 +1692,7 @@ export const scalarReferenceTemplate = <Configuration extends object>(options: {
|
|
|
1587
1692
|
// Generated by Blume. Do not edit.
|
|
1588
1693
|
import { ScalarComponent } from "@scalar/astro";
|
|
1589
1694
|
import ReferenceLayout from "blume/components/layout/ReferenceLayout.astro";
|
|
1590
|
-
import data from
|
|
1695
|
+
import data from "blume:data";
|
|
1591
1696
|
|
|
1592
1697
|
export const prerender = true;
|
|
1593
1698
|
|
|
@@ -2411,6 +2516,231 @@ const suggestions = [
|
|
|
2411
2516
|
</PageLayout>
|
|
2412
2517
|
`;
|
|
2413
2518
|
|
|
2519
|
+
/**
|
|
2520
|
+
* Generate `.blume/src/pages/404.md.ts`: the Markdown twin of the default 404
|
|
2521
|
+
* page, prerendered to `dist/404.md`. An agent that asked for a missing page
|
|
2522
|
+
* with `Accept: text/markdown` — or fetched a `.md` URL no page backs — gets
|
|
2523
|
+
* this body with the 404 status instead of the HTML shell; Vercel server
|
|
2524
|
+
* builds wire that into the routing config (`deploy/vercel-negotiation.ts`).
|
|
2525
|
+
* Same recovery links as the HTML page, absolute when the site URL is known:
|
|
2526
|
+
* the body is read out of context, so a relative link would leave the reader
|
|
2527
|
+
* guessing the host. Written alongside `404.astro` and skipped under the same
|
|
2528
|
+
* rule, so a project that owns `/404` owns both variants.
|
|
2529
|
+
*/
|
|
2530
|
+
export const notFoundMarkdownTemplate =
|
|
2531
|
+
(): string => `// Generated by Blume. Do not edit. Override by adding \`pages/404.astro\`.
|
|
2532
|
+
import { withBase } from "blume/components/islands/base-path.ts";
|
|
2533
|
+
import { absoluteUrl } from "blume/core/site-url.ts";
|
|
2534
|
+
import data from "blume:data";
|
|
2535
|
+
|
|
2536
|
+
export const prerender = true;
|
|
2537
|
+
|
|
2538
|
+
const nf = data.ui.notFound;
|
|
2539
|
+
|
|
2540
|
+
// Absolute for internal routes when the site is known; an external tab href
|
|
2541
|
+
// passes through untouched.
|
|
2542
|
+
const href = (path: string): string => {
|
|
2543
|
+
const based = withBase(path);
|
|
2544
|
+
return data.config.site && based.startsWith("/") && !based.startsWith("//")
|
|
2545
|
+
? absoluteUrl(data.config.site, based)
|
|
2546
|
+
: based;
|
|
2547
|
+
};
|
|
2548
|
+
|
|
2549
|
+
// The recovery set of 404.astro: home, every top-level section (a tab links to
|
|
2550
|
+
// its resolved target), then the machine-readable indexes that exist.
|
|
2551
|
+
const links = [
|
|
2552
|
+
{ href: href("/"), label: nf.home },
|
|
2553
|
+
...data.navigation.tabs.map((tab) => ({
|
|
2554
|
+
href: href(tab.href ?? tab.path),
|
|
2555
|
+
label: tab.label,
|
|
2556
|
+
})),
|
|
2557
|
+
...(data.config.discovery.sitemap
|
|
2558
|
+
? [{ href: href("/sitemap.xml"), label: nf.sitemap }]
|
|
2559
|
+
: []),
|
|
2560
|
+
...(data.config.discovery.llmsTxt
|
|
2561
|
+
? [{ href: href("/llms.txt"), label: nf.llms }]
|
|
2562
|
+
: []),
|
|
2563
|
+
...(data.config.discovery.api
|
|
2564
|
+
? [{ href: href("/openapi.json"), label: nf.api }]
|
|
2565
|
+
: []),
|
|
2566
|
+
];
|
|
2567
|
+
|
|
2568
|
+
const body = [
|
|
2569
|
+
"# " + nf.title,
|
|
2570
|
+
"",
|
|
2571
|
+
nf.description,
|
|
2572
|
+
"",
|
|
2573
|
+
"## " + nf.suggestions,
|
|
2574
|
+
"",
|
|
2575
|
+
...links.map((link) => "- [" + link.label + "](" + link.href + ")"),
|
|
2576
|
+
"",
|
|
2577
|
+
].join("\\n");
|
|
2578
|
+
|
|
2579
|
+
export function GET() {
|
|
2580
|
+
return new Response(body, {
|
|
2581
|
+
headers: {
|
|
2582
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
2583
|
+
// ~4 characters per token; keep in sync with markdownTokenCount.
|
|
2584
|
+
"x-markdown-tokens": String(Math.ceil(body.length / 4)),
|
|
2585
|
+
},
|
|
2586
|
+
});
|
|
2587
|
+
}
|
|
2588
|
+
`;
|
|
2589
|
+
|
|
2590
|
+
/**
|
|
2591
|
+
* Generate `.blume/src/pages/404.json.ts`: the JSON twin of the default 404
|
|
2592
|
+
* page, prerendered to `dist/404.json` as RFC 9457 problem details. An agent
|
|
2593
|
+
* that asked for a missing page with `Accept: application/json` gets this body
|
|
2594
|
+
* with the 404 status instead of the HTML shell (Vercel server builds wire
|
|
2595
|
+
* that into the routing config, like the Markdown twin). Same recovery links
|
|
2596
|
+
* as the other variants, carried as `links` and spelled out in `resolution`.
|
|
2597
|
+
* Written alongside `404.astro` and skipped under the same rule.
|
|
2598
|
+
*/
|
|
2599
|
+
export const notFoundJsonTemplate =
|
|
2600
|
+
(): string => `// Generated by Blume. Do not edit. Override by adding \`pages/404.astro\`.
|
|
2601
|
+
import { problem } from "blume/ai/api/problem.ts";
|
|
2602
|
+
import { withBase } from "blume/components/islands/base-path.ts";
|
|
2603
|
+
import { absoluteUrl } from "blume/core/site-url.ts";
|
|
2604
|
+
import data from "blume:data";
|
|
2605
|
+
|
|
2606
|
+
export const prerender = true;
|
|
2607
|
+
|
|
2608
|
+
const nf = data.ui.notFound;
|
|
2609
|
+
|
|
2610
|
+
// Absolute for internal routes when the site is known; an external tab href
|
|
2611
|
+
// passes through untouched.
|
|
2612
|
+
const href = (path: string): string => {
|
|
2613
|
+
const based = withBase(path);
|
|
2614
|
+
return data.config.site && based.startsWith("/") && !based.startsWith("//")
|
|
2615
|
+
? absoluteUrl(data.config.site, based)
|
|
2616
|
+
: based;
|
|
2617
|
+
};
|
|
2618
|
+
|
|
2619
|
+
// The recovery set of 404.astro: home, every top-level section (a tab links to
|
|
2620
|
+
// its resolved target), then the machine-readable indexes that exist.
|
|
2621
|
+
const links = [
|
|
2622
|
+
{ href: href("/"), label: nf.home },
|
|
2623
|
+
...data.navigation.tabs.map((tab) => ({
|
|
2624
|
+
href: href(tab.href ?? tab.path),
|
|
2625
|
+
label: tab.label,
|
|
2626
|
+
})),
|
|
2627
|
+
...(data.config.discovery.sitemap
|
|
2628
|
+
? [{ href: href("/sitemap.xml"), label: nf.sitemap }]
|
|
2629
|
+
: []),
|
|
2630
|
+
...(data.config.discovery.llmsTxt
|
|
2631
|
+
? [{ href: href("/llms.txt"), label: nf.llms }]
|
|
2632
|
+
: []),
|
|
2633
|
+
...(data.config.discovery.api
|
|
2634
|
+
? [{ href: href("/openapi.json"), label: nf.api }]
|
|
2635
|
+
: []),
|
|
2636
|
+
];
|
|
2637
|
+
|
|
2638
|
+
const body = problem({
|
|
2639
|
+
code: "PAGE_NOT_FOUND",
|
|
2640
|
+
detail: nf.description,
|
|
2641
|
+
links,
|
|
2642
|
+
resolution: nf.suggestions + ": " + links.map((link) => link.href).join(", "),
|
|
2643
|
+
status: 404,
|
|
2644
|
+
title: nf.title,
|
|
2645
|
+
});
|
|
2646
|
+
|
|
2647
|
+
export function GET() {
|
|
2648
|
+
return new Response(JSON.stringify(body, null, 2) + "\\n", {
|
|
2649
|
+
headers: { "Content-Type": "application/problem+json; charset=utf-8" },
|
|
2650
|
+
});
|
|
2651
|
+
}
|
|
2652
|
+
`;
|
|
2653
|
+
|
|
2654
|
+
/**
|
|
2655
|
+
* Generate the prerendered JSON docs API endpoints under
|
|
2656
|
+
* `.blume/src/pages/api/docs/`: the page index (`pages.json`), one JSON
|
|
2657
|
+
* document per page (`pages/[...route].json`), and the navigation tree
|
|
2658
|
+
* (`navigation.json`). Each is a thin wrapper over `blume/ai/api/handlers.ts`
|
|
2659
|
+
* reading the same snapshot the MCP server serves (`blume:mcp-data`), so the
|
|
2660
|
+
* REST and MCP answers can never diverge.
|
|
2661
|
+
*/
|
|
2662
|
+
export const apiPagesIndexTemplate = (): string =>
|
|
2663
|
+
`// Generated by Blume. Do not edit.
|
|
2664
|
+
import { pagesIndexResponse } from "blume/ai/api/handlers.ts";
|
|
2665
|
+
import data from "blume:mcp-data";
|
|
2666
|
+
|
|
2667
|
+
export const prerender = true;
|
|
2668
|
+
|
|
2669
|
+
export function GET() {
|
|
2670
|
+
return pagesIndexResponse(data);
|
|
2671
|
+
}
|
|
2672
|
+
`;
|
|
2673
|
+
|
|
2674
|
+
export const apiPageTemplate = (): string =>
|
|
2675
|
+
`// Generated by Blume. Do not edit.
|
|
2676
|
+
import { pageParams, pageResponse } from "blume/ai/api/handlers.ts";
|
|
2677
|
+
import data from "blume:mcp-data";
|
|
2678
|
+
|
|
2679
|
+
export const prerender = true;
|
|
2680
|
+
|
|
2681
|
+
export function getStaticPaths() {
|
|
2682
|
+
return pageParams(data);
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
export function GET({ props }: { props: { route: string } }) {
|
|
2686
|
+
return pageResponse(data, props.route);
|
|
2687
|
+
}
|
|
2688
|
+
`;
|
|
2689
|
+
|
|
2690
|
+
export const apiNavigationTemplate = (): string =>
|
|
2691
|
+
`// Generated by Blume. Do not edit.
|
|
2692
|
+
import { navigationResponse } from "blume/ai/api/handlers.ts";
|
|
2693
|
+
import data from "blume:mcp-data";
|
|
2694
|
+
|
|
2695
|
+
export const prerender = true;
|
|
2696
|
+
|
|
2697
|
+
export function GET() {
|
|
2698
|
+
return navigationResponse(data);
|
|
2699
|
+
}
|
|
2700
|
+
`;
|
|
2701
|
+
|
|
2702
|
+
/**
|
|
2703
|
+
* Generate the live search endpoint (`.blume/src/pages/api/docs/search.ts`),
|
|
2704
|
+
* server output only: the REST twin of the MCP `search_docs` tool, over the
|
|
2705
|
+
* same snapshot and index.
|
|
2706
|
+
*/
|
|
2707
|
+
export const apiSearchTemplate = (): string =>
|
|
2708
|
+
`// Generated by Blume. Do not edit.
|
|
2709
|
+
import type { APIRoute } from "astro";
|
|
2710
|
+
import { createSearchHandler } from "blume/ai/api/handlers.ts";
|
|
2711
|
+
import data from "blume:mcp-data";
|
|
2712
|
+
|
|
2713
|
+
export const prerender = false;
|
|
2714
|
+
|
|
2715
|
+
const handler = createSearchHandler(data);
|
|
2716
|
+
|
|
2717
|
+
export const GET: APIRoute = ({ request }) => handler(request);
|
|
2718
|
+
`;
|
|
2719
|
+
|
|
2720
|
+
/**
|
|
2721
|
+
* Generate the API namespace's catch-all (`.blume/src/pages/api/[...path].ts`),
|
|
2722
|
+
* server output only: any `/api/…` request no endpoint answers gets an RFC
|
|
2723
|
+
* 9457 problem document with the 404 status instead of the HTML not-found
|
|
2724
|
+
* page. Static segments always beat the rest parameter, so `/api/ask`, the
|
|
2725
|
+
* search proxy, and every prerendered docs endpoint keep winning. The site
|
|
2726
|
+
* context is baked in so the resolution links are absolute when the site is
|
|
2727
|
+
* known.
|
|
2728
|
+
*/
|
|
2729
|
+
export const apiNotFoundTemplate = (context: {
|
|
2730
|
+
base: string;
|
|
2731
|
+
site: string | null;
|
|
2732
|
+
}): string =>
|
|
2733
|
+
`// Generated by Blume. Do not edit.
|
|
2734
|
+
import type { APIRoute } from "astro";
|
|
2735
|
+
import { apiNotFoundResponse } from "blume/ai/api/handlers.ts";
|
|
2736
|
+
|
|
2737
|
+
export const prerender = false;
|
|
2738
|
+
|
|
2739
|
+
const context = ${JSON.stringify(context)};
|
|
2740
|
+
|
|
2741
|
+
export const ALL: APIRoute = ({ request }) => apiNotFoundResponse(request, context);
|
|
2742
|
+
`;
|
|
2743
|
+
|
|
2414
2744
|
/** The literal Astro hydration directive for an island's client mode. */
|
|
2415
2745
|
const islandDirective = (spec: IslandSpec): string =>
|
|
2416
2746
|
spec.client === "only"
|
|
@@ -2692,6 +3022,36 @@ declare module "blume:data" {
|
|
|
2692
3022
|
export default data;
|
|
2693
3023
|
}
|
|
2694
3024
|
|
|
3025
|
+
declare module "blume:ask-data" {
|
|
3026
|
+
const askData: import("blume/ai/ask-context.ts").AskData;
|
|
3027
|
+
export default askData;
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
declare module "blume:content-assets" {
|
|
3031
|
+
const assets: Record<string, string>;
|
|
3032
|
+
export default assets;
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
declare module "blume:mcp-data" {
|
|
3036
|
+
const data: import("blume/ai/mcp/data.ts").McpData;
|
|
3037
|
+
export default data;
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
declare module "blume:raw-markdown" {
|
|
3041
|
+
const raw: Record<string, import("blume/ai/markdown.ts").RawMarkdownEntry>;
|
|
3042
|
+
export default raw;
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
declare module "blume:rss" {
|
|
3046
|
+
const feeds: Record<string, string>;
|
|
3047
|
+
export default feeds;
|
|
3048
|
+
}
|
|
3049
|
+
|
|
3050
|
+
declare module "blume:search-index" {
|
|
3051
|
+
const documents: import("blume/search/documents.ts").SearchDocument[];
|
|
3052
|
+
export default documents;
|
|
3053
|
+
}
|
|
3054
|
+
|
|
2695
3055
|
declare module "blume:examples" {
|
|
2696
3056
|
type Examples = typeof import("./generated/examples.ts").examples;
|
|
2697
3057
|
export const examples: Record<string, Examples[keyof Examples]>;
|
|
@@ -334,12 +334,20 @@ const emitVercelNegotiation = async (
|
|
|
334
334
|
// endpoint stamps it on dev/server-rendered responses itself.
|
|
335
335
|
const rawMarkdown = await buildRawMarkdown(project);
|
|
336
336
|
const home = rawMarkdown["/"];
|
|
337
|
+
// The Markdown and JSON 404 routes point at the prerendered twins; only
|
|
338
|
+
// wire each when the build actually emitted it (a project that owns `/404`
|
|
339
|
+
// gets none).
|
|
340
|
+
const staticDir = join(root, ".vercel", "output", "static");
|
|
337
341
|
const injected = injectNegotiationRoutes(
|
|
338
342
|
await readFile(configPath, "utf-8"),
|
|
339
343
|
routePaths,
|
|
340
344
|
buildHomeLinkHeader(config, routePaths),
|
|
341
345
|
overrides,
|
|
342
|
-
home ? markdownTokenCount(agentMarkdown(home)) : undefined
|
|
346
|
+
home ? markdownTokenCount(agentMarkdown(home)) : undefined,
|
|
347
|
+
{
|
|
348
|
+
json: existsSync(join(staticDir, "404.json")),
|
|
349
|
+
markdown: existsSync(join(staticDir, "404.md")),
|
|
350
|
+
}
|
|
343
351
|
);
|
|
344
352
|
if (injected === null) {
|
|
345
353
|
logger.warn(
|
package/src/cli/commands/dev.ts
CHANGED
|
@@ -19,8 +19,9 @@ import { prepareProject } from "../prepare.ts";
|
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Resolve a `--host` flag value into what Astro/Vite's `server.host` expects.
|
|
22
|
-
* citty
|
|
23
|
-
* string and a bare `--host` parses as `""`
|
|
22
|
+
* citty has no mixed string/boolean arg type, so `host` is declared as a
|
|
23
|
+
* string and a bare `--host` parses as `""` (the CLI entry rewrites it to
|
|
24
|
+
* `--host=` first; see `host-args.ts`) — Node would bind all interfaces
|
|
24
25
|
* for `""`, but Vite's `resolveHostname` treats it as a literal hostname and
|
|
25
26
|
* prints malformed URLs like `http://:4321/`. Match Astro's own `--host`
|
|
26
27
|
* semantics instead: bare flag → `true` (bind all interfaces), `--host
|
|
@@ -219,7 +220,9 @@ export const devCommand = defineCommand({
|
|
|
219
220
|
}).on("all", regenerate);
|
|
220
221
|
const disposers = [
|
|
221
222
|
...project.sources.map((source) => source.watch?.(regenerate)),
|
|
222
|
-
() =>
|
|
223
|
+
() => {
|
|
224
|
+
void projectWatcher.close();
|
|
225
|
+
},
|
|
223
226
|
].filter((dispose) => dispose !== undefined);
|
|
224
227
|
|
|
225
228
|
const shutdown = async () => {
|