blume 0.0.0 → 0.1.0
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/bin/blume.mjs +18 -0
- package/dist/cli/index.js +11989 -0
- package/dist/cli/index.js.map +141 -0
- package/docs/01-quickstart.mdx +99 -0
- package/docs/02-deployment.mdx +129 -0
- package/docs/advanced/api-reference.mdx +114 -0
- package/docs/advanced/blog.mdx +121 -0
- package/docs/advanced/changelog.mdx +113 -0
- package/docs/advanced/custom-pages.mdx +187 -0
- package/docs/advanced/meta.ts +7 -0
- package/docs/changelog/v0-1-0.mdx +12 -0
- package/docs/changelog/v0-2-0.mdx +16 -0
- package/docs/configuration/ai.mdx +228 -0
- package/docs/configuration/analytics.mdx +98 -0
- package/docs/configuration/customization.mdx +91 -0
- package/docs/configuration/export.mdx +70 -0
- package/docs/configuration/index.mdx +271 -0
- package/docs/configuration/meta.ts +15 -0
- package/docs/configuration/search.mdx +172 -0
- package/docs/configuration/seo.mdx +196 -0
- package/docs/configuration/theming.mdx +178 -0
- package/docs/content/components.mdx +565 -0
- package/docs/content/i18n.mdx +205 -0
- package/docs/content/index.mdx +161 -0
- package/docs/content/islands.mdx +94 -0
- package/docs/content/meta.mdx +119 -0
- package/docs/content/meta.ts +15 -0
- package/docs/content/navigation.mdx +168 -0
- package/docs/content/sources.mdx +216 -0
- package/docs/content/syntax.mdx +445 -0
- package/docs/index.mdx +112 -0
- package/docs/reference/cli.mdx +43 -0
- package/docs/reference/frontmatter.mdx +74 -0
- package/docs/reference/meta.ts +7 -0
- package/package.json +140 -6
- package/src/ai/ask.ts +93 -0
- package/src/ai/llms.ts +65 -0
- package/src/ai/markdown.ts +31 -0
- package/src/ai/mcp/data.ts +74 -0
- package/src/ai/mcp/discovery.ts +49 -0
- package/src/ai/mcp/server.ts +225 -0
- package/src/ai/mcp/tools.ts +47 -0
- package/src/assets/icon.png +0 -0
- package/src/astro/generate.ts +878 -0
- package/src/astro/index.ts +4 -0
- package/src/astro/integration.ts +74 -0
- package/src/astro/islands.ts +131 -0
- package/src/astro/markdown-negotiation.ts +68 -0
- package/src/astro/pages.ts +28 -0
- package/src/astro/templates.ts +1199 -0
- package/src/cli/commands/add.ts +81 -0
- package/src/cli/commands/build.ts +103 -0
- package/src/cli/commands/dev.ts +108 -0
- package/src/cli/commands/doctor.ts +74 -0
- package/src/cli/commands/eject.ts +57 -0
- package/src/cli/commands/init.ts +98 -0
- package/src/cli/commands/migrate.ts +39 -0
- package/src/cli/commands/preview.ts +39 -0
- package/src/cli/commands/sync.ts +52 -0
- package/src/cli/commands/validate.ts +60 -0
- package/src/cli/index.ts +35 -0
- package/src/cli/log.ts +37 -0
- package/src/cli/prepare.ts +80 -0
- package/src/components/Icon.astro +99 -0
- package/src/components/content/Accordion.astro +8 -0
- package/src/components/content/AccordionItem.astro +121 -0
- package/src/components/content/AutoTypeTable.astro +51 -0
- package/src/components/content/Badge.astro +124 -0
- package/src/components/content/Callout.astro +73 -0
- package/src/components/content/Card.astro +104 -0
- package/src/components/content/CardGroup.astro +14 -0
- package/src/components/content/CodeGroup.astro +13 -0
- package/src/components/content/Color.astro +15 -0
- package/src/components/content/ColorItem.astro +87 -0
- package/src/components/content/ColorRow.astro +10 -0
- package/src/components/content/Column.astro +6 -0
- package/src/components/content/Columns.astro +9 -0
- package/src/components/content/Expandable.astro +11 -0
- package/src/components/content/FileTree.astro +8 -0
- package/src/components/content/Frame.astro +70 -0
- package/src/components/content/GithubInfo.astro +110 -0
- package/src/components/content/Math.astro +24 -0
- package/src/components/content/Panel.astro +20 -0
- package/src/components/content/Prompt.astro +129 -0
- package/src/components/content/Step.astro +34 -0
- package/src/components/content/Steps.astro +20 -0
- package/src/components/content/Tab.astro +40 -0
- package/src/components/content/Tabs.astro +273 -0
- package/src/components/content/Tile.astro +42 -0
- package/src/components/content/Tooltip.astro +68 -0
- package/src/components/content/Tree.astro +300 -0
- package/src/components/content/TreeFile.astro +15 -0
- package/src/components/content/TreeFolder.astro +62 -0
- package/src/components/content/TypeTable.astro +106 -0
- package/src/components/content/Update.astro +66 -0
- package/src/components/content/Visibility.astro +12 -0
- package/src/components/content/Warning.astro +9 -0
- package/src/components/content/auto-type-table.ts +141 -0
- package/src/components/content/github-info.ts +79 -0
- package/src/components/content/mermaid-element.ts +68 -0
- package/src/components/github-mark.ts +9 -0
- package/src/components/index.ts +14 -0
- package/src/components/islands/AskAI.astro +12 -0
- package/src/components/islands/ask-ai.tsx +156 -0
- package/src/components/layout/Analytics.astro +63 -0
- package/src/components/layout/Banner.astro +50 -0
- package/src/components/layout/Breadcrumbs.astro +31 -0
- package/src/components/layout/Favicon.astro +15 -0
- package/src/components/layout/Fonts.astro +14 -0
- package/src/components/layout/Header.astro +188 -0
- package/src/components/layout/LanguageSwitcher.astro +56 -0
- package/src/components/layout/NavTree.astro +462 -0
- package/src/components/layout/PageActions.astro +438 -0
- package/src/components/layout/PageFeedback.astro +58 -0
- package/src/components/layout/Pagination.astro +56 -0
- package/src/components/layout/ReferenceLayout.astro +102 -0
- package/src/components/layout/RootLayout.astro +533 -0
- package/src/components/layout/Search.astro +608 -0
- package/src/components/layout/TableOfContents.astro +68 -0
- package/src/components/layout/analytics-client.ts +38 -0
- package/src/components/layout/nav-utils.ts +87 -0
- package/src/components/layout/overrides.ts +32 -0
- package/src/components/layout/search/algolia.ts +43 -0
- package/src/components/layout/search/endpoint.ts +22 -0
- package/src/components/layout/search/flexsearch.ts +52 -0
- package/src/components/layout/search/orama-cloud.ts +41 -0
- package/src/components/layout/search/orama.ts +26 -0
- package/src/components/layout/search/pagefind.ts +43 -0
- package/src/components/layout/search/types.ts +163 -0
- package/src/components/layout/search/typesense.ts +60 -0
- package/src/components/layout/toc-element.ts +108 -0
- package/src/core/bridge.ts +92 -0
- package/src/core/config.ts +112 -0
- package/src/core/content.ts +50 -0
- package/src/core/define-components.ts +34 -0
- package/src/core/define-meta.ts +20 -0
- package/src/core/deployment-env.ts +73 -0
- package/src/core/diagnostics.ts +104 -0
- package/src/core/graph.ts +128 -0
- package/src/core/i18n-ui.ts +171 -0
- package/src/core/i18n.ts +169 -0
- package/src/core/last-modified.ts +88 -0
- package/src/core/links.ts +336 -0
- package/src/core/load-module.ts +15 -0
- package/src/core/manifest.ts +126 -0
- package/src/core/meta.ts +97 -0
- package/src/core/navigation.ts +392 -0
- package/src/core/package-root.ts +37 -0
- package/src/core/project-graph.ts +153 -0
- package/src/core/project.ts +56 -0
- package/src/core/schema.ts +1057 -0
- package/src/core/server-features.ts +23 -0
- package/src/core/sources/assets.ts +77 -0
- package/src/core/sources/cache.ts +122 -0
- package/src/core/sources/filesystem.ts +99 -0
- package/src/core/sources/mdx-remote.ts +216 -0
- package/src/core/sources/mintlify.ts +161 -0
- package/src/core/sources/normalize.ts +227 -0
- package/src/core/sources/notion.ts +440 -0
- package/src/core/sources/portable-text.ts +143 -0
- package/src/core/sources/read.ts +36 -0
- package/src/core/sources/resolve.ts +158 -0
- package/src/core/sources/sanity.ts +218 -0
- package/src/core/sources/types.ts +105 -0
- package/src/core/types.ts +261 -0
- package/src/core/ui-packs/ar.ts +47 -0
- package/src/core/ui-packs/bg.ts +47 -0
- package/src/core/ui-packs/bn.ts +47 -0
- package/src/core/ui-packs/ca.ts +47 -0
- package/src/core/ui-packs/cs.ts +47 -0
- package/src/core/ui-packs/da.ts +47 -0
- package/src/core/ui-packs/de.ts +47 -0
- package/src/core/ui-packs/el.ts +47 -0
- package/src/core/ui-packs/es.ts +47 -0
- package/src/core/ui-packs/fa.ts +47 -0
- package/src/core/ui-packs/fi.ts +47 -0
- package/src/core/ui-packs/fr.ts +47 -0
- package/src/core/ui-packs/he.ts +47 -0
- package/src/core/ui-packs/hi.ts +47 -0
- package/src/core/ui-packs/hr.ts +47 -0
- package/src/core/ui-packs/hu.ts +47 -0
- package/src/core/ui-packs/id.ts +47 -0
- package/src/core/ui-packs/index.ts +87 -0
- package/src/core/ui-packs/it.ts +47 -0
- package/src/core/ui-packs/ja.ts +47 -0
- package/src/core/ui-packs/ko.ts +47 -0
- package/src/core/ui-packs/nl.ts +47 -0
- package/src/core/ui-packs/no.ts +47 -0
- package/src/core/ui-packs/pl.ts +47 -0
- package/src/core/ui-packs/pt-br.ts +47 -0
- package/src/core/ui-packs/pt.ts +47 -0
- package/src/core/ui-packs/ro.ts +47 -0
- package/src/core/ui-packs/ru.ts +47 -0
- package/src/core/ui-packs/sk.ts +47 -0
- package/src/core/ui-packs/sr.ts +47 -0
- package/src/core/ui-packs/sv.ts +47 -0
- package/src/core/ui-packs/th.ts +47 -0
- package/src/core/ui-packs/tr.ts +47 -0
- package/src/core/ui-packs/uk.ts +47 -0
- package/src/core/ui-packs/vi.ts +47 -0
- package/src/core/ui-packs/zh-tw.ts +47 -0
- package/src/core/ui-packs/zh.ts +47 -0
- package/src/core/version.ts +23 -0
- package/src/deploy/robots.ts +20 -0
- package/src/deploy/rss.ts +128 -0
- package/src/deploy/sitemap.ts +28 -0
- package/src/index.ts +27 -0
- package/src/markdown/code-title.ts +71 -0
- package/src/markdown/directives.ts +83 -0
- package/src/markdown/heading-anchors.ts +137 -0
- package/src/markdown/index.ts +159 -0
- package/src/markdown/inline-code.ts +108 -0
- package/src/markdown/language-icon.ts +172 -0
- package/src/markdown/math.ts +32 -0
- package/src/markdown/mdast.ts +48 -0
- package/src/markdown/mermaid.ts +37 -0
- package/src/markdown/package-commands.ts +159 -0
- package/src/markdown/package-install.ts +40 -0
- package/src/migrate/fumadocs/config.ts +106 -0
- package/src/migrate/fumadocs/content.ts +365 -0
- package/src/migrate/fumadocs/frontmatter.ts +18 -0
- package/src/migrate/fumadocs/index.ts +252 -0
- package/src/migrate/fumadocs/meta.ts +114 -0
- package/src/migrate/migrate.ts +53 -0
- package/src/migrate/mintlify/config.ts +1040 -0
- package/src/migrate/mintlify/content.ts +98 -0
- package/src/migrate/mintlify/frontmatter.ts +126 -0
- package/src/migrate/mintlify/i18n.ts +51 -0
- package/src/migrate/mintlify/icons.ts +128 -0
- package/src/migrate/mintlify/index.ts +266 -0
- package/src/migrate/mintlify/snippets.ts +305 -0
- package/src/migrate/mintlify/transform.ts +81 -0
- package/src/migrate/nextra/content.ts +46 -0
- package/src/migrate/nextra/frontmatter.ts +40 -0
- package/src/migrate/nextra/index.ts +374 -0
- package/src/migrate/nextra/meta.ts +266 -0
- package/src/migrate/shared.ts +623 -0
- package/src/migrate/starlight/config.ts +459 -0
- package/src/migrate/starlight/content.ts +78 -0
- package/src/migrate/starlight/frontmatter.ts +111 -0
- package/src/migrate/starlight/i18n.ts +54 -0
- package/src/migrate/starlight/index.ts +131 -0
- package/src/og/card.ts +92 -0
- package/src/og/index.ts +2 -0
- package/src/openapi/scalar.ts +246 -0
- package/src/registry/eject.ts +263 -0
- package/src/registry/registry.ts +100 -0
- package/src/registry/rewrite-imports.ts +39 -0
- package/src/runtime/index.ts +14 -0
- package/src/search/build.ts +23 -0
- package/src/search/documents.ts +165 -0
- package/src/search/orama-index.ts +66 -0
- package/src/search/providers.ts +91 -0
- package/src/search/sync/algolia.ts +30 -0
- package/src/search/sync/index.ts +50 -0
- package/src/search/sync/orama-cloud.ts +40 -0
- package/src/search/sync/typesense.ts +65 -0
- package/src/seo/jsonld.ts +113 -0
- package/src/theme/entry.ts +608 -0
- package/src/theme/fonts.ts +198 -0
- package/src/theme/icons.ts +184 -0
- package/src/theme/palette.ts +143 -0
- package/src/theme/twoslash.ts +81 -0
|
@@ -0,0 +1,1199 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import { dirname, join } from "pathe";
|
|
4
|
+
|
|
5
|
+
import { askBackendRuntimeDep } from "../ai/ask.ts";
|
|
6
|
+
import type { AskBackend } from "../ai/ask.ts";
|
|
7
|
+
import type { ResolvedConfig } from "../core/schema.ts";
|
|
8
|
+
import type { ProjectContext } from "../core/types.ts";
|
|
9
|
+
import { searchProviderMeta } from "../search/providers.ts";
|
|
10
|
+
import { buildFontEntries } from "../theme/fonts.ts";
|
|
11
|
+
import type { BlumePageRoute } from "./integration.ts";
|
|
12
|
+
import type { IslandSpec } from "./islands.ts";
|
|
13
|
+
|
|
14
|
+
const WORKSPACE_MARKERS = [
|
|
15
|
+
".git",
|
|
16
|
+
"bun.lock",
|
|
17
|
+
"bun.lockb",
|
|
18
|
+
"package-lock.json",
|
|
19
|
+
"pnpm-lock.yaml",
|
|
20
|
+
"pnpm-workspace.yaml",
|
|
21
|
+
"yarn.lock",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/** True when the package.json at this path declares a `workspaces` field. */
|
|
25
|
+
const hasWorkspacesField = (pkgPath: string): boolean => {
|
|
26
|
+
if (!existsSync(pkgPath)) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as {
|
|
31
|
+
workspaces?: unknown;
|
|
32
|
+
};
|
|
33
|
+
return pkg.workspaces !== undefined;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** True when a directory looks like a workspace/monorepo root. */
|
|
40
|
+
const hasWorkspaceMarker = (dir: string): boolean =>
|
|
41
|
+
hasWorkspacesField(join(dir, "package.json")) ||
|
|
42
|
+
WORKSPACE_MARKERS.some((marker) => existsSync(join(dir, marker)));
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Walk up from the project root to the workspace/monorepo root so Vite's
|
|
46
|
+
* `fs.allow` can reach hoisted dependencies — e.g. KaTeX fonts that resolve to
|
|
47
|
+
* a monorepo root `node_modules` outside the project directory. Falls back to
|
|
48
|
+
* the project root when no workspace markers are found.
|
|
49
|
+
*/
|
|
50
|
+
const findWorkspaceRoot = (start: string): string => {
|
|
51
|
+
let dir = start;
|
|
52
|
+
for (;;) {
|
|
53
|
+
if (hasWorkspaceMarker(dir)) {
|
|
54
|
+
return dir;
|
|
55
|
+
}
|
|
56
|
+
const parent = dirname(dir);
|
|
57
|
+
if (parent === dir) {
|
|
58
|
+
return start;
|
|
59
|
+
}
|
|
60
|
+
dir = parent;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const ADAPTER_IMPORTS: Record<string, string> = {
|
|
65
|
+
cloudflare: "@astrojs/cloudflare",
|
|
66
|
+
netlify: "@astrojs/netlify",
|
|
67
|
+
node: "@astrojs/node",
|
|
68
|
+
vercel: "@astrojs/vercel",
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const ADAPTER_OPTIONS: Record<string, string> = {
|
|
72
|
+
node: '{ mode: "standalone" }',
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Integration packages the generated runtime imports. Declaring them in
|
|
77
|
+
* `.blume/package.json` lets Astro's framework-package crawl discover and bundle
|
|
78
|
+
* them — notably the React renderer's server entry, which imports the
|
|
79
|
+
* `astro:react:opts` virtual module and must not be externalized (this applies
|
|
80
|
+
* across the `ssr`, `prerender`, and `client` Vite environments).
|
|
81
|
+
*/
|
|
82
|
+
export const runtimeDependencies = (options: {
|
|
83
|
+
config: ResolvedConfig;
|
|
84
|
+
needsReact: boolean;
|
|
85
|
+
needsVue?: boolean;
|
|
86
|
+
needsSvelte?: boolean;
|
|
87
|
+
}): string[] => {
|
|
88
|
+
const { config, needsReact, needsSvelte, needsVue } = options;
|
|
89
|
+
const deps = ["@astrojs/mdx"];
|
|
90
|
+
if (needsReact) {
|
|
91
|
+
deps.push("@astrojs/react");
|
|
92
|
+
}
|
|
93
|
+
if (needsVue) {
|
|
94
|
+
deps.push("@astrojs/vue");
|
|
95
|
+
}
|
|
96
|
+
if (needsSvelte) {
|
|
97
|
+
deps.push("@astrojs/svelte");
|
|
98
|
+
}
|
|
99
|
+
// The Scalar integration is only declared when an API reference is configured,
|
|
100
|
+
// so projects that don't use it never pull it into the runtime.
|
|
101
|
+
if (config.openapi.enabled || config.asyncapi.enabled) {
|
|
102
|
+
deps.push("@scalar/astro");
|
|
103
|
+
}
|
|
104
|
+
// Only the configured search provider's SDK is declared, so a project pulls in
|
|
105
|
+
// (and the user installs) exactly the backend it uses — nothing more.
|
|
106
|
+
deps.push(...searchProviderMeta(config.search.provider).runtimeDeps);
|
|
107
|
+
// Ask AI's provider SDK, when its backend needs one (gateway uses core `ai`).
|
|
108
|
+
if (config.ai.ask?.enabled) {
|
|
109
|
+
const askDep = askBackendRuntimeDep(config.ai.ask);
|
|
110
|
+
if (askDep) {
|
|
111
|
+
deps.push(askDep);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const { deployment } = config;
|
|
115
|
+
if (deployment.output === "server" && deployment.adapter) {
|
|
116
|
+
const adapter = ADAPTER_IMPORTS[deployment.adapter];
|
|
117
|
+
if (adapter) {
|
|
118
|
+
deps.push(adapter);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return deps;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/** Generate `.blume/astro.config.mjs`. */
|
|
125
|
+
export const astroConfigTemplate = (options: {
|
|
126
|
+
context: ProjectContext;
|
|
127
|
+
config: ResolvedConfig;
|
|
128
|
+
needsReact: boolean;
|
|
129
|
+
needsVue?: boolean;
|
|
130
|
+
needsSvelte?: boolean;
|
|
131
|
+
pages: BlumePageRoute[];
|
|
132
|
+
contentRoutes: string[];
|
|
133
|
+
dataPath: string;
|
|
134
|
+
themePath: string;
|
|
135
|
+
searchClientPath: string;
|
|
136
|
+
}): string => {
|
|
137
|
+
const { context, config, needsReact, pages, dataPath, themePath } = options;
|
|
138
|
+
const { contentRoutes, needsSvelte, needsVue, searchClientPath } = options;
|
|
139
|
+
const { deployment } = config;
|
|
140
|
+
const server = deployment.output === "server";
|
|
141
|
+
|
|
142
|
+
// The project root plus the workspace root, so hoisted dependencies (e.g.
|
|
143
|
+
// KaTeX fonts under a monorepo's root node_modules) stay servable in dev.
|
|
144
|
+
const fsAllow = [...new Set([findWorkspaceRoot(context.root), context.root])];
|
|
145
|
+
|
|
146
|
+
const adapterImport =
|
|
147
|
+
server && deployment.adapter
|
|
148
|
+
? `import adapter from "${ADAPTER_IMPORTS[deployment.adapter]}";\n`
|
|
149
|
+
: "";
|
|
150
|
+
const adapterArgs =
|
|
151
|
+
server && deployment.adapter
|
|
152
|
+
? (ADAPTER_OPTIONS[deployment.adapter] ?? "")
|
|
153
|
+
: "";
|
|
154
|
+
const adapterOption =
|
|
155
|
+
server && deployment.adapter ? `\n adapter: adapter(${adapterArgs}),` : "";
|
|
156
|
+
|
|
157
|
+
const siteOption = deployment.site
|
|
158
|
+
? `\n site: ${JSON.stringify(deployment.site)},`
|
|
159
|
+
: "";
|
|
160
|
+
const baseOption = deployment.base
|
|
161
|
+
? `\n base: ${JSON.stringify(deployment.base)},`
|
|
162
|
+
: "";
|
|
163
|
+
|
|
164
|
+
// Astro's native i18n gives locale-aware helpers + `<html lang>` correctness.
|
|
165
|
+
// Blume owns getStaticPaths and materializes fallback routes in the manifest,
|
|
166
|
+
// so we deliberately omit Astro's `fallback` to keep one source of routing.
|
|
167
|
+
const i18nOption = config.i18n
|
|
168
|
+
? `\n i18n: ${JSON.stringify({
|
|
169
|
+
defaultLocale: config.i18n.defaultLocale,
|
|
170
|
+
locales: config.i18n.locales.map((locale) => locale.code),
|
|
171
|
+
routing: {
|
|
172
|
+
prefixDefaultLocale: !config.i18n.hideDefaultLocalePrefix,
|
|
173
|
+
},
|
|
174
|
+
})},`
|
|
175
|
+
: "";
|
|
176
|
+
|
|
177
|
+
const redirectsOption =
|
|
178
|
+
config.redirects.length > 0
|
|
179
|
+
? `\n redirects: ${JSON.stringify(
|
|
180
|
+
Object.fromEntries(
|
|
181
|
+
config.redirects.map((redirect) => [
|
|
182
|
+
redirect.from,
|
|
183
|
+
{ destination: redirect.to, status: redirect.status },
|
|
184
|
+
])
|
|
185
|
+
)
|
|
186
|
+
)},`
|
|
187
|
+
: "";
|
|
188
|
+
|
|
189
|
+
// Self-hosted Google Fonts via Astro's Fonts API, derived from theme.fonts.
|
|
190
|
+
// `fontProviders` is only imported when at least one font is configured.
|
|
191
|
+
const fontEntries = buildFontEntries(config.theme.fonts);
|
|
192
|
+
const fontsOption = fontEntries.length
|
|
193
|
+
? `\n fonts: [${fontEntries
|
|
194
|
+
.map(
|
|
195
|
+
(font) =>
|
|
196
|
+
`{ provider: fontProviders.google(), name: ${JSON.stringify(
|
|
197
|
+
font.name
|
|
198
|
+
)}, cssVariable: ${JSON.stringify(
|
|
199
|
+
font.cssVariable
|
|
200
|
+
)}, weights: ${JSON.stringify(
|
|
201
|
+
font.weights
|
|
202
|
+
)}, fallbacks: ${JSON.stringify(font.fallbacks)} }`
|
|
203
|
+
)
|
|
204
|
+
.join(", ")}],`
|
|
205
|
+
: "";
|
|
206
|
+
const defineConfigImport = fontEntries.length
|
|
207
|
+
? `import { defineConfig, fontProviders } from "astro/config";`
|
|
208
|
+
: `import { defineConfig } from "astro/config";`;
|
|
209
|
+
|
|
210
|
+
// Framework renderers are only wired in when an island (or Ask AI, for React)
|
|
211
|
+
// needs them. The core theme is Astro-first and ships no client JS.
|
|
212
|
+
const reactImport = needsReact ? `import react from "@astrojs/react";\n` : "";
|
|
213
|
+
const vueImport = needsVue ? `import vue from "@astrojs/vue";\n` : "";
|
|
214
|
+
const svelteImport = needsSvelte
|
|
215
|
+
? `import svelte from "@astrojs/svelte";\n`
|
|
216
|
+
: "";
|
|
217
|
+
const blumeImport = `import { blumeIntegration } from "blume/astro";\n`;
|
|
218
|
+
|
|
219
|
+
// Twoslash runs first, before the always-on transformers, but only on fences
|
|
220
|
+
// with the `twoslash` meta (explicitTrigger) — so it's opt-in per block with
|
|
221
|
+
// no config flag; the TypeScript compiler only spins up when a block uses it.
|
|
222
|
+
const twoslashImport = `import { transformerTwoslash } from "@shikijs/twoslash";\n`;
|
|
223
|
+
const twoslashTransformer =
|
|
224
|
+
"transformerTwoslash({ explicitTrigger: true }), ";
|
|
225
|
+
|
|
226
|
+
const integrations = [
|
|
227
|
+
`mdx({ processor: blumeMdxProcessor(${JSON.stringify({
|
|
228
|
+
headingAnchors: config.markdown.headingAnchors,
|
|
229
|
+
inline: config.markdown.code.inline,
|
|
230
|
+
math: config.markdown.math,
|
|
231
|
+
})}) })`,
|
|
232
|
+
];
|
|
233
|
+
if (needsReact) {
|
|
234
|
+
integrations.push("react()");
|
|
235
|
+
}
|
|
236
|
+
if (needsVue) {
|
|
237
|
+
integrations.push("vue()");
|
|
238
|
+
}
|
|
239
|
+
if (needsSvelte) {
|
|
240
|
+
integrations.push("svelte()");
|
|
241
|
+
}
|
|
242
|
+
// Always mounted: injects user pages (a no-op when there are none) and wires
|
|
243
|
+
// up dev-server `Accept: text/markdown` negotiation over the content routes.
|
|
244
|
+
integrations.push(
|
|
245
|
+
`blumeIntegration(${JSON.stringify({ contentRoutes, pages })})`
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
return `// Generated by Blume. Do not edit; this file is recreated on each run.
|
|
249
|
+
${defineConfigImport}
|
|
250
|
+
import mdx from "@astrojs/mdx";
|
|
251
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
252
|
+
import { blumeMarkdownProcessor, blumeMdxProcessor, blumeShikiTransformers } from "blume/markdown";
|
|
253
|
+
${twoslashImport}${reactImport}${vueImport}${svelteImport}${blumeImport}${adapterImport}
|
|
254
|
+
export default defineConfig({
|
|
255
|
+
root: ${JSON.stringify(context.outDir)},
|
|
256
|
+
srcDir: ${JSON.stringify(`${context.outDir}/src`)},
|
|
257
|
+
outDir: ${JSON.stringify(`${context.root}/dist`)},
|
|
258
|
+
publicDir: ${JSON.stringify(`${context.root}/public`)},
|
|
259
|
+
output: ${JSON.stringify(deployment.output)},${adapterOption}${siteOption}${baseOption}${redirectsOption}${i18nOption}${fontsOption}
|
|
260
|
+
integrations: [${integrations.join(", ")}],
|
|
261
|
+
markdown: {
|
|
262
|
+
processor: blumeMarkdownProcessor(${JSON.stringify({
|
|
263
|
+
headingAnchors: config.markdown.headingAnchors,
|
|
264
|
+
inline: config.markdown.code.inline,
|
|
265
|
+
})}),
|
|
266
|
+
shikiConfig: {
|
|
267
|
+
themes: {
|
|
268
|
+
light: "github-light",
|
|
269
|
+
dark: "github-dark",
|
|
270
|
+
},
|
|
271
|
+
defaultColor: false,
|
|
272
|
+
transformers: [${twoslashTransformer}...blumeShikiTransformers(${JSON.stringify(
|
|
273
|
+
{ icons: config.markdown.code.icons }
|
|
274
|
+
)})],
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
devToolbar: { enabled: false },
|
|
278
|
+
vite: {
|
|
279
|
+
plugins: [tailwindcss()],
|
|
280
|
+
// @takumi-rs/core (OG image rendering) is a native NAPI addon that loads a
|
|
281
|
+
// platform-specific .node binding via createRequire(import.meta.url). Astro's
|
|
282
|
+
// build bundles it into the per-environment output by default, which
|
|
283
|
+
// relocates import.meta.url and breaks the binding lookup ("Cannot find
|
|
284
|
+
// native binding") on other platforms (e.g. the Linux CI runner). Astro 7
|
|
285
|
+
// configures externalization per Vite environment, so it must be forced
|
|
286
|
+
// external on the prerender (static) and ssr (server) environments -- a
|
|
287
|
+
// top-level ssr.external only reaches the latter -- so the binding resolves
|
|
288
|
+
// from node_modules at runtime instead.
|
|
289
|
+
environments: {
|
|
290
|
+
prerender: { resolve: { external: ["@takumi-rs/core"] } },
|
|
291
|
+
ssr: { resolve: { external: ["@takumi-rs/core"] } },
|
|
292
|
+
},
|
|
293
|
+
resolve: {
|
|
294
|
+
alias: {
|
|
295
|
+
"blume:data": ${JSON.stringify(dataPath)},
|
|
296
|
+
"blume:search-client": ${JSON.stringify(searchClientPath)},
|
|
297
|
+
"blume:theme": ${JSON.stringify(themePath)},
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
server: {
|
|
301
|
+
fs: {
|
|
302
|
+
allow: ${JSON.stringify(fsAllow)},
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
`;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
/** The default staged-content base, relative to the runtime `outDir`. */
|
|
311
|
+
export const stagedContentDir = (outDir: string): string =>
|
|
312
|
+
join(outDir, "content");
|
|
313
|
+
|
|
314
|
+
/** Generate `.blume/src/content.config.ts`. */
|
|
315
|
+
export const contentConfigTemplate = (options: {
|
|
316
|
+
context: ProjectContext;
|
|
317
|
+
config: ResolvedConfig;
|
|
318
|
+
/** Whether any non-filesystem source materialized MDX into the staged dir. */
|
|
319
|
+
staged?: boolean;
|
|
320
|
+
/** Base dir for the staged collection; defaults to `<outDir>/content`. */
|
|
321
|
+
stagedBase?: string;
|
|
322
|
+
}): string => {
|
|
323
|
+
const { context, config } = options;
|
|
324
|
+
const stagedBase = options.stagedBase ?? stagedContentDir(context.outDir);
|
|
325
|
+
|
|
326
|
+
// Fold the content excludes into the glob as negative patterns so the `docs`
|
|
327
|
+
// collection never walks into ignored trees. This matters when `content.root`
|
|
328
|
+
// is the project root (Mintlify bridge mode, or a migrated `.`-rooted project):
|
|
329
|
+
// without it the collection would scan `node_modules`, `snippets`, etc.
|
|
330
|
+
const docsPattern = [
|
|
331
|
+
...config.content.include,
|
|
332
|
+
...(config.content.exclude ?? []).map((pattern) => `!${pattern}`),
|
|
333
|
+
];
|
|
334
|
+
|
|
335
|
+
// Non-filesystem sources render through a parallel `staged` collection backed
|
|
336
|
+
// by materialized MDX, so the filesystem `docs` collection stays untouched.
|
|
337
|
+
const stagedBlock = options.staged
|
|
338
|
+
? `
|
|
339
|
+
const staged = defineCollection({
|
|
340
|
+
loader: glob({
|
|
341
|
+
pattern: ["**/*.{md,mdx}"],
|
|
342
|
+
base: ${JSON.stringify(stagedBase)},
|
|
343
|
+
generateId: ({ entry }) => entry,
|
|
344
|
+
}),
|
|
345
|
+
});
|
|
346
|
+
`
|
|
347
|
+
: "";
|
|
348
|
+
|
|
349
|
+
return `// Generated by Blume. Do not edit.
|
|
350
|
+
import { defineCollection } from "astro:content";
|
|
351
|
+
import { glob } from "astro/loaders";
|
|
352
|
+
|
|
353
|
+
const docs = defineCollection({
|
|
354
|
+
loader: glob({
|
|
355
|
+
pattern: ${JSON.stringify(docsPattern)},
|
|
356
|
+
base: ${JSON.stringify(context.contentRoot)},
|
|
357
|
+
generateId: ({ entry }) => entry,
|
|
358
|
+
}),
|
|
359
|
+
});
|
|
360
|
+
${stagedBlock}
|
|
361
|
+
export const collections = { docs${options.staged ? ", staged" : ""} };
|
|
362
|
+
`;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
/** Generate `.blume/src/pages/[...slug].astro`, the docs catch-all route. */
|
|
366
|
+
/** Generate the Ask AI server endpoint (`.blume/src/pages/api/ask.ts`). */
|
|
367
|
+
export const askEndpointTemplate = (backend: AskBackend): string => {
|
|
368
|
+
const imports = [
|
|
369
|
+
'import type { APIRoute } from "astro";',
|
|
370
|
+
'import { streamText } from "ai";',
|
|
371
|
+
];
|
|
372
|
+
let setup = "";
|
|
373
|
+
let modelExpr = JSON.stringify(backend.model);
|
|
374
|
+
if (backend.kind === "openrouter") {
|
|
375
|
+
imports.push(
|
|
376
|
+
'import { createOpenRouter } from "@openrouter/ai-sdk-provider";'
|
|
377
|
+
);
|
|
378
|
+
setup = `\nconst openrouter = createOpenRouter({ apiKey: process.env[${JSON.stringify(
|
|
379
|
+
backend.apiKeyEnv
|
|
380
|
+
)}] });\n`;
|
|
381
|
+
modelExpr = `openrouter(${JSON.stringify(backend.model)})`;
|
|
382
|
+
} else if (backend.kind === "openai-compatible") {
|
|
383
|
+
imports.push(
|
|
384
|
+
'import { createOpenAICompatible } from "@ai-sdk/openai-compatible";'
|
|
385
|
+
);
|
|
386
|
+
setup = `\nconst provider = createOpenAICompatible({
|
|
387
|
+
apiKey: process.env[${JSON.stringify(backend.apiKeyEnv)}],
|
|
388
|
+
baseURL: ${JSON.stringify(backend.baseUrl)},
|
|
389
|
+
name: ${JSON.stringify(backend.name)},
|
|
390
|
+
});\n`;
|
|
391
|
+
modelExpr = `provider(${JSON.stringify(backend.model)})`;
|
|
392
|
+
}
|
|
393
|
+
return `// Generated by Blume. Do not edit.
|
|
394
|
+
${imports.join("\n")}
|
|
395
|
+
|
|
396
|
+
export const prerender = false;
|
|
397
|
+
${setup}
|
|
398
|
+
export const POST: APIRoute = async ({ request }) => {
|
|
399
|
+
const { messages } = await request.json();
|
|
400
|
+
const result = streamText({
|
|
401
|
+
model: ${modelExpr},
|
|
402
|
+
system:
|
|
403
|
+
"You are a helpful documentation assistant. Answer using the project's documentation.",
|
|
404
|
+
messages,
|
|
405
|
+
});
|
|
406
|
+
return result.toTextStreamResponse();
|
|
407
|
+
};
|
|
408
|
+
`;
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
/** Generate the static search index endpoint (`/blume-search.json`). */
|
|
412
|
+
export const searchEndpointTemplate = (): string =>
|
|
413
|
+
`// Generated by Blume. Do not edit.
|
|
414
|
+
import documents from "../generated/search.json";
|
|
415
|
+
|
|
416
|
+
export const prerender = true;
|
|
417
|
+
|
|
418
|
+
export function GET() {
|
|
419
|
+
return new Response(JSON.stringify(documents), {
|
|
420
|
+
headers: { "Content-Type": "application/json" },
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
`;
|
|
424
|
+
|
|
425
|
+
const SEARCH_CLIENT_HEADER = "// Generated by Blume. Do not edit.\n";
|
|
426
|
+
|
|
427
|
+
/** Import the chosen provider's `createSearch` from the Blume package. */
|
|
428
|
+
const searchClientImport = (module: string): string =>
|
|
429
|
+
`import { createSearch as create } from "blume/components/layout/search/${module}.ts";\n`;
|
|
430
|
+
|
|
431
|
+
/** A client that loads a static `blume-search.json` index (Orama, FlexSearch). */
|
|
432
|
+
const staticSearchClient = (module: string): string =>
|
|
433
|
+
`${SEARCH_CLIENT_HEADER}${searchClientImport(module)}
|
|
434
|
+
const indexUrl = \`\${import.meta.env.BASE_URL}blume-search.json\`.replace("//", "/");
|
|
435
|
+
|
|
436
|
+
export const createSearch = () => create({ indexUrl });
|
|
437
|
+
`;
|
|
438
|
+
|
|
439
|
+
/** A client that passes public credentials straight to the provider SDK. */
|
|
440
|
+
const hostedSearchClient = (
|
|
441
|
+
module: string,
|
|
442
|
+
options: Record<string, unknown>
|
|
443
|
+
): string =>
|
|
444
|
+
`${SEARCH_CLIENT_HEADER}${searchClientImport(module)}
|
|
445
|
+
export const createSearch = () => create(${JSON.stringify(options)});
|
|
446
|
+
`;
|
|
447
|
+
|
|
448
|
+
/** Build the per-provider config object the hosted client is created with. */
|
|
449
|
+
const hostedSearchOptions = (
|
|
450
|
+
search: ResolvedConfig["search"]
|
|
451
|
+
): { module: string; options: Record<string, unknown> } | null => {
|
|
452
|
+
switch (search.provider) {
|
|
453
|
+
case "algolia": {
|
|
454
|
+
return { module: "algolia", options: { ...search.algolia } };
|
|
455
|
+
}
|
|
456
|
+
case "orama-cloud": {
|
|
457
|
+
return {
|
|
458
|
+
module: "orama-cloud",
|
|
459
|
+
options: {
|
|
460
|
+
apiKey: search.oramaCloud?.apiKey,
|
|
461
|
+
endpoint: search.oramaCloud?.endpoint,
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
case "typesense": {
|
|
466
|
+
return { module: "typesense", options: { ...search.typesense } };
|
|
467
|
+
}
|
|
468
|
+
default: {
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Generate `.blume/src/generated/search-client.ts` — the provider-specific
|
|
476
|
+
* loader the `<Search>` component lazy-imports via the `blume:search-client`
|
|
477
|
+
* alias. Only the configured provider's module (and therefore its SDK) is
|
|
478
|
+
* referenced, so the build bundles exactly one backend. Public credentials are
|
|
479
|
+
* baked in here; secret keys never reach the client.
|
|
480
|
+
*/
|
|
481
|
+
export const searchClientTemplate = (config: ResolvedConfig): string => {
|
|
482
|
+
const { search } = config;
|
|
483
|
+
|
|
484
|
+
if (search.provider === "orama" || search.provider === "flexsearch") {
|
|
485
|
+
return staticSearchClient(search.provider);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const hosted = hostedSearchOptions(search);
|
|
489
|
+
if (hosted) {
|
|
490
|
+
return hostedSearchClient(hosted.module, hosted.options);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (search.provider === "mixedbread") {
|
|
494
|
+
return `${SEARCH_CLIENT_HEADER}${searchClientImport("endpoint")}
|
|
495
|
+
const api = \`\${import.meta.env.BASE_URL}api/search\`.replace("//", "/");
|
|
496
|
+
|
|
497
|
+
export const createSearch = () => create({ api });
|
|
498
|
+
`;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (search.provider === "pagefind") {
|
|
502
|
+
return `${SEARCH_CLIENT_HEADER}${searchClientImport("pagefind")}
|
|
503
|
+
const url = \`\${import.meta.env.BASE_URL}pagefind/pagefind.js\`.replace("//", "/");
|
|
504
|
+
|
|
505
|
+
export const createSearch = () => create({ url });
|
|
506
|
+
`;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Search disabled: a no-op client so the alias always resolves.
|
|
510
|
+
return `${SEARCH_CLIENT_HEADER}export const createSearch = () => () =>
|
|
511
|
+
Promise.resolve({ hits: [], sections: [] });
|
|
512
|
+
`;
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Generate the Mixedbread search endpoint (`/api/search`). It holds the secret
|
|
517
|
+
* key server-side and proxies semantic queries to the configured store. The
|
|
518
|
+
* result mapping is best-effort and may need tuning to how your content was
|
|
519
|
+
* synced (see the Mixedbread sync step / \`mxbai vs sync\`).
|
|
520
|
+
*/
|
|
521
|
+
export const mixedbreadSearchEndpointTemplate = (storeId: string): string =>
|
|
522
|
+
`// Generated by Blume. Do not edit.
|
|
523
|
+
import type { APIRoute } from "astro";
|
|
524
|
+
import Mixedbread from "@mixedbread/sdk";
|
|
525
|
+
|
|
526
|
+
export const prerender = false;
|
|
527
|
+
|
|
528
|
+
const client = new Mixedbread({ apiKey: process.env.MIXEDBREAD_API_KEY ?? "" });
|
|
529
|
+
const STORE_ID = ${JSON.stringify(storeId)};
|
|
530
|
+
|
|
531
|
+
export const POST: APIRoute = async ({ request }) => {
|
|
532
|
+
const { query } = await request.json();
|
|
533
|
+
if (!query) {
|
|
534
|
+
return new Response("[]", {
|
|
535
|
+
headers: { "Content-Type": "application/json" },
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
const response = await client.stores.search({
|
|
539
|
+
query,
|
|
540
|
+
store_identifiers: [STORE_ID],
|
|
541
|
+
top_k: 8,
|
|
542
|
+
});
|
|
543
|
+
const hits = (response.data ?? []).map((chunk) => {
|
|
544
|
+
const meta = chunk.generated_metadata ?? {};
|
|
545
|
+
return {
|
|
546
|
+
excerpt: chunk.text ?? meta.excerpt ?? "",
|
|
547
|
+
title: meta.title ?? chunk.filename ?? "",
|
|
548
|
+
url: meta.url ?? "",
|
|
549
|
+
};
|
|
550
|
+
});
|
|
551
|
+
return new Response(JSON.stringify(hits), {
|
|
552
|
+
headers: { "Content-Type": "application/json" },
|
|
553
|
+
});
|
|
554
|
+
};
|
|
555
|
+
`;
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Generate the raw-Markdown endpoints (`[...slug].md.ts` and `[...slug].mdx.ts`).
|
|
559
|
+
* Each route's source is served verbatim so `/<route>.md` returns plain Markdown.
|
|
560
|
+
*/
|
|
561
|
+
export const rawMarkdownEndpointTemplate = (): string =>
|
|
562
|
+
`// Generated by Blume. Do not edit.
|
|
563
|
+
import raw from "../generated/raw-markdown.json";
|
|
564
|
+
|
|
565
|
+
export const prerender = true;
|
|
566
|
+
|
|
567
|
+
export function getStaticPaths() {
|
|
568
|
+
return Object.keys(raw).map((route) => ({
|
|
569
|
+
params: { slug: route === "/" ? "index" : route.slice(1) },
|
|
570
|
+
props: { route },
|
|
571
|
+
}));
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export function GET({ props }) {
|
|
575
|
+
return new Response(raw[props.route] ?? "", {
|
|
576
|
+
headers: { "Content-Type": "text/markdown; charset=utf-8" },
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
`;
|
|
580
|
+
|
|
581
|
+
/** The `src/pages` file that serves a route, e.g. `/mcp` -> `mcp.ts`. */
|
|
582
|
+
export const mcpPageFile = (route: string): string => {
|
|
583
|
+
const clean = route.replace(/^\/+/u, "").replace(/\/+$/u, "");
|
|
584
|
+
return `${clean}.ts`;
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Generate the hosted MCP server endpoint (e.g. `.blume/src/pages/mcp.ts`). A
|
|
589
|
+
* thin wrapper around the shipped `createMcpFetchHandler`, served from the
|
|
590
|
+
* generated data snapshot. Runs server-side (no prerender) so agents can query
|
|
591
|
+
* the docs over Streamable HTTP.
|
|
592
|
+
*/
|
|
593
|
+
export const mcpEndpointTemplate = (route: string): string => {
|
|
594
|
+
const clean = route.replace(/^\/+/u, "").replace(/\/+$/u, "");
|
|
595
|
+
const up = "../".repeat(clean.split("/").length);
|
|
596
|
+
return `// Generated by Blume. Do not edit.
|
|
597
|
+
import type { APIRoute } from "astro";
|
|
598
|
+
import { createMcpFetchHandler } from "blume/ai/mcp/server.ts";
|
|
599
|
+
import data from "${up}generated/mcp-data.json";
|
|
600
|
+
|
|
601
|
+
export const prerender = false;
|
|
602
|
+
|
|
603
|
+
const handler = createMcpFetchHandler(data);
|
|
604
|
+
|
|
605
|
+
export const ALL: APIRoute = ({ request }) => handler(request);
|
|
606
|
+
`;
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
/** Generate a prerendered endpoint that serves a fixed JSON payload. */
|
|
610
|
+
export const staticJsonEndpointTemplate = (payload: unknown): string =>
|
|
611
|
+
`// Generated by Blume. Do not edit.
|
|
612
|
+
export const prerender = true;
|
|
613
|
+
|
|
614
|
+
const payload = ${JSON.stringify(payload, null, 2)};
|
|
615
|
+
|
|
616
|
+
export function GET() {
|
|
617
|
+
return new Response(JSON.stringify(payload), {
|
|
618
|
+
headers: { "Content-Type": "application/json" },
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
`;
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Generate the RSS endpoint (`[section]/rss.xml.ts`). One feed per content
|
|
625
|
+
* type is served from the generated `rss.json`, e.g. `/blog/rss.xml`.
|
|
626
|
+
*/
|
|
627
|
+
export const rssEndpointTemplate = (): string =>
|
|
628
|
+
`// Generated by Blume. Do not edit.
|
|
629
|
+
import feeds from "../../generated/rss.json";
|
|
630
|
+
|
|
631
|
+
export const prerender = true;
|
|
632
|
+
|
|
633
|
+
export function getStaticPaths() {
|
|
634
|
+
return Object.keys(feeds).map((section) => ({
|
|
635
|
+
params: { section },
|
|
636
|
+
props: { section },
|
|
637
|
+
}));
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
export function GET({ props }) {
|
|
641
|
+
return new Response(feeds[props.section] ?? "", {
|
|
642
|
+
headers: { "Content-Type": "application/rss+xml; charset=utf-8" },
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
`;
|
|
646
|
+
|
|
647
|
+
/** Generate the OG image endpoint (`.blume/src/pages/_og/[...slug].png.ts`). */
|
|
648
|
+
export const ogEndpointTemplate = (): string =>
|
|
649
|
+
`// Generated by Blume. Do not edit.
|
|
650
|
+
import { renderOgImage } from "blume/og";
|
|
651
|
+
import data from "../../generated/data.json";
|
|
652
|
+
|
|
653
|
+
export const prerender = true;
|
|
654
|
+
|
|
655
|
+
export function getStaticPaths() {
|
|
656
|
+
return data.routes.map((route) => ({
|
|
657
|
+
params: { slug: route.path === "/" ? "index" : route.path.slice(1) },
|
|
658
|
+
props: { title: route.title },
|
|
659
|
+
}));
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export async function GET({ props }) {
|
|
663
|
+
const png = await renderOgImage({
|
|
664
|
+
title: props.title,
|
|
665
|
+
eyebrow: data.config.title,
|
|
666
|
+
accent: data.config.theme.accent,
|
|
667
|
+
});
|
|
668
|
+
return new Response(png, {
|
|
669
|
+
headers: {
|
|
670
|
+
"Cache-Control": "public, max-age=31536000, immutable",
|
|
671
|
+
"Content-Type": "image/png",
|
|
672
|
+
},
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
`;
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Generate a Scalar API/AsyncAPI reference page (`.blume/src/pages/<route>.astro`).
|
|
679
|
+
* The reference UI is owned by Scalar (its standalone bundle, loaded from a CDN)
|
|
680
|
+
* but mounted inside Blume's {@link ReferenceLayout} so the page keeps Blume's
|
|
681
|
+
* navbar on top. `renderMode: "client"` mounts the reference into a container
|
|
682
|
+
* element (rather than emitting a full HTML document), which is what lets it
|
|
683
|
+
* live inside our shell. `dataImport` is the route-depth-aware relative path to
|
|
684
|
+
* the generated data module the layout reads.
|
|
685
|
+
*/
|
|
686
|
+
export const scalarReferenceTemplate = (options: {
|
|
687
|
+
configuration: Record<string, unknown>;
|
|
688
|
+
dataImport: string;
|
|
689
|
+
route: string;
|
|
690
|
+
title: string;
|
|
691
|
+
}): string =>
|
|
692
|
+
`---
|
|
693
|
+
// Generated by Blume. Do not edit.
|
|
694
|
+
import { ScalarComponent } from "@scalar/astro";
|
|
695
|
+
import ReferenceLayout from "blume/components/layout/ReferenceLayout.astro";
|
|
696
|
+
import data from ${JSON.stringify(options.dataImport)};
|
|
697
|
+
|
|
698
|
+
export const prerender = true;
|
|
699
|
+
|
|
700
|
+
const configuration = ${JSON.stringify(options.configuration, null, 2)};
|
|
701
|
+
---
|
|
702
|
+
|
|
703
|
+
<ReferenceLayout
|
|
704
|
+
analytics={data.config.analytics}
|
|
705
|
+
banner={data.config.banner}
|
|
706
|
+
fontCssVars={data.fontCssVars}
|
|
707
|
+
logo={data.config.logo}
|
|
708
|
+
favicon={data.config.favicon}
|
|
709
|
+
navigation={data.navigation}
|
|
710
|
+
pageTitle={${JSON.stringify(options.title)}}
|
|
711
|
+
route={${JSON.stringify(options.route)}}
|
|
712
|
+
searchEnabled={data.config.search.enabled}
|
|
713
|
+
site={{ title: data.config.title, description: data.config.description }}
|
|
714
|
+
themeMode={data.config.theme.mode}
|
|
715
|
+
>
|
|
716
|
+
<ScalarComponent configuration={configuration} renderMode="client" />
|
|
717
|
+
</ReferenceLayout>
|
|
718
|
+
`;
|
|
719
|
+
|
|
720
|
+
export const catchAllPageTemplate = (options: {
|
|
721
|
+
askEnabled: boolean;
|
|
722
|
+
exportEpub: boolean;
|
|
723
|
+
exportPdf: boolean;
|
|
724
|
+
mathEnabled: boolean;
|
|
725
|
+
}): string => {
|
|
726
|
+
const askImport = options.askEnabled
|
|
727
|
+
? 'import AskAI from "blume/components/islands/AskAI.astro";\n'
|
|
728
|
+
: "";
|
|
729
|
+
const askSlot = options.askEnabled
|
|
730
|
+
? '\n <AskAI slot="ask" strings={ui.ask} />'
|
|
731
|
+
: "";
|
|
732
|
+
const mathImport = options.mathEnabled
|
|
733
|
+
? 'import Math from "blume/components/content/Math.astro";\n'
|
|
734
|
+
: "";
|
|
735
|
+
const mathEntry = options.mathEnabled ? "Math,\n " : "";
|
|
736
|
+
|
|
737
|
+
return `---
|
|
738
|
+
// Generated by Blume. Do not edit.
|
|
739
|
+
import { getEntry, render } from "astro:content";
|
|
740
|
+
import RootLayout from "blume/components/layout/RootLayout.astro";
|
|
741
|
+
${askImport}
|
|
742
|
+
import Accordion from "blume/components/content/Accordion.astro";
|
|
743
|
+
import AccordionItem from "blume/components/content/AccordionItem.astro";
|
|
744
|
+
import AutoTypeTable from "blume/components/content/AutoTypeTable.astro";
|
|
745
|
+
import Badge from "blume/components/content/Badge.astro";
|
|
746
|
+
import Callout from "blume/components/content/Callout.astro";
|
|
747
|
+
import Card from "blume/components/content/Card.astro";
|
|
748
|
+
import CardGroup from "blume/components/content/CardGroup.astro";
|
|
749
|
+
import CodeGroup from "blume/components/content/CodeGroup.astro";
|
|
750
|
+
import ColorRoot from "blume/components/content/Color.astro";
|
|
751
|
+
import ColorItem from "blume/components/content/ColorItem.astro";
|
|
752
|
+
import ColorRow from "blume/components/content/ColorRow.astro";
|
|
753
|
+
import Column from "blume/components/content/Column.astro";
|
|
754
|
+
import Columns from "blume/components/content/Columns.astro";
|
|
755
|
+
import Expandable from "blume/components/content/Expandable.astro";
|
|
756
|
+
import FileTree from "blume/components/content/FileTree.astro";
|
|
757
|
+
import Frame from "blume/components/content/Frame.astro";
|
|
758
|
+
import GithubInfo from "blume/components/content/GithubInfo.astro";
|
|
759
|
+
import Panel from "blume/components/content/Panel.astro";
|
|
760
|
+
import Prompt from "blume/components/content/Prompt.astro";
|
|
761
|
+
import Step from "blume/components/content/Step.astro";
|
|
762
|
+
import Steps from "blume/components/content/Steps.astro";
|
|
763
|
+
import Tab from "blume/components/content/Tab.astro";
|
|
764
|
+
import Tabs from "blume/components/content/Tabs.astro";
|
|
765
|
+
import Tile from "blume/components/content/Tile.astro";
|
|
766
|
+
import Tooltip from "blume/components/content/Tooltip.astro";
|
|
767
|
+
import TreeRoot from "blume/components/content/Tree.astro";
|
|
768
|
+
import TreeFile from "blume/components/content/TreeFile.astro";
|
|
769
|
+
import TreeFolder from "blume/components/content/TreeFolder.astro";
|
|
770
|
+
import TypeTable from "blume/components/content/TypeTable.astro";
|
|
771
|
+
import Visibility from "blume/components/content/Visibility.astro";
|
|
772
|
+
import Icon from "blume/components/Icon.astro";
|
|
773
|
+
${mathImport}import { mdxComponents as userMdx, layoutOverrides } from "../generated/components.ts";
|
|
774
|
+
import { islandComponents } from "../generated/islands.ts";
|
|
775
|
+
import data from "../generated/data.json";
|
|
776
|
+
|
|
777
|
+
const Color = Object.assign(ColorRoot, { Item: ColorItem, Row: ColorRow });
|
|
778
|
+
const Tree = Object.assign(TreeRoot, { File: TreeFile, Folder: TreeFolder });
|
|
779
|
+
|
|
780
|
+
// Docs content is file-based and always prerendered, even in server output
|
|
781
|
+
// (where only endpoints like /api/ask render on demand). Without this, server
|
|
782
|
+
// builds would render this route on demand and ignore getStaticPaths, leaving
|
|
783
|
+
// the entry id undefined.
|
|
784
|
+
export const prerender = true;
|
|
785
|
+
|
|
786
|
+
const components = {
|
|
787
|
+
Accordion,
|
|
788
|
+
AccordionItem,
|
|
789
|
+
AutoTypeTable,
|
|
790
|
+
Badge,
|
|
791
|
+
Callout,
|
|
792
|
+
Card,
|
|
793
|
+
CardGroup,
|
|
794
|
+
CodeGroup,
|
|
795
|
+
Color,
|
|
796
|
+
Column,
|
|
797
|
+
Columns,
|
|
798
|
+
Expandable,
|
|
799
|
+
FileTree,
|
|
800
|
+
Frame,
|
|
801
|
+
GithubInfo,
|
|
802
|
+
Icon,
|
|
803
|
+
Panel,
|
|
804
|
+
Prompt,
|
|
805
|
+
Step,
|
|
806
|
+
Steps,
|
|
807
|
+
Tab,
|
|
808
|
+
Tabs,
|
|
809
|
+
Tile,
|
|
810
|
+
Tooltip,
|
|
811
|
+
Tree,
|
|
812
|
+
TypeTable,
|
|
813
|
+
Visibility,
|
|
814
|
+
${mathEntry}...islandComponents,
|
|
815
|
+
...userMdx,
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
export function getStaticPaths() {
|
|
819
|
+
return data.routes.map((route) => ({
|
|
820
|
+
params: { slug: route.path === "/" ? undefined : route.path.slice(1) },
|
|
821
|
+
props: {
|
|
822
|
+
alternates: route.alternates,
|
|
823
|
+
collection: route.collection,
|
|
824
|
+
editUrl: route.editUrl,
|
|
825
|
+
entryId: route.entryId,
|
|
826
|
+
fallback: route.fallback,
|
|
827
|
+
indexable: route.indexable,
|
|
828
|
+
lastModified: route.lastModified,
|
|
829
|
+
locale: route.locale,
|
|
830
|
+
route: route.path,
|
|
831
|
+
title: route.title,
|
|
832
|
+
},
|
|
833
|
+
}));
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const { entryId, collection, route, title, indexable, editUrl, lastModified, locale, alternates, fallback } = Astro.props;
|
|
837
|
+
const entry = await getEntry(collection, entryId);
|
|
838
|
+
if (!entry) {
|
|
839
|
+
return new Response(null, { status: 404 });
|
|
840
|
+
}
|
|
841
|
+
const { Content, headings } = await render(entry);
|
|
842
|
+
const frontmatter = entry.data ?? {};
|
|
843
|
+
|
|
844
|
+
const seo = frontmatter.seo ?? {};
|
|
845
|
+
const base = data.config.site ? data.config.site.replace(/\\/$/, "") : null;
|
|
846
|
+
|
|
847
|
+
const ogPath = data.config.og.enabled
|
|
848
|
+
? \`/og/\${route === "/" ? "index" : route.slice(1)}.png\`
|
|
849
|
+
: null;
|
|
850
|
+
const ogRel = seo.image ?? ogPath;
|
|
851
|
+
const ogImage = ogRel && base ? \`\${base}\${ogRel}\` : ogRel;
|
|
852
|
+
|
|
853
|
+
const canonical =
|
|
854
|
+
seo.canonical ?? (base ? \`\${base}\${route === "/" ? "" : route}\` : null);
|
|
855
|
+
|
|
856
|
+
// Locale resolution. With i18n on, pick the active locale's nav + dictionary,
|
|
857
|
+
// build hreflang alternates, and derive the language-switcher targets.
|
|
858
|
+
const i18n = data.config.i18n;
|
|
859
|
+
const localePrefix = (codeArg) =>
|
|
860
|
+
i18n && codeArg === i18n.defaultLocale && i18n.hideDefaultLocalePrefix
|
|
861
|
+
? ""
|
|
862
|
+
: \`/\${codeArg}\`;
|
|
863
|
+
const localizeRoute = (logical, codeArg) => {
|
|
864
|
+
const prefix = localePrefix(codeArg);
|
|
865
|
+
if (!prefix) {
|
|
866
|
+
return logical;
|
|
867
|
+
}
|
|
868
|
+
return logical === "/" ? prefix : \`\${prefix}\${logical}\`;
|
|
869
|
+
};
|
|
870
|
+
const stripLocale = (path, codeArg) => {
|
|
871
|
+
const prefix = localePrefix(codeArg);
|
|
872
|
+
return prefix && path.startsWith(prefix) ? path.slice(prefix.length) || "/" : path;
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
const navigation = i18n ? (data.navigationByLocale[locale] ?? data.navigation) : data.navigation;
|
|
876
|
+
const ui = i18n ? (data.uiByLocale[locale] ?? data.ui) : data.ui;
|
|
877
|
+
const localeMeta = i18n ? i18n.locales.find((l) => l.code === locale) : null;
|
|
878
|
+
const dir = localeMeta?.dir ?? "ltr";
|
|
879
|
+
const htmlLang = i18n ? locale : "en";
|
|
880
|
+
// A fallback page renders the fallback locale's content, so its text direction
|
|
881
|
+
// follows that language — not the (mirrored) page locale.
|
|
882
|
+
const contentLocale =
|
|
883
|
+
fallback && i18n?.fallbackLocale ? i18n.fallbackLocale : locale;
|
|
884
|
+
const contentDir = i18n
|
|
885
|
+
? (i18n.locales.find((l) => l.code === contentLocale)?.dir ?? "ltr")
|
|
886
|
+
: "ltr";
|
|
887
|
+
const absolute = (path) => base + (path === "/" ? "" : path);
|
|
888
|
+
|
|
889
|
+
const localeAlternates =
|
|
890
|
+
i18n && base
|
|
891
|
+
? (alternates ?? []).map((alt) => ({ hreflang: alt.locale, href: absolute(alt.path) }))
|
|
892
|
+
: [];
|
|
893
|
+
const defaultAlt = i18n ? (alternates ?? []).find((alt) => alt.locale === i18n.defaultLocale) : null;
|
|
894
|
+
const xDefault = defaultAlt && base ? absolute(defaultAlt.path) : null;
|
|
895
|
+
|
|
896
|
+
const logicalRoute = i18n ? stripLocale(route, locale) : route;
|
|
897
|
+
const localeSwitch = i18n
|
|
898
|
+
? i18n.locales.map((l) => {
|
|
899
|
+
const alt = (alternates ?? []).find((x) => x.locale === l.code);
|
|
900
|
+
return {
|
|
901
|
+
code: l.code,
|
|
902
|
+
current: l.code === locale,
|
|
903
|
+
dir: l.dir,
|
|
904
|
+
href: alt ? alt.path : localizeRoute(logicalRoute, l.code),
|
|
905
|
+
label: l.label,
|
|
906
|
+
untranslated: !alt,
|
|
907
|
+
};
|
|
908
|
+
})
|
|
909
|
+
: [];
|
|
910
|
+
---
|
|
911
|
+
|
|
912
|
+
<RootLayout
|
|
913
|
+
site={{ title: data.config.title, description: data.config.description }}
|
|
914
|
+
layout={layoutOverrides}
|
|
915
|
+
logo={data.config.logo}
|
|
916
|
+
mcp={data.config.mcp}
|
|
917
|
+
favicon={data.config.favicon}
|
|
918
|
+
banner={data.config.banner}
|
|
919
|
+
analytics={data.config.analytics}
|
|
920
|
+
imageZoom={data.config.imageZoom}
|
|
921
|
+
codeWrap={data.config.codeWrap}
|
|
922
|
+
navigation={navigation}
|
|
923
|
+
locale={htmlLang}
|
|
924
|
+
dir={dir}
|
|
925
|
+
contentDir={contentDir}
|
|
926
|
+
ui={ui}
|
|
927
|
+
localeAlternates={localeAlternates}
|
|
928
|
+
xDefault={xDefault}
|
|
929
|
+
localeSwitch={localeSwitch}
|
|
930
|
+
page={{ title: seo.title ?? title, description: seo.description ?? frontmatter.description, route }}
|
|
931
|
+
headings={headings}
|
|
932
|
+
themeMode={data.config.theme.mode}
|
|
933
|
+
fontCssVars={data.fontCssVars}
|
|
934
|
+
searchEnabled={data.config.search.enabled}
|
|
935
|
+
indexable={indexable}
|
|
936
|
+
ogImage={ogImage}
|
|
937
|
+
canonical={canonical}
|
|
938
|
+
editUrl={editUrl}
|
|
939
|
+
feedback={data.config.feedback}
|
|
940
|
+
askEnabled={${options.askEnabled}}
|
|
941
|
+
exportPdf={${options.exportPdf}}
|
|
942
|
+
exportEpub={${options.exportEpub}}
|
|
943
|
+
feeds={data.feeds}
|
|
944
|
+
siteUrl={data.config.site}
|
|
945
|
+
pageType={frontmatter.type}
|
|
946
|
+
published={frontmatter.date ?? frontmatter.changelog?.date ?? null}
|
|
947
|
+
lastModified={lastModified}
|
|
948
|
+
noindex={seo.noindex}
|
|
949
|
+
structuredDataEnabled={data.config.structuredData}
|
|
950
|
+
>${askSlot}
|
|
951
|
+
<h1>{title}</h1>
|
|
952
|
+
{frontmatter.description && <p class="text-lg text-muted-foreground">{frontmatter.description}</p>}
|
|
953
|
+
<Content components={components} />
|
|
954
|
+
</RootLayout>
|
|
955
|
+
`;
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* Generate `.blume/src/pages/changelog.astro` — the changelog index. Collects
|
|
960
|
+
* every `type: changelog` entry, sorts newest-first, and renders each through
|
|
961
|
+
* the `Update` timeline layout (date/version rail + entry content). Only written
|
|
962
|
+
* by {@link generateAstroProject} when changelog entries exist.
|
|
963
|
+
*/
|
|
964
|
+
export const changelogIndexTemplate = (options: {
|
|
965
|
+
askEnabled: boolean;
|
|
966
|
+
exportEpub: boolean;
|
|
967
|
+
exportPdf: boolean;
|
|
968
|
+
}): string => {
|
|
969
|
+
const askImport = options.askEnabled
|
|
970
|
+
? 'import AskAI from "blume/components/islands/AskAI.astro";\n'
|
|
971
|
+
: "";
|
|
972
|
+
const askSlot = options.askEnabled ? '\n <AskAI slot="ask" />' : "";
|
|
973
|
+
|
|
974
|
+
return `---
|
|
975
|
+
// Generated by Blume. Do not edit.
|
|
976
|
+
import { getCollection, render } from "astro:content";
|
|
977
|
+
import RootLayout from "blume/components/layout/RootLayout.astro";
|
|
978
|
+
import Update from "blume/components/content/Update.astro";
|
|
979
|
+
import { layoutOverrides } from "../generated/components.ts";
|
|
980
|
+
${askImport}import data from "../generated/data.json";
|
|
981
|
+
|
|
982
|
+
export const prerender = true;
|
|
983
|
+
|
|
984
|
+
const entryDate = (entry) =>
|
|
985
|
+
entry.data.date ?? entry.data.changelog?.date ?? null;
|
|
986
|
+
|
|
987
|
+
const toTime = (value) => {
|
|
988
|
+
if (!value) {
|
|
989
|
+
return 0;
|
|
990
|
+
}
|
|
991
|
+
const date = new Date(value);
|
|
992
|
+
return Number.isNaN(date.getTime()) ? 0 : date.getTime();
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
const formatDate = (value) => {
|
|
996
|
+
if (!value) {
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
const date = new Date(value);
|
|
1000
|
+
return Number.isNaN(date.getTime())
|
|
1001
|
+
? undefined
|
|
1002
|
+
: new Intl.DateTimeFormat("en", {
|
|
1003
|
+
dateStyle: "long",
|
|
1004
|
+
timeZone: "UTC",
|
|
1005
|
+
}).format(date);
|
|
1006
|
+
};
|
|
1007
|
+
|
|
1008
|
+
const slugify = (text) =>
|
|
1009
|
+
text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") ||
|
|
1010
|
+
"update";
|
|
1011
|
+
|
|
1012
|
+
const changelogEntries = (await getCollection("docs"))
|
|
1013
|
+
.filter(
|
|
1014
|
+
(entry) =>
|
|
1015
|
+
entry.data.type === "changelog" &&
|
|
1016
|
+
!entry.data.draft &&
|
|
1017
|
+
!entry.data.sidebar?.hidden
|
|
1018
|
+
)
|
|
1019
|
+
.toSorted((a, b) => toTime(entryDate(b)) - toTime(entryDate(a)));
|
|
1020
|
+
|
|
1021
|
+
const items = await Promise.all(
|
|
1022
|
+
changelogEntries.map(async (entry) => {
|
|
1023
|
+
const label =
|
|
1024
|
+
entry.data.title ??
|
|
1025
|
+
(entry.data.changelog?.version
|
|
1026
|
+
? "v" + entry.data.changelog.version
|
|
1027
|
+
: "Update");
|
|
1028
|
+
return {
|
|
1029
|
+
Content: (await render(entry)).Content,
|
|
1030
|
+
date: formatDate(entryDate(entry)),
|
|
1031
|
+
id: slugify(label),
|
|
1032
|
+
label,
|
|
1033
|
+
tags: entry.data.changelog?.category
|
|
1034
|
+
? [entry.data.changelog.category]
|
|
1035
|
+
: [],
|
|
1036
|
+
};
|
|
1037
|
+
})
|
|
1038
|
+
);
|
|
1039
|
+
|
|
1040
|
+
const headings = items.map((item) => ({
|
|
1041
|
+
depth: 2,
|
|
1042
|
+
slug: item.id,
|
|
1043
|
+
text: item.label,
|
|
1044
|
+
}));
|
|
1045
|
+
|
|
1046
|
+
const base = data.config.site ? data.config.site.replace(/\\/$/, "") : null;
|
|
1047
|
+
const canonical = base ? base + "/changelog" : null;
|
|
1048
|
+
---
|
|
1049
|
+
|
|
1050
|
+
<RootLayout
|
|
1051
|
+
site={{ title: data.config.title, description: data.config.description }}
|
|
1052
|
+
layout={layoutOverrides}
|
|
1053
|
+
logo={data.config.logo}
|
|
1054
|
+
mcp={data.config.mcp}
|
|
1055
|
+
favicon={data.config.favicon}
|
|
1056
|
+
banner={data.config.banner}
|
|
1057
|
+
analytics={data.config.analytics}
|
|
1058
|
+
imageZoom={data.config.imageZoom}
|
|
1059
|
+
codeWrap={data.config.codeWrap}
|
|
1060
|
+
navigation={data.navigation}
|
|
1061
|
+
page={{
|
|
1062
|
+
title: data.config.title + " changelog",
|
|
1063
|
+
description: "Product updates and release notes.",
|
|
1064
|
+
route: "/changelog",
|
|
1065
|
+
}}
|
|
1066
|
+
headings={headings}
|
|
1067
|
+
themeMode={data.config.theme.mode}
|
|
1068
|
+
fontCssVars={data.fontCssVars}
|
|
1069
|
+
searchEnabled={data.config.search.enabled}
|
|
1070
|
+
indexable={true}
|
|
1071
|
+
ogImage={null}
|
|
1072
|
+
canonical={canonical}
|
|
1073
|
+
askEnabled={${options.askEnabled}}
|
|
1074
|
+
exportPdf={${options.exportPdf}}
|
|
1075
|
+
exportEpub={${options.exportEpub}}
|
|
1076
|
+
feeds={data.feeds}
|
|
1077
|
+
siteUrl={data.config.site}
|
|
1078
|
+
noindex={false}
|
|
1079
|
+
structuredDataEnabled={data.config.structuredData}
|
|
1080
|
+
>${askSlot}
|
|
1081
|
+
<h1>Changelog</h1>
|
|
1082
|
+
{
|
|
1083
|
+
items.length === 0 ? (
|
|
1084
|
+
<p>No changelog entries yet.</p>
|
|
1085
|
+
) : (
|
|
1086
|
+
<div class="not-prose mt-8">
|
|
1087
|
+
{items.map(({ Content, id, label, date, tags }) => (
|
|
1088
|
+
<Update description={date} id={id} label={label} tags={tags}>
|
|
1089
|
+
<Content />
|
|
1090
|
+
</Update>
|
|
1091
|
+
))}
|
|
1092
|
+
</div>
|
|
1093
|
+
)
|
|
1094
|
+
}
|
|
1095
|
+
</RootLayout>
|
|
1096
|
+
`;
|
|
1097
|
+
};
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* Generate `.blume/src/generated/components.ts`, which re-exports the user's
|
|
1101
|
+
* component overrides (or empty maps when no `components.ts` exists). Importing
|
|
1102
|
+
* the user file here lets Astro/Vite compile any `.astro`/`.tsx` it references.
|
|
1103
|
+
*/
|
|
1104
|
+
export const userComponentsTemplate = (
|
|
1105
|
+
componentsFile: string | null
|
|
1106
|
+
): string => {
|
|
1107
|
+
if (!componentsFile) {
|
|
1108
|
+
return `// Generated by Blume. Do not edit.
|
|
1109
|
+
export const mdxComponents = {};
|
|
1110
|
+
export const layoutOverrides = {};
|
|
1111
|
+
`;
|
|
1112
|
+
}
|
|
1113
|
+
return `// Generated by Blume. Do not edit.
|
|
1114
|
+
import overrides from ${JSON.stringify(componentsFile)};
|
|
1115
|
+
export const mdxComponents = overrides.mdx ?? {};
|
|
1116
|
+
export const layoutOverrides = overrides.layout ?? {};
|
|
1117
|
+
`;
|
|
1118
|
+
};
|
|
1119
|
+
|
|
1120
|
+
/** The literal Astro hydration directive for an island's client mode. */
|
|
1121
|
+
const islandDirective = (spec: IslandSpec): string =>
|
|
1122
|
+
spec.client === "only"
|
|
1123
|
+
? `client:only="${spec.framework}"`
|
|
1124
|
+
: `client:${spec.client}`;
|
|
1125
|
+
|
|
1126
|
+
/**
|
|
1127
|
+
* Generate `.blume/src/generated/islands/<Name>.astro` — a wrapper that renders
|
|
1128
|
+
* a convention island with its hydration directive applied. Astro client
|
|
1129
|
+
* directives must be written statically, so one wrapper is emitted per island;
|
|
1130
|
+
* props and the default slot (MDX children) forward through.
|
|
1131
|
+
*/
|
|
1132
|
+
export const islandWrapperTemplate = (spec: IslandSpec): string =>
|
|
1133
|
+
`---
|
|
1134
|
+
// Generated by Blume. Do not edit.
|
|
1135
|
+
import Island from ${JSON.stringify(spec.file)};
|
|
1136
|
+
---
|
|
1137
|
+
<Island ${islandDirective(spec)} {...Astro.props}><slot /></Island>
|
|
1138
|
+
`;
|
|
1139
|
+
|
|
1140
|
+
/**
|
|
1141
|
+
* Generate `.blume/src/generated/islands.ts` — the map of island names to their
|
|
1142
|
+
* wrappers, spread into the MDX component scope by the catch-all page. Always
|
|
1143
|
+
* written (an empty map when there are no islands) so the import resolves.
|
|
1144
|
+
*/
|
|
1145
|
+
export const islandMapTemplate = (specs: IslandSpec[]): string => {
|
|
1146
|
+
if (specs.length === 0) {
|
|
1147
|
+
return `// Generated by Blume. Do not edit.
|
|
1148
|
+
export const islandComponents = {};
|
|
1149
|
+
`;
|
|
1150
|
+
}
|
|
1151
|
+
const imports = specs
|
|
1152
|
+
.map(
|
|
1153
|
+
(spec, index) => `import I${index} from "./islands/${spec.name}.astro";`
|
|
1154
|
+
)
|
|
1155
|
+
.join("\n");
|
|
1156
|
+
const entries = specs
|
|
1157
|
+
.map((spec, index) => ` ${spec.name}: I${index},`)
|
|
1158
|
+
.join("\n");
|
|
1159
|
+
return `// Generated by Blume. Do not edit.
|
|
1160
|
+
${imports}
|
|
1161
|
+
export const islandComponents = {
|
|
1162
|
+
${entries}
|
|
1163
|
+
};
|
|
1164
|
+
`;
|
|
1165
|
+
};
|
|
1166
|
+
|
|
1167
|
+
/** Generate `.blume/src/env.d.ts`. */
|
|
1168
|
+
export const envTemplate =
|
|
1169
|
+
(): string => `/// <reference path="../.astro/types.d.ts" />
|
|
1170
|
+
/// <reference types="astro/client" />
|
|
1171
|
+
`;
|
|
1172
|
+
|
|
1173
|
+
/** Generate `.blume/package.json`. */
|
|
1174
|
+
export const runtimePackageTemplate = (dependencies: string[] = []): string =>
|
|
1175
|
+
`${JSON.stringify(
|
|
1176
|
+
{
|
|
1177
|
+
dependencies: Object.fromEntries(
|
|
1178
|
+
[...dependencies].toSorted().map((name) => [name, "*"])
|
|
1179
|
+
),
|
|
1180
|
+
name: "blume-runtime",
|
|
1181
|
+
private: true,
|
|
1182
|
+
type: "module",
|
|
1183
|
+
version: "0.0.0",
|
|
1184
|
+
},
|
|
1185
|
+
null,
|
|
1186
|
+
2
|
|
1187
|
+
)}\n`;
|
|
1188
|
+
|
|
1189
|
+
/** Generate `.blume/tsconfig.json`. */
|
|
1190
|
+
export const runtimeTsconfigTemplate = (): string =>
|
|
1191
|
+
`${JSON.stringify(
|
|
1192
|
+
{
|
|
1193
|
+
exclude: ["dist"],
|
|
1194
|
+
extends: "astro/tsconfigs/strict",
|
|
1195
|
+
include: [".astro/types.d.ts", "**/*"],
|
|
1196
|
+
},
|
|
1197
|
+
null,
|
|
1198
|
+
2
|
|
1199
|
+
)}\n`;
|