blume 1.5.3 → 1.6.1
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 +94 -0
- package/dist/cli/index.js +3949 -1403
- package/dist/cli/index.js.map +111 -96
- package/dist/types/ai/component-markdown.d.ts +79 -0
- package/dist/types/components/layout/nav-utils.d.ts +60 -0
- package/dist/types/core/base-path.d.ts +9 -0
- package/dist/types/core/config-input.d.ts +206 -4
- package/dist/types/core/config.d.ts +6 -4
- package/dist/types/core/data.d.ts +33 -2
- package/dist/types/core/github.d.ts +35 -0
- package/dist/types/core/i18n-ui.d.ts +10 -0
- package/dist/types/core/navigation.d.ts +69 -0
- package/dist/types/core/schema.d.ts +122 -1
- package/dist/types/core/sources/types.d.ts +31 -6
- package/dist/types/core/types.d.ts +29 -2
- package/dist/types/markdown/features.d.ts +21 -0
- package/dist/types/openapi/references.d.ts +26 -1
- package/dist/types/seo/jsonld.d.ts +105 -0
- package/dist/types/theme/fonts.d.ts +34 -4
- package/docs/07-faq.mdx +9 -9
- package/docs/_snippets/include-demo.mdx +7 -0
- package/docs/advanced/api-reference.mdx +13 -4
- package/docs/advanced/custom-pages.mdx +4 -2
- package/docs/advanced/graphql.mdx +84 -0
- package/docs/advanced/meta.ts +8 -1
- package/docs/configuration/ai.mdx +25 -3
- package/docs/configuration/index.mdx +24 -0
- package/docs/configuration/search.mdx +13 -1
- package/docs/configuration/seo.mdx +30 -3
- package/docs/configuration/theming.mdx +23 -0
- package/docs/content/components.mdx +15 -1
- package/docs/content/includes.mdx +68 -0
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +25 -0
- package/docs/content/sources.mdx +42 -1
- package/docs/content/syntax.mdx +69 -1
- package/docs/content/versioning.mdx +15 -9
- package/docs/reference/cli.mdx +2 -1
- package/package.json +66 -57
- package/skills/blume-migrate/SKILL.md +16 -7
- package/skills/blume-migrate/references/docusaurus.md +5 -3
- package/skills/blume-migrate/references/fumadocs.md +10 -2
- package/skills/blume-migrate/references/mintlify.md +3 -2
- package/skills/blume-migrate/references/nextra.md +2 -2
- package/skills/blume-migrate/references/starlight.md +1 -1
- package/src/ai/agent-readability.ts +2 -1
- package/src/ai/ask-data.ts +2 -1
- package/src/ai/component-markdown.ts +199 -36
- package/src/ai/llms.ts +93 -6
- package/src/ai/markdown.ts +2 -2
- package/src/ai/mcp/discovery.ts +10 -2
- package/src/ai/mcp/server.ts +74 -2
- package/src/astro/examples.ts +29 -2
- package/src/astro/generate.ts +282 -177
- package/src/astro/include-hmr.ts +81 -0
- package/src/astro/include-refresh.ts +0 -0
- package/src/astro/index.ts +10 -5
- package/src/astro/markdown-negotiation.ts +1 -1
- package/src/astro/runtime-modules.ts +196 -0
- package/src/astro/templates.ts +365 -113
- package/src/cli/commands/build.ts +91 -16
- 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/cli/init/questions.ts +1 -0
- package/src/cli/init/scaffold.ts +27 -4
- package/src/components/colors.ts +142 -0
- package/src/components/content/Badge.astro +5 -12
- package/src/components/content/Callout.astro +19 -36
- package/src/components/content/Card.astro +15 -21
- package/src/components/content/Component.astro +10 -1
- package/src/components/content/GithubInfo.astro +28 -9
- package/src/components/content/Tabs.astro +27 -5
- package/src/components/content/github-info.ts +20 -5
- package/src/components/copy-feedback.ts +93 -9
- package/src/components/dropdown-dismiss.ts +122 -0
- package/src/components/islands/ask-ai.tsx +4 -1
- package/src/components/islands/hooks.ts +3 -1
- package/src/components/layout/Fonts.astro +15 -8
- package/src/components/layout/Header.astro +44 -0
- package/src/components/layout/LanguageSwitcher.astro +9 -1
- package/src/components/layout/NavSelector.astro +12 -3
- package/src/components/layout/NavTree.astro +6 -18
- package/src/components/layout/PageActions.astro +54 -22
- package/src/components/layout/PageLayout.astro +2 -0
- package/src/components/layout/ReferenceLayout.astro +6 -1
- package/src/components/layout/RootLayout.astro +42 -15
- package/src/components/layout/Search.astro +36 -4
- package/src/components/layout/TableOfContents.astro +8 -2
- package/src/components/layout/head-scripts.ts +30 -1
- package/src/components/openapi/ApiOverview.astro +13 -3
- package/src/components/openapi/AsyncApiOperation.astro +7 -14
- package/src/components/openapi/GraphqlChip.astro +33 -0
- package/src/components/openapi/GraphqlFieldsTable.astro +111 -0
- package/src/components/openapi/GraphqlOperation.astro +186 -0
- package/src/components/openapi/GraphqlType.astro +154 -0
- package/src/components/openapi/MethodBadge.astro +3 -14
- package/src/components/openapi/Operation.astro +12 -5
- package/src/components/openapi/OperationPanel.astro +43 -0
- package/src/components/openapi/RequestPanel.astro +5 -10
- package/src/components/openapi/Responses.astro +1 -16
- package/src/components/openapi/graphql-helpers.ts +466 -0
- package/src/components/openapi/playground-client.ts +15 -0
- package/src/components/openapi/sample-panels.ts +45 -0
- package/src/components/openapi/snippets.ts +13 -35
- package/src/core/base-path.ts +11 -0
- package/src/core/config-input.ts +209 -2
- package/src/core/config.ts +6 -4
- package/src/core/content-assets.ts +15 -4
- package/src/core/data.ts +28 -3
- package/src/core/define-components.ts +2 -0
- package/src/core/diagnostics.ts +8 -0
- package/src/core/frontmatter.ts +20 -8
- package/src/core/github.ts +71 -0
- package/src/core/graph.ts +22 -8
- package/src/core/heading-markers.ts +96 -0
- package/src/core/i18n-ui.ts +12 -0
- package/src/core/includes.ts +633 -0
- package/src/core/last-modified.ts +36 -11
- package/src/core/links.ts +79 -13
- package/src/core/manifest.ts +10 -0
- package/src/core/meta.ts +2 -1
- package/src/core/nav-diagnostics.ts +11 -2
- package/src/core/navigation.ts +27 -6
- package/src/core/project-graph.ts +61 -9
- package/src/core/schema.ts +235 -36
- package/src/core/server-features.ts +5 -9
- package/src/core/sources/github-releases.ts +2 -2
- package/src/core/sources/normalize.ts +502 -115
- package/src/core/sources/notion.ts +43 -8
- package/src/core/sources/obsidian.ts +1038 -0
- package/src/core/sources/read.ts +36 -1
- package/src/core/sources/resolve.ts +34 -1
- package/src/core/sources/types.ts +28 -6
- package/src/core/sources/watch.ts +12 -8
- package/src/core/tsconfig-aliases.ts +48 -35
- package/src/core/types.ts +31 -2
- package/src/core/ui-packs/ar.ts +2 -0
- package/src/core/ui-packs/bg.ts +3 -0
- package/src/core/ui-packs/bn.ts +2 -0
- package/src/core/ui-packs/ca.ts +3 -0
- package/src/core/ui-packs/cs.ts +2 -0
- package/src/core/ui-packs/da.ts +2 -0
- package/src/core/ui-packs/de.ts +3 -0
- package/src/core/ui-packs/el.ts +3 -0
- package/src/core/ui-packs/es.ts +3 -0
- package/src/core/ui-packs/fa.ts +2 -0
- package/src/core/ui-packs/fi.ts +2 -0
- package/src/core/ui-packs/fr.ts +3 -0
- package/src/core/ui-packs/he.ts +2 -0
- package/src/core/ui-packs/hi.ts +2 -0
- package/src/core/ui-packs/hr.ts +3 -0
- package/src/core/ui-packs/hu.ts +3 -0
- package/src/core/ui-packs/id.ts +3 -0
- package/src/core/ui-packs/it.ts +2 -0
- package/src/core/ui-packs/ja.ts +3 -0
- package/src/core/ui-packs/ko.ts +3 -0
- package/src/core/ui-packs/nl.ts +3 -0
- package/src/core/ui-packs/no.ts +3 -0
- package/src/core/ui-packs/pl.ts +3 -0
- package/src/core/ui-packs/pt-br.ts +3 -0
- package/src/core/ui-packs/pt.ts +3 -0
- package/src/core/ui-packs/ro.ts +3 -0
- package/src/core/ui-packs/ru.ts +3 -0
- package/src/core/ui-packs/sk.ts +2 -0
- package/src/core/ui-packs/sr.ts +2 -0
- package/src/core/ui-packs/sv.ts +3 -0
- package/src/core/ui-packs/th.ts +2 -0
- package/src/core/ui-packs/tr.ts +3 -0
- package/src/core/ui-packs/uk.ts +3 -0
- package/src/core/ui-packs/vi.ts +2 -0
- package/src/core/ui-packs/zh-tw.ts +2 -0
- package/src/core/ui-packs/zh.ts +2 -0
- package/src/core/version-cut.ts +26 -6
- package/src/core/yaml.ts +26 -0
- package/src/deploy/function-bundle.ts +251 -0
- package/src/deploy/vercel-negotiation.ts +49 -6
- package/src/eval/schema.ts +3 -1
- package/src/markdown/code-title.ts +22 -16
- package/src/markdown/features.ts +21 -0
- package/src/markdown/fence-meta.ts +50 -0
- package/src/markdown/heading-anchors.ts +198 -37
- package/src/markdown/include.ts +247 -0
- package/src/markdown/index.ts +43 -34
- package/src/markdown/language-icon.ts +2 -2
- package/src/markdown/mdast.ts +7 -3
- package/src/markdown/ts2js.ts +264 -0
- package/src/og/card.ts +1 -1
- package/src/openapi/asyncapi.ts +4 -1
- package/src/openapi/graphql-build.ts +293 -0
- package/src/openapi/graphql.ts +212 -0
- package/src/openapi/model.ts +38 -5
- package/src/openapi/parse.ts +34 -0
- package/src/openapi/proxy.ts +30 -5
- package/src/openapi/references.ts +97 -13
- package/src/openapi/render-mdx.ts +66 -12
- package/src/openapi/scalar.ts +5 -16
- package/src/openapi/source.ts +91 -23
- package/src/registry/eject.ts +47 -17
- package/src/search/documents.ts +229 -37
- package/src/search/orama-index.ts +9 -5
- package/src/seo/jsonld.ts +293 -51
- package/src/theme/code-block-padding.ts +16 -0
- package/src/theme/entry.ts +67 -13
- package/src/theme/fonts.ts +189 -16
- package/src/theme/sources.ts +49 -0
- package/src/translate/prompts.ts +2 -0
- package/src/translate/run.ts +7 -0
- package/src/translate/work-list.ts +0 -0
package/src/astro/templates.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { askBackendRuntimeDep } from "../ai/ask.ts";
|
|
|
8
8
|
import type { AskBackend } from "../ai/ask.ts";
|
|
9
9
|
import { buildHomeLinkHeader } from "../ai/link-headers.ts";
|
|
10
10
|
import { normalizeBasePath } from "../core/base-path.ts";
|
|
11
|
+
import { TOC_HIDDEN_KEY } from "../core/heading-markers.ts";
|
|
11
12
|
import type { ResolvedConfig } from "../core/schema.ts";
|
|
12
13
|
import { BLUME_IGNORE_DIRS } from "../core/sources/watch.ts";
|
|
13
14
|
import { trimChar } from "../core/trim.ts";
|
|
@@ -16,11 +17,12 @@ import { applyBaseToAstroRedirects } from "../deploy/redirects.ts";
|
|
|
16
17
|
import type { OgFont, OgFontFamilies } from "../og/card.ts";
|
|
17
18
|
import { hasScalarReferences } from "../openapi/references.ts";
|
|
18
19
|
import { searchProviderMeta } from "../search/providers.ts";
|
|
19
|
-
import { buildFontEntries } from "../theme/fonts.ts";
|
|
20
|
+
import { buildFontEntries, fontLocaleCodes } from "../theme/fonts.ts";
|
|
20
21
|
import type { ExampleSpec } from "./examples.ts";
|
|
21
22
|
import type { BlumePageRoute } from "./integration.ts";
|
|
22
23
|
import type { IslandSpec } from "./islands.ts";
|
|
23
24
|
import type { OgCustomRoute } from "./pages.ts";
|
|
25
|
+
import { RUNTIME_MODULE_FILES } from "./runtime-modules.ts";
|
|
24
26
|
|
|
25
27
|
const WORKSPACE_MARKERS = [
|
|
26
28
|
".git",
|
|
@@ -289,9 +291,38 @@ const adapterRoot = (context: ProjectContext): string =>
|
|
|
289
291
|
* re-optimization. A blanket `/node_modules/` exclude would instead switch the
|
|
290
292
|
* React Compiler off for Blume's own components in published installs (they
|
|
291
293
|
* resolve under `node_modules/blume/src`, and exclude beats include in the
|
|
292
|
-
* 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.
|
|
293
297
|
*/
|
|
294
|
-
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
|
+
};
|
|
295
326
|
|
|
296
327
|
/**
|
|
297
328
|
* The `react()` integration call. When `compilerPath` is set (the resolved
|
|
@@ -307,35 +338,6 @@ const reactIntegration = (compilerPath: string | null | undefined): string =>
|
|
|
307
338
|
? `react({ babel: { plugins: [[${JSON.stringify(compilerPath)}, { target: "19" }]] }, ${REACT_EXCLUDE} })`
|
|
308
339
|
: `react({ ${REACT_EXCLUDE} })`;
|
|
309
340
|
|
|
310
|
-
/**
|
|
311
|
-
* The `server.watch` block for the generated dev config. Keeps the watcher out
|
|
312
|
-
* of Astro's cache dir — but ONLY when the docs collection is rooted at a
|
|
313
|
-
* directory containing the runtime dir (a migrated, `content.root: "."`
|
|
314
|
-
* project). There, the glob loader's watcher match (`picomatch.isMatch(entry,
|
|
315
|
-
* pattern)` with array-OR semantics, where any negated pattern matches
|
|
316
|
-
* unrelated files) fires on every `.blume/.astro` write — "No entry type
|
|
317
|
-
* found" noise, and a `data-store.json` event can re-ingest the store file as
|
|
318
|
-
* a JSON entry and loop the sync. Everywhere else the watcher MUST see
|
|
319
|
-
* `.astro/data-store.json`: its change events are the only trigger for
|
|
320
|
-
* Astro's dev-time content invalidation (see vite-plugin-content-virtual-mod),
|
|
321
|
-
* and `.md` bodies are rendered into the store at load time — so ignoring the
|
|
322
|
-
* file serves stale `.md` HTML on every request until the server restarts,
|
|
323
|
-
* even though the loader logs a reload.
|
|
324
|
-
*/
|
|
325
|
-
const devWatchOption = (
|
|
326
|
-
outDir: string,
|
|
327
|
-
contentWatchesRuntimeDir: boolean | undefined
|
|
328
|
-
): string =>
|
|
329
|
-
contentWatchesRuntimeDir
|
|
330
|
-
? `
|
|
331
|
-
// Astro's cache dir sits inside the docs collection, whose watcher would
|
|
332
|
-
// otherwise churn (and can loop) on Astro's own writes. Trade-off: .md
|
|
333
|
-
// body edits need a dev-server restart in this layout.
|
|
334
|
-
watch: {
|
|
335
|
-
ignored: ${JSON.stringify([join(outDir, ".astro", "**")])},
|
|
336
|
-
},`
|
|
337
|
-
: "";
|
|
338
|
-
|
|
339
341
|
interface IntegrationBridgeOptions {
|
|
340
342
|
/** Config path relative to the generated Astro config. */
|
|
341
343
|
configFile: string;
|
|
@@ -387,8 +389,11 @@ interface OptimizeDepsConfig {
|
|
|
387
389
|
* the Vite root is the generated runtime, so user pages, convention islands,
|
|
388
390
|
* and alias-reachable components all live outside it and are otherwise only
|
|
389
391
|
* crawled when first requested. The compiler runtime rides the include list
|
|
390
|
-
* because it is Babel-injected and no source scan can see it.
|
|
391
|
-
*
|
|
392
|
+
* because it is Babel-injected and no source scan can see it. @vitejs/plugin-react
|
|
393
|
+
* would add it itself, but only when the babel plugin is passed by its bare
|
|
394
|
+
* name (`getReactCompilerPlugin` is an exact string match) — Blume passes an
|
|
395
|
+
* absolute path (see `reactIntegration`), which that check never matches. See
|
|
396
|
+
* the optimizeDeps comment in the generated config for the failure this prevents.
|
|
392
397
|
*/
|
|
393
398
|
const resolveOptimizeDeps = (options: {
|
|
394
399
|
aliases: Record<string, string> | undefined;
|
|
@@ -419,6 +424,40 @@ const resolveOptimizeDeps = (options: {
|
|
|
419
424
|
return { optimizeDepsEntries, optimizeDepsInclude };
|
|
420
425
|
};
|
|
421
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
|
+
|
|
422
461
|
export const astroConfigTemplate = (options: {
|
|
423
462
|
context: ProjectContext;
|
|
424
463
|
config: ResolvedConfig;
|
|
@@ -429,13 +468,18 @@ export const astroConfigTemplate = (options: {
|
|
|
429
468
|
contentRoutes: string[];
|
|
430
469
|
/** The generated Ask trigger (`blume:ask`); renders nothing when Ask is off. */
|
|
431
470
|
askPath: string;
|
|
432
|
-
dataPath: string;
|
|
433
471
|
examplesPath: string;
|
|
434
472
|
/** The example-preview Tailwind entry (`blume:examples-theme`). */
|
|
435
473
|
examplesThemePath: string;
|
|
436
474
|
themePath: string;
|
|
437
475
|
searchClientPath: string;
|
|
438
|
-
|
|
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;
|
|
439
483
|
/**
|
|
440
484
|
* Absolute path to `babel-plugin-react-compiler` when the React Compiler is
|
|
441
485
|
* enabled (resolved from Blume's package root by the caller); null/absent
|
|
@@ -445,26 +489,34 @@ export const astroConfigTemplate = (options: {
|
|
|
445
489
|
/** Project tsconfig path aliases (`find` -> absolute dir), e.g. `@` -> src. */
|
|
446
490
|
aliases?: Record<string, string>;
|
|
447
491
|
/**
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
* only layout where the dev watcher must be kept out of Astro's cache dir.
|
|
451
|
-
* See {@link devWatchOption} for why this must stay scoped.
|
|
492
|
+
* The docs collection's content root; bounds `<include>` resolution in the
|
|
493
|
+
* processors and locates the include graph for dev-server invalidation.
|
|
452
494
|
*/
|
|
453
|
-
|
|
495
|
+
contentRoot?: string;
|
|
454
496
|
/** Bridge used to load configured integrations without serializing them. */
|
|
455
497
|
integrationBridge?: IntegrationBridgeOptions;
|
|
456
498
|
}): string => {
|
|
457
|
-
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
|
+
);
|
|
458
505
|
const {
|
|
459
506
|
askPath,
|
|
460
507
|
contentRoutes,
|
|
461
508
|
examplesPath,
|
|
462
509
|
examplesThemePath,
|
|
510
|
+
generatedModulesDir,
|
|
463
511
|
needsSvelte,
|
|
464
512
|
needsVue,
|
|
465
|
-
openapiPath,
|
|
466
513
|
searchClientPath,
|
|
467
514
|
} = options;
|
|
515
|
+
const {
|
|
516
|
+
aliasLines: runtimeModuleAliasLines,
|
|
517
|
+
imports: runtimeModuleImports,
|
|
518
|
+
pluginEntry: runtimeModulesPluginEntry,
|
|
519
|
+
} = renderRuntimeModuleWiring(generatedModulesDir);
|
|
468
520
|
const { deployment } = config;
|
|
469
521
|
const userAliasLines = renderUserAliases(options.aliases);
|
|
470
522
|
const server = deployment.output === "server";
|
|
@@ -552,7 +604,12 @@ export const astroConfigTemplate = (options: {
|
|
|
552
604
|
// `fontProviders` is only imported when at least one font is configured.
|
|
553
605
|
// Local variant sources are emitted as absolute paths (the Astro root is
|
|
554
606
|
// `.blume/`, not the user's project, so root-relative paths would miss).
|
|
555
|
-
|
|
607
|
+
// Subsets follow the configured locales (a Vietnamese site loads the
|
|
608
|
+
// `vietnamese` faces) unless a family pins its own.
|
|
609
|
+
const fontEntries = buildFontEntries(
|
|
610
|
+
config.theme.fonts,
|
|
611
|
+
fontLocaleCodes(config.i18n)
|
|
612
|
+
);
|
|
556
613
|
const fontsOption = fontEntries.length
|
|
557
614
|
? `\n fonts: [${fontEntries
|
|
558
615
|
.map((font) =>
|
|
@@ -588,6 +645,8 @@ export const astroConfigTemplate = (options: {
|
|
|
588
645
|
font.cssVariable
|
|
589
646
|
)}, weights: ${JSON.stringify(
|
|
590
647
|
font.weights
|
|
648
|
+
)}, subsets: ${JSON.stringify(
|
|
649
|
+
font.subsets
|
|
591
650
|
)}, fallbacks: ${JSON.stringify(font.fallbacks)} }`
|
|
592
651
|
)
|
|
593
652
|
.join(", ")}],`
|
|
@@ -606,8 +665,9 @@ export const astroConfigTemplate = (options: {
|
|
|
606
665
|
: "";
|
|
607
666
|
const blumeImports = [
|
|
608
667
|
"blumeIntegration",
|
|
668
|
+
"includeHmrPlugin",
|
|
609
669
|
"prerenderDepsPlugin",
|
|
610
|
-
|
|
670
|
+
...runtimeModuleImports,
|
|
611
671
|
...(adapterOption.includes("withAdapterRoot") ? ["withAdapterRoot"] : []),
|
|
612
672
|
];
|
|
613
673
|
const blumeImport = `import { ${blumeImports.join(", ")} } from "blume/astro";\n`;
|
|
@@ -632,6 +692,7 @@ export const astroConfigTemplate = (options: {
|
|
|
632
692
|
`mdx({ processor: blumeMdxProcessor(${JSON.stringify({
|
|
633
693
|
basePath: config.basePath,
|
|
634
694
|
codeThemes: config.markdown.codeBlocks.theme,
|
|
695
|
+
contentRoot: options.contentRoot,
|
|
635
696
|
deployBase,
|
|
636
697
|
headingAnchors: config.markdown.headingAnchors,
|
|
637
698
|
})}) })`,
|
|
@@ -657,10 +718,6 @@ export const astroConfigTemplate = (options: {
|
|
|
657
718
|
})})`
|
|
658
719
|
);
|
|
659
720
|
|
|
660
|
-
const watchOption = devWatchOption(
|
|
661
|
-
context.outDir,
|
|
662
|
-
options.contentWatchesRuntimeDir
|
|
663
|
-
);
|
|
664
721
|
const {
|
|
665
722
|
configSourceMarker,
|
|
666
723
|
userConfigImports,
|
|
@@ -678,13 +735,14 @@ ${userConfigSetup}export default defineConfig({
|
|
|
678
735
|
root: ${JSON.stringify(context.outDir)},
|
|
679
736
|
srcDir: ${JSON.stringify(`${context.outDir}/src`)},
|
|
680
737
|
outDir: ${JSON.stringify(astroOutDir(context))},
|
|
681
|
-
publicDir: ${JSON.stringify(`${context.root}/public`)}
|
|
738
|
+
publicDir: ${JSON.stringify(`${context.root}/public`)},${cacheOptions}
|
|
682
739
|
output: ${JSON.stringify(deployment.output)},${adapterOption}${sessionOption}${siteOption}${baseOption}${imageOption}${redirectsOption}${i18nOption}${fontsOption}
|
|
683
740
|
integrations: [${integrations.join(", ")}${userIntegrationSpread}],
|
|
684
741
|
markdown: {
|
|
685
742
|
processor: blumeMarkdownProcessor(${JSON.stringify({
|
|
686
743
|
basePath: config.basePath,
|
|
687
744
|
codeThemes: config.markdown.codeBlocks.theme,
|
|
745
|
+
contentRoot: options.contentRoot,
|
|
688
746
|
deployBase,
|
|
689
747
|
headingAnchors: config.markdown.headingAnchors,
|
|
690
748
|
})}),
|
|
@@ -705,8 +763,10 @@ ${userConfigSetup}export default defineConfig({
|
|
|
705
763
|
// request latency behind the user's intent, so most navigations swap
|
|
706
764
|
// instantly.
|
|
707
765
|
prefetch: { prefetchAll: true },
|
|
708
|
-
vite: {
|
|
709
|
-
plugins: [tailwindcss(),
|
|
766
|
+
vite: {${viteCacheOption}
|
|
767
|
+
plugins: [${runtimeModulesPluginEntry}tailwindcss(), includeHmrPlugin(${JSON.stringify(
|
|
768
|
+
`${context.outDir}/src/generated/includes.json`
|
|
769
|
+
)}), prerenderDepsPlugin()],
|
|
710
770
|
// Everything hydration can reach must be part of the dev dep optimizer's
|
|
711
771
|
// FIRST run. The Vite root is the generated runtime, so user pages,
|
|
712
772
|
// islands, and aliased components live outside it and are only crawled
|
|
@@ -753,18 +813,16 @@ ${userConfigSetup}export default defineConfig({
|
|
|
753
813
|
resolve: {
|
|
754
814
|
alias: {
|
|
755
815
|
"blume:ask": ${JSON.stringify(askPath)},
|
|
756
|
-
"blume:data": ${JSON.stringify(dataPath)},
|
|
757
816
|
"blume:examples": ${JSON.stringify(examplesPath)},
|
|
758
817
|
"blume:examples-theme": ${JSON.stringify(examplesThemePath)},
|
|
759
|
-
"blume:openapi": ${JSON.stringify(openapiPath)},
|
|
760
818
|
"blume:search-client": ${JSON.stringify(searchClientPath)},
|
|
761
|
-
"blume:theme": ${JSON.stringify(themePath)},${userAliasLines}
|
|
819
|
+
"blume:theme": ${JSON.stringify(themePath)},${runtimeModuleAliasLines}${userAliasLines}
|
|
762
820
|
},
|
|
763
821
|
},
|
|
764
822
|
server: {
|
|
765
823
|
fs: {
|
|
766
824
|
allow: ${JSON.stringify(fsAllow)},
|
|
767
|
-
}
|
|
825
|
+
},
|
|
768
826
|
},
|
|
769
827
|
},
|
|
770
828
|
});
|
|
@@ -778,14 +836,11 @@ export const stagedContentDir = (outDir: string): string =>
|
|
|
778
836
|
/**
|
|
779
837
|
* The runtime dir relative to the docs collection `base` when it sits inside
|
|
780
838
|
* it (a migrated, `content.root: "."` project) — null when it lives elsewhere.
|
|
781
|
-
* Drives
|
|
782
|
-
*
|
|
783
|
-
*
|
|
839
|
+
* Drives the collection's negative glob in `contentConfigTemplate`, which both
|
|
840
|
+
* keeps runtime-dir files out of the collection and (Astro's watcher honors
|
|
841
|
+
* negated patterns) keeps the content watcher off Astro's own `.astro` writes.
|
|
784
842
|
*/
|
|
785
|
-
|
|
786
|
-
base: string,
|
|
787
|
-
outDir: string
|
|
788
|
-
): string | null => {
|
|
843
|
+
const runtimeDirWithin = (base: string, outDir: string): string | null => {
|
|
789
844
|
const rel = relative(base, outDir);
|
|
790
845
|
return rel && !rel.startsWith("..") && !isAbsolute(rel) ? rel : null;
|
|
791
846
|
};
|
|
@@ -836,15 +891,11 @@ export const contentConfigTemplate = (options: {
|
|
|
836
891
|
const outDirRel = runtimeDirWithin(collectionBase, context.outDir);
|
|
837
892
|
const outDirIgnore = outDirRel ? [`!${outDirRel}/**`] : [];
|
|
838
893
|
|
|
839
|
-
// With no filesystem source, no route renders through `docs`, so glob
|
|
840
|
-
//
|
|
841
|
-
//
|
|
842
|
-
// collection
|
|
843
|
-
//
|
|
844
|
-
// — with array-OR semantics, any `!ignored/**` negation *matches* unrelated
|
|
845
|
-
// files, so negative patterns can't exclude a subtree there. An empty pattern
|
|
846
|
-
// matches nothing, so the watcher stays silent. The collection is still
|
|
847
|
-
// declared below so `getCollection("docs")` / `getEntry` resolve (to empty).
|
|
894
|
+
// With no filesystem source, no route renders through `docs`, so glob
|
|
895
|
+
// nothing: an all-staged project roots the collection at the project dir,
|
|
896
|
+
// and a patterned glob would scan (and watch) the whole project for nothing.
|
|
897
|
+
// The collection is still declared below so `getCollection("docs")` /
|
|
898
|
+
// `getEntry` resolve (to empty).
|
|
848
899
|
const filesystem = options.filesystem ?? true;
|
|
849
900
|
const docsPattern = filesystem
|
|
850
901
|
? [
|
|
@@ -880,13 +931,17 @@ const staged = defineCollection({
|
|
|
880
931
|
return `// Generated by Blume. Do not edit.
|
|
881
932
|
import { defineCollection } from "astro:content";
|
|
882
933
|
import { glob } from "astro/loaders";
|
|
934
|
+
import { withIncludeRefresh } from "blume/astro";
|
|
883
935
|
|
|
936
|
+
// withIncludeRefresh keeps <include>-bearing pages fresh: plain .md entries
|
|
937
|
+
// are rendered at sync time and digest-cached on the page file alone, so a
|
|
938
|
+
// partial edit (or a warm-cache rebuild after one) would serve stale HTML.
|
|
884
939
|
const docs = defineCollection({
|
|
885
|
-
loader: glob({
|
|
940
|
+
loader: withIncludeRefresh(glob({
|
|
886
941
|
pattern: ${JSON.stringify(docsPattern)},
|
|
887
942
|
base: ${JSON.stringify(astroGlobBase(collectionBase))},
|
|
888
943
|
generateId: ({ entry }) => entry,
|
|
889
|
-
}),
|
|
944
|
+
}), ${JSON.stringify(`${context.outDir}/src/generated/includes.json`)}),
|
|
890
945
|
});
|
|
891
946
|
${stagedBlock}
|
|
892
947
|
export const collections = { docs${options.staged ? ", staged" : ""} };
|
|
@@ -955,7 +1010,7 @@ export const askEndpointTemplate = (
|
|
|
955
1010
|
if (grounded) {
|
|
956
1011
|
imports.push(
|
|
957
1012
|
'import { createAskContext } from "blume/ai/ask-context.ts";',
|
|
958
|
-
'import askData from "
|
|
1013
|
+
'import askData from "blume:ask-data";'
|
|
959
1014
|
);
|
|
960
1015
|
const groundFields: string[] = [];
|
|
961
1016
|
if (instructions) {
|
|
@@ -1103,7 +1158,7 @@ const { strings } = Astro.props;
|
|
|
1103
1158
|
/** Generate the static search index endpoint (`/blume-search.json`). */
|
|
1104
1159
|
export const searchEndpointTemplate = (): string =>
|
|
1105
1160
|
`// Generated by Blume. Do not edit.
|
|
1106
|
-
import documents from "
|
|
1161
|
+
import documents from "blume:search-index";
|
|
1107
1162
|
|
|
1108
1163
|
export const prerender = true;
|
|
1109
1164
|
|
|
@@ -1281,7 +1336,7 @@ export const POST: APIRoute = async ({ request }) => {
|
|
|
1281
1336
|
*/
|
|
1282
1337
|
export const rawMarkdownEndpointTemplate = (kind: "md" | "mdx"): string =>
|
|
1283
1338
|
`// Generated by Blume. Do not edit.
|
|
1284
|
-
import raw from "
|
|
1339
|
+
import raw from "blume:raw-markdown";
|
|
1285
1340
|
|
|
1286
1341
|
export const prerender = true;
|
|
1287
1342
|
|
|
@@ -1331,7 +1386,7 @@ import { existsSync } from "node:fs";
|
|
|
1331
1386
|
import { readdir, readFile } from "node:fs/promises";
|
|
1332
1387
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
1333
1388
|
import type { APIRoute } from "astro";
|
|
1334
|
-
import assets from "
|
|
1389
|
+
import assets from "blume:content-assets";
|
|
1335
1390
|
|
|
1336
1391
|
export const prerender = true;
|
|
1337
1392
|
|
|
@@ -1423,22 +1478,18 @@ export const mcpPageFile = (route: string): string =>
|
|
|
1423
1478
|
* generated data snapshot. Runs server-side (no prerender) so agents can query
|
|
1424
1479
|
* the docs over Streamable HTTP.
|
|
1425
1480
|
*/
|
|
1426
|
-
export const mcpEndpointTemplate = (
|
|
1427
|
-
|
|
1428
|
-
const up = "../".repeat(clean.split("/").length);
|
|
1429
|
-
return `// Generated by Blume. Do not edit.
|
|
1481
|
+
export const mcpEndpointTemplate = (): string =>
|
|
1482
|
+
`// Generated by Blume. Do not edit.
|
|
1430
1483
|
import type { APIRoute } from "astro";
|
|
1431
1484
|
import { createMcpFetchHandler } from "blume/ai/mcp/server.ts";
|
|
1432
|
-
import
|
|
1433
|
-
import data from "${up}generated/mcp-data.json";
|
|
1485
|
+
import data from "blume:mcp-data";
|
|
1434
1486
|
|
|
1435
1487
|
export const prerender = false;
|
|
1436
1488
|
|
|
1437
|
-
const handler = createMcpFetchHandler(data
|
|
1489
|
+
const handler = createMcpFetchHandler(data);
|
|
1438
1490
|
|
|
1439
1491
|
export const ALL: APIRoute = ({ request }) => handler(request);
|
|
1440
1492
|
`;
|
|
1441
|
-
};
|
|
1442
1493
|
|
|
1443
1494
|
/**
|
|
1444
1495
|
* Generate the playground's CORS proxy endpoint
|
|
@@ -1487,7 +1538,7 @@ export function GET() {
|
|
|
1487
1538
|
*/
|
|
1488
1539
|
export const rssEndpointTemplate = (): string =>
|
|
1489
1540
|
`// Generated by Blume. Do not edit.
|
|
1490
|
-
import feeds from "
|
|
1541
|
+
import feeds from "blume:rss";
|
|
1491
1542
|
|
|
1492
1543
|
export const prerender = true;
|
|
1493
1544
|
|
|
@@ -1509,7 +1560,16 @@ export function GET({ props }: { props: { section: string } }) {
|
|
|
1509
1560
|
/** Generate the OG image endpoint (`.blume/src/pages/og/[...slug].png.ts`). */
|
|
1510
1561
|
export const ogEndpointTemplate = (
|
|
1511
1562
|
customRoutes: OgCustomRoute[] = [],
|
|
1512
|
-
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
|
+
} = {},
|
|
1513
1573
|
includeChangelog = false
|
|
1514
1574
|
): string =>
|
|
1515
1575
|
`// Generated by Blume. Do not edit.
|
|
@@ -1533,44 +1593,69 @@ const families: OgFontFamilies | undefined = ${
|
|
|
1533
1593
|
og.families ? JSON.stringify(og.families) : "undefined"
|
|
1534
1594
|
};
|
|
1535
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
|
+
|
|
1536
1608
|
export function getStaticPaths() {
|
|
1537
1609
|
const seen = new Set<string>();
|
|
1538
|
-
const paths: { params: { slug: string }; props:
|
|
1539
|
-
const add = (slug: string, title: string) => {
|
|
1610
|
+
const paths: { params: { slug: string }; props: CardProps }[] = [];
|
|
1611
|
+
const add = (slug: string, title: string, description: string | null) => {
|
|
1540
1612
|
if (seen.has(slug)) {
|
|
1541
1613
|
return;
|
|
1542
1614
|
}
|
|
1543
1615
|
seen.add(slug);
|
|
1544
|
-
paths.push({
|
|
1616
|
+
paths.push({
|
|
1617
|
+
params: { slug },
|
|
1618
|
+
props: { title, description: pageDescriptions ? description : null },
|
|
1619
|
+
});
|
|
1545
1620
|
};
|
|
1546
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.
|
|
1547
1623
|
for (const route of customRoutes) {
|
|
1548
|
-
add(route.slug, route.title);
|
|
1624
|
+
add(route.slug, route.title, null);
|
|
1549
1625
|
}
|
|
1550
1626
|
for (const route of data.routes) {
|
|
1551
|
-
add(
|
|
1627
|
+
add(
|
|
1628
|
+
route.path === "/" ? "index" : route.path.slice(1),
|
|
1629
|
+
route.title,
|
|
1630
|
+
route.description
|
|
1631
|
+
);
|
|
1552
1632
|
}${
|
|
1553
1633
|
includeChangelog
|
|
1554
1634
|
? `
|
|
1555
1635
|
// The generated changelog index is not a content route, so it needs its own
|
|
1556
1636
|
// card. Added last: a custom page or content route owning /changelog wins.
|
|
1557
|
-
add(
|
|
1637
|
+
add(
|
|
1638
|
+
"changelog",
|
|
1639
|
+
data.ui.changelog?.title ?? "Changelog",
|
|
1640
|
+
data.ui.changelog?.description ?? null
|
|
1641
|
+
);`
|
|
1558
1642
|
: ""
|
|
1559
1643
|
}
|
|
1560
1644
|
return paths;
|
|
1561
1645
|
}
|
|
1562
1646
|
|
|
1563
|
-
// Footer branding shared by every card. The
|
|
1564
|
-
//
|
|
1565
|
-
|
|
1566
|
-
|
|
1647
|
+
// Footer branding shared by every card. The slug comes from the configured
|
|
1648
|
+
// repo rather than the URL, so an Enterprise host reads the same as github.com;
|
|
1649
|
+
// the site text (host plus deployment base) is resolved at generate time.
|
|
1650
|
+
const repoSlug = data.config.github
|
|
1651
|
+
? \`\${data.config.github.owner}/\${data.config.github.repo}\`
|
|
1567
1652
|
: undefined;
|
|
1568
1653
|
|
|
1569
|
-
export async function GET({ props }: { props:
|
|
1654
|
+
export async function GET({ props }: { props: CardProps }) {
|
|
1570
1655
|
const png = await renderOgImage({
|
|
1571
1656
|
accent: data.config.og.palette?.accent ?? data.config.theme.accent.light,
|
|
1572
1657
|
brand: data.config.title,
|
|
1573
|
-
description: data.config.og.description,
|
|
1658
|
+
description: props.description ?? data.config.og.description,
|
|
1574
1659
|
families,
|
|
1575
1660
|
fonts,
|
|
1576
1661
|
logo: data.config.og.logo,
|
|
@@ -1594,13 +1679,11 @@ export async function GET({ props }: { props: { title: string } }) {
|
|
|
1594
1679
|
* but mounted inside Blume's {@link ReferenceLayout} so the page keeps Blume's
|
|
1595
1680
|
* navbar on top. `renderMode: "client"` mounts the reference into a container
|
|
1596
1681
|
* element (rather than emitting a full HTML document), which is what lets it
|
|
1597
|
-
* live inside our shell.
|
|
1598
|
-
* the generated data module the layout reads.
|
|
1682
|
+
* live inside our shell.
|
|
1599
1683
|
*/
|
|
1600
1684
|
export const scalarReferenceTemplate = <Configuration extends object>(options: {
|
|
1601
1685
|
/** Scalar options forwarded verbatim (spec/theme config plus the author's `scalar` escape hatch). */
|
|
1602
1686
|
configuration: Configuration;
|
|
1603
|
-
dataImport: string;
|
|
1604
1687
|
noindex?: boolean;
|
|
1605
1688
|
route: string;
|
|
1606
1689
|
title: string;
|
|
@@ -1609,7 +1692,7 @@ export const scalarReferenceTemplate = <Configuration extends object>(options: {
|
|
|
1609
1692
|
// Generated by Blume. Do not edit.
|
|
1610
1693
|
import { ScalarComponent } from "@scalar/astro";
|
|
1611
1694
|
import ReferenceLayout from "blume/components/layout/ReferenceLayout.astro";
|
|
1612
|
-
import data from
|
|
1695
|
+
import data from "blume:data";
|
|
1613
1696
|
|
|
1614
1697
|
export const prerender = true;
|
|
1615
1698
|
|
|
@@ -1786,7 +1869,18 @@ const entry = await getEntry(collection as CollectionKey, entryId);
|
|
|
1786
1869
|
if (!entry) {
|
|
1787
1870
|
return new Response(null, { status: 404 });
|
|
1788
1871
|
}
|
|
1789
|
-
const { Content, headings } = await render(entry);
|
|
1872
|
+
const { Content, headings: allHeadings, remarkPluginFrontmatter } = await render(entry);
|
|
1873
|
+
// \`[!toc]\`-marked headings render on the page but stay out of the table of
|
|
1874
|
+
// contents; the heading plugin reports their slugs through the render's
|
|
1875
|
+
// frontmatter (see markdown/heading-anchors.ts). Only the plugin's array
|
|
1876
|
+
// counts: \`frontmatter.extend\` can declare the same key, and on a page with
|
|
1877
|
+
// no headings that user-supplied value would pass straight through.
|
|
1878
|
+
const tocHiddenRaw = remarkPluginFrontmatter?.${TOC_HIDDEN_KEY};
|
|
1879
|
+
const tocHidden = new Set(Array.isArray(tocHiddenRaw) ? tocHiddenRaw : []);
|
|
1880
|
+
const headings =
|
|
1881
|
+
tocHidden.size > 0
|
|
1882
|
+
? allHeadings.filter((heading) => !tocHidden.has(heading.slug))
|
|
1883
|
+
: allHeadings;
|
|
1790
1884
|
const frontmatter = entry.data ?? {};
|
|
1791
1885
|
|
|
1792
1886
|
const seo = frontmatter.seo ?? {};
|
|
@@ -2312,6 +2406,7 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
|
|
|
2312
2406
|
*/
|
|
2313
2407
|
export const notFoundPageTemplate = (): string => `---
|
|
2314
2408
|
// Generated by Blume. Do not edit. Override by adding \`pages/404.astro\`.
|
|
2409
|
+
import Icon from "blume/components/Icon.astro";
|
|
2315
2410
|
import PageLayout from "blume/components/layout/PageLayout.astro";
|
|
2316
2411
|
import { withBase } from "blume/components/islands/base-path.ts";
|
|
2317
2412
|
import data from "blume:data";
|
|
@@ -2329,6 +2424,24 @@ const localeMeta = i18n
|
|
|
2329
2424
|
: null;
|
|
2330
2425
|
const dir = localeMeta?.dir ?? "ltr";
|
|
2331
2426
|
const htmlLang = i18n ? i18n.defaultLocale : "en";
|
|
2427
|
+
|
|
2428
|
+
// Recovery links, so a reader — or an agent that followed a stale URL — can
|
|
2429
|
+
// get back on track without guessing: every top-level section, then the
|
|
2430
|
+
// machine-readable indexes the build emits (the sitemap only exists with a
|
|
2431
|
+
// \`deployment.site\`; llms.txt only when \`ai.llmsTxt\` is on). Tabs link to
|
|
2432
|
+
// their resolved target when the section has no index page of its own.
|
|
2433
|
+
const suggestions = [
|
|
2434
|
+
...data.navigation.tabs.map((tab) => ({
|
|
2435
|
+
href: withBase(tab.href ?? tab.path),
|
|
2436
|
+
label: tab.label,
|
|
2437
|
+
})),
|
|
2438
|
+
...(data.config.discovery.sitemap
|
|
2439
|
+
? [{ href: withBase("/sitemap.xml"), label: nf.sitemap }]
|
|
2440
|
+
: []),
|
|
2441
|
+
...(data.config.discovery.llmsTxt
|
|
2442
|
+
? [{ href: withBase("/llms.txt"), label: nf.llms }]
|
|
2443
|
+
: []),
|
|
2444
|
+
];
|
|
2332
2445
|
---
|
|
2333
2446
|
|
|
2334
2447
|
<PageLayout
|
|
@@ -2349,19 +2462,128 @@ const htmlLang = i18n ? i18n.defaultLocale : "en";
|
|
|
2349
2462
|
noindex={true}
|
|
2350
2463
|
>
|
|
2351
2464
|
<div
|
|
2352
|
-
class="mx-auto
|
|
2465
|
+
class="mx-auto grid w-full max-w-5xl gap-12 px-6 py-20 sm:py-28 md:grid-cols-[3fr_2fr] md:gap-16 lg:gap-24 lg:py-36"
|
|
2353
2466
|
>
|
|
2354
|
-
<
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2467
|
+
<div class="flex flex-col items-start">
|
|
2468
|
+
<p
|
|
2469
|
+
class="font-mono text-xs font-medium tracking-widest text-muted-foreground"
|
|
2470
|
+
>
|
|
2471
|
+
404
|
|
2472
|
+
</p>
|
|
2473
|
+
<h1
|
|
2474
|
+
class="mt-4 text-balance text-4xl font-semibold tracking-tight text-foreground sm:text-5xl"
|
|
2475
|
+
>
|
|
2476
|
+
{nf.title}
|
|
2477
|
+
</h1>
|
|
2478
|
+
<p class="mt-4 max-w-md text-pretty text-lg text-muted-foreground">
|
|
2479
|
+
{nf.description}
|
|
2480
|
+
</p>
|
|
2481
|
+
<a
|
|
2482
|
+
class="mt-8 inline-flex items-center gap-1.5 rounded-full bg-accent py-2 pe-4 ps-3.5 text-sm font-medium text-accent-foreground transition-opacity hover:opacity-90"
|
|
2483
|
+
href={withBase("/")}
|
|
2484
|
+
>
|
|
2485
|
+
<Icon class="rtl:-scale-x-100" name="arrow-left" size={14} />
|
|
2486
|
+
{nf.home}
|
|
2487
|
+
</a>
|
|
2488
|
+
</div>
|
|
2489
|
+
{
|
|
2490
|
+
suggestions.length > 0 && (
|
|
2491
|
+
<nav aria-label={nf.suggestions} class="md:border-s md:border-border md:ps-12 lg:ps-16">
|
|
2492
|
+
<h2 class="text-xs font-medium uppercase tracking-widest text-muted-foreground">
|
|
2493
|
+
{nf.suggestions}
|
|
2494
|
+
</h2>
|
|
2495
|
+
<ul class="mt-4 divide-y divide-border border-y border-border">
|
|
2496
|
+
{suggestions.map((link) => (
|
|
2497
|
+
<li>
|
|
2498
|
+
<a
|
|
2499
|
+
class="group flex items-center justify-between gap-4 py-3 text-sm font-medium text-foreground transition-colors hover:text-accent"
|
|
2500
|
+
href={link.href}
|
|
2501
|
+
>
|
|
2502
|
+
<span>{link.label}</span>
|
|
2503
|
+
<Icon
|
|
2504
|
+
class="shrink-0 text-muted-foreground transition-transform group-hover:translate-x-0.5 rtl:-scale-x-100 rtl:group-hover:-translate-x-0.5"
|
|
2505
|
+
name="arrow-right"
|
|
2506
|
+
size={14}
|
|
2507
|
+
/>
|
|
2508
|
+
</a>
|
|
2509
|
+
</li>
|
|
2510
|
+
))}
|
|
2511
|
+
</ul>
|
|
2512
|
+
</nav>
|
|
2513
|
+
)
|
|
2514
|
+
}
|
|
2361
2515
|
</div>
|
|
2362
2516
|
</PageLayout>
|
|
2363
2517
|
`;
|
|
2364
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
|
+
];
|
|
2564
|
+
|
|
2565
|
+
const body = [
|
|
2566
|
+
"# " + nf.title,
|
|
2567
|
+
"",
|
|
2568
|
+
nf.description,
|
|
2569
|
+
"",
|
|
2570
|
+
"## " + nf.suggestions,
|
|
2571
|
+
"",
|
|
2572
|
+
...links.map((link) => "- [" + link.label + "](" + link.href + ")"),
|
|
2573
|
+
"",
|
|
2574
|
+
].join("\\n");
|
|
2575
|
+
|
|
2576
|
+
export function GET() {
|
|
2577
|
+
return new Response(body, {
|
|
2578
|
+
headers: {
|
|
2579
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
2580
|
+
// ~4 characters per token; keep in sync with markdownTokenCount.
|
|
2581
|
+
"x-markdown-tokens": String(Math.ceil(body.length / 4)),
|
|
2582
|
+
},
|
|
2583
|
+
});
|
|
2584
|
+
}
|
|
2585
|
+
`;
|
|
2586
|
+
|
|
2365
2587
|
/** The literal Astro hydration directive for an island's client mode. */
|
|
2366
2588
|
const islandDirective = (spec: IslandSpec): string =>
|
|
2367
2589
|
spec.client === "only"
|
|
@@ -2643,6 +2865,36 @@ declare module "blume:data" {
|
|
|
2643
2865
|
export default data;
|
|
2644
2866
|
}
|
|
2645
2867
|
|
|
2868
|
+
declare module "blume:ask-data" {
|
|
2869
|
+
const askData: import("blume/ai/ask-context.ts").AskData;
|
|
2870
|
+
export default askData;
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
declare module "blume:content-assets" {
|
|
2874
|
+
const assets: Record<string, string>;
|
|
2875
|
+
export default assets;
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
declare module "blume:mcp-data" {
|
|
2879
|
+
const data: import("blume/ai/mcp/data.ts").McpData;
|
|
2880
|
+
export default data;
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
declare module "blume:raw-markdown" {
|
|
2884
|
+
const raw: Record<string, import("blume/ai/markdown.ts").RawMarkdownEntry>;
|
|
2885
|
+
export default raw;
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
declare module "blume:rss" {
|
|
2889
|
+
const feeds: Record<string, string>;
|
|
2890
|
+
export default feeds;
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
declare module "blume:search-index" {
|
|
2894
|
+
const documents: import("blume/search/documents.ts").SearchDocument[];
|
|
2895
|
+
export default documents;
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2646
2898
|
declare module "blume:examples" {
|
|
2647
2899
|
type Examples = typeof import("./generated/examples.ts").examples;
|
|
2648
2900
|
export const examples: Record<string, Examples[keyof Examples]>;
|