blume 0.6.7 → 0.8.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/CHANGELOG.md +618 -0
- package/LICENSE +21 -0
- package/README.md +107 -0
- package/dist/cli/index.js +2609 -1041
- package/dist/cli/index.js.map +110 -103
- package/dist/types/ai/component-markdown.d.ts +34 -0
- package/dist/types/components/content/youtube.d.ts +18 -0
- package/dist/types/core/base-path.d.ts +47 -0
- package/dist/types/core/config-input.d.ts +110 -12
- package/dist/types/core/config.d.ts +6 -4
- package/dist/types/core/data.d.ts +4 -0
- package/dist/types/core/i18n-ui.d.ts +477 -135
- package/dist/types/core/schema.d.ts +309 -195
- package/dist/types/core/sources/types.d.ts +2 -0
- package/dist/types/core/types.d.ts +6 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/openapi/references.d.ts +60 -0
- package/docs/01-quickstart.mdx +5 -2
- package/docs/02-deployment.mdx +24 -9
- package/docs/03-faq.mdx +46 -16
- package/docs/advanced/custom-pages.mdx +1 -1
- package/docs/advanced/skills.mdx +1 -1
- package/docs/configuration/ai.mdx +49 -10
- package/docs/configuration/customization.mdx +11 -0
- package/docs/configuration/index.mdx +33 -3
- package/docs/configuration/seo.mdx +2 -2
- package/docs/content/components.mdx +30 -3
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/islands.mdx +8 -0
- package/docs/content/navigation.mdx +3 -3
- package/docs/content/sources.mdx +1 -1
- package/docs/content/syntax.mdx +17 -2
- package/docs/index.mdx +2 -2
- package/docs/reference/cli.mdx +8 -6
- package/package.json +15 -4
- package/skills/blume/SKILL.md +5 -3
- package/skills/blume-update-docs/SKILL.md +3 -2
- package/src/ai/agent-readability.ts +11 -5
- package/src/ai/ask-context.ts +7 -2
- package/src/ai/ask-data.ts +3 -0
- package/src/ai/ask.ts +12 -7
- package/src/ai/component-markdown.ts +461 -0
- package/src/ai/llms.ts +143 -23
- package/src/ai/markdown.ts +35 -6
- package/src/ai/mcp/data.ts +33 -8
- package/src/ai/mcp/discovery.ts +10 -3
- package/src/ai/mcp/server.ts +24 -7
- package/src/ai/visibility.ts +74 -0
- package/src/astro/component-slots.ts +16 -4
- package/src/astro/examples.ts +12 -7
- package/src/astro/generate.ts +393 -189
- package/src/astro/index.ts +5 -1
- package/src/astro/integration.ts +9 -5
- package/src/astro/islands.ts +11 -5
- package/src/astro/markdown-negotiation.ts +2 -2
- package/src/astro/pages.ts +89 -22
- package/src/astro/templates.ts +259 -25
- package/src/blume-modules.d.ts +8 -0
- package/src/cli/commands/build.ts +131 -38
- package/src/cli/commands/check.ts +1 -1
- package/src/cli/commands/dev.ts +71 -17
- package/src/cli/commands/doctor.ts +2 -2
- package/src/cli/commands/eject.ts +47 -19
- package/src/cli/commands/init.ts +120 -180
- package/src/cli/commands/preview.ts +4 -1
- package/src/cli/commands/validate.ts +44 -2
- package/src/cli/dev-lock.ts +34 -19
- package/src/cli/eject-scripts.ts +72 -0
- package/src/cli/env.ts +15 -5
- package/src/cli/init/questions.ts +158 -0
- package/src/cli/init/scaffold.ts +380 -0
- package/src/cli/required-secrets.ts +2 -1
- package/src/components/content/AccordionItem.astro +23 -4
- package/src/components/content/Badge.astro +3 -1
- package/src/components/content/Card.astro +4 -2
- package/src/components/content/CodeBlock.astro +3 -0
- package/src/components/content/Component.astro +30 -16
- package/src/components/content/Diff.astro +3 -1
- package/src/components/content/Step.astro +10 -1
- package/src/components/content/Tabs.astro +15 -3
- package/src/components/content/Tile.astro +2 -1
- package/src/components/content/Tooltip.astro +3 -1
- package/src/components/content/Update.astro +9 -2
- package/src/components/content/auto-type-table.ts +25 -9
- package/src/components/content/base-href.ts +33 -0
- package/src/components/content/changelog-element.ts +9 -2
- package/src/components/content/diff.ts +12 -6
- package/src/components/content/mermaid-element.ts +10 -2
- package/src/components/index.ts +23 -1
- package/src/components/islands/AskAI.astro +5 -2
- package/src/components/islands/ask-ai.tsx +68 -12
- package/src/components/islands/base-path.ts +28 -0
- package/src/components/islands/hooks.ts +44 -9
- package/src/components/layout/Banner.astro +12 -3
- package/src/components/layout/Breadcrumbs.astro +2 -1
- package/src/components/layout/Favicon.astro +3 -2
- package/src/components/layout/Header.astro +15 -5
- package/src/components/layout/LanguageSwitcher.astro +2 -1
- package/src/components/layout/Logo.astro +13 -4
- package/src/components/layout/NavSelector.astro +2 -1
- package/src/components/layout/NavTree.astro +22 -7
- package/src/components/layout/PageActions.astro +25 -10
- package/src/components/layout/PageFeedback.astro +4 -1
- package/src/components/layout/PageLayout.astro +51 -9
- package/src/components/layout/Pagination.astro +3 -2
- package/src/components/layout/ReferenceLayout.astro +8 -1
- package/src/components/layout/RootLayout.astro +74 -13
- package/src/components/layout/Search.astro +107 -27
- package/src/components/layout/nav-utils.ts +18 -10
- package/src/components/layout/search/algolia.ts +11 -2
- package/src/components/layout/search/endpoint.ts +11 -5
- package/src/components/layout/search/orama-cloud.ts +8 -2
- package/src/components/layout/search/pagefind.ts +3 -0
- package/src/components/layout/search/types.ts +5 -1
- package/src/components/layout/search/typesense.ts +4 -1
- package/src/components/layout/toc-element.ts +8 -2
- package/src/components/openapi/ApiTagOperations.astro +2 -1
- package/src/components/openapi/Operation.astro +47 -40
- package/src/components/openapi/RequestPanel.astro +8 -2
- package/src/components/openapi/helpers.ts +71 -3
- package/src/components/openapi/panel.ts +1 -1
- package/src/components/openapi/snippets.ts +25 -11
- package/src/core/base-path.ts +94 -0
- package/src/core/builtin-tags.ts +2 -0
- package/src/core/component-overrides.ts +103 -74
- package/src/core/config-input.ts +118 -17
- package/src/core/config.ts +8 -5
- package/src/core/content.ts +2 -0
- package/src/core/data.ts +4 -0
- package/src/core/diagnostics.ts +54 -34
- package/src/core/gitignore.ts +4 -1
- package/src/core/graph.ts +166 -88
- package/src/core/i18n-ui.ts +63 -3
- package/src/core/last-modified.ts +15 -6
- package/src/core/links.ts +69 -25
- package/src/core/manifest.ts +62 -45
- package/src/core/nav-diagnostics.ts +1 -1
- package/src/core/navigation.ts +144 -58
- package/src/core/package-json.ts +17 -2
- package/src/core/project-graph.ts +25 -15
- package/src/core/schema.ts +605 -620
- package/src/core/sources/assets.ts +6 -1
- package/src/core/sources/filesystem.ts +4 -0
- package/src/core/sources/github-releases.ts +2 -1
- package/src/core/sources/mdx-remote.ts +76 -63
- package/src/core/sources/normalize.ts +236 -91
- package/src/core/sources/notion.ts +27 -18
- package/src/core/sources/types.ts +2 -0
- package/src/core/tsconfig-aliases.ts +59 -30
- package/src/core/types.ts +6 -1
- package/src/core/ui-packs/ar.ts +1 -0
- package/src/core/ui-packs/bg.ts +1 -0
- package/src/core/ui-packs/bn.ts +1 -0
- package/src/core/ui-packs/ca.ts +1 -0
- package/src/core/ui-packs/cs.ts +1 -0
- package/src/core/ui-packs/da.ts +1 -0
- package/src/core/ui-packs/de.ts +1 -0
- package/src/core/ui-packs/el.ts +1 -0
- package/src/core/ui-packs/es.ts +1 -0
- package/src/core/ui-packs/fa.ts +1 -0
- package/src/core/ui-packs/fi.ts +1 -0
- package/src/core/ui-packs/fr.ts +2 -1
- package/src/core/ui-packs/he.ts +1 -0
- package/src/core/ui-packs/hi.ts +1 -0
- package/src/core/ui-packs/hr.ts +1 -0
- package/src/core/ui-packs/hu.ts +1 -0
- package/src/core/ui-packs/id.ts +1 -0
- package/src/core/ui-packs/it.ts +1 -0
- package/src/core/ui-packs/ja.ts +1 -0
- package/src/core/ui-packs/ko.ts +1 -0
- package/src/core/ui-packs/nl.ts +1 -0
- package/src/core/ui-packs/no.ts +1 -0
- package/src/core/ui-packs/pl.ts +1 -0
- package/src/core/ui-packs/pt-br.ts +1 -0
- package/src/core/ui-packs/pt.ts +1 -0
- package/src/core/ui-packs/ro.ts +1 -0
- package/src/core/ui-packs/ru.ts +1 -0
- package/src/core/ui-packs/sk.ts +1 -0
- package/src/core/ui-packs/sr.ts +1 -0
- package/src/core/ui-packs/sv.ts +1 -0
- package/src/core/ui-packs/th.ts +1 -0
- package/src/core/ui-packs/tr.ts +1 -0
- package/src/core/ui-packs/uk.ts +1 -0
- package/src/core/ui-packs/vi.ts +1 -0
- package/src/core/ui-packs/zh-tw.ts +1 -0
- package/src/core/ui-packs/zh.ts +1 -0
- package/src/deploy/adapter-output.ts +18 -8
- package/src/deploy/redirects.ts +25 -2
- package/src/deploy/robots.ts +6 -1
- package/src/deploy/rss.ts +10 -3
- package/src/deploy/sitemap.ts +59 -13
- package/src/index.ts +5 -0
- package/src/markdown/base-links.ts +60 -0
- package/src/markdown/code-title.ts +11 -14
- package/src/markdown/index.ts +46 -9
- package/src/markdown/inline-code.ts +14 -4
- package/src/markdown/package-commands.ts +10 -4
- package/src/markdown/themes.ts +24 -0
- package/src/openapi/model.ts +15 -5
- package/src/openapi/parse.ts +21 -0
- package/src/openapi/references.ts +75 -21
- package/src/openapi/render-mdx.ts +11 -6
- package/src/openapi/scalar.ts +32 -16
- package/src/openapi/source.ts +59 -10
- package/src/registry/eject.ts +247 -19
- package/src/registry/registry.ts +0 -3
- package/src/search/build.ts +3 -0
- package/src/search/documents.ts +36 -4
- package/src/search/sync/typesense.ts +6 -4
- package/src/seo/jsonld.ts +28 -17
- package/src/theme/entry.ts +85 -20
package/src/astro/templates.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
2
3
|
|
|
3
4
|
import { dirname, isAbsolute, join, relative } from "pathe";
|
|
4
5
|
|
|
5
6
|
import { askBackendRuntimeDep } from "../ai/ask.ts";
|
|
6
7
|
import type { AskBackend } from "../ai/ask.ts";
|
|
8
|
+
import { normalizeBasePath } from "../core/base-path.ts";
|
|
7
9
|
import type { ResolvedConfig } from "../core/schema.ts";
|
|
8
10
|
import { BLUME_IGNORE_DIRS } from "../core/sources/watch.ts";
|
|
9
11
|
import type { ProjectContext } from "../core/types.ts";
|
|
12
|
+
import { applyBaseToRedirects } from "../deploy/redirects.ts";
|
|
10
13
|
import { hasScalarReferences } from "../openapi/references.ts";
|
|
11
14
|
import { searchProviderMeta } from "../search/providers.ts";
|
|
12
15
|
import { buildFontEntries } from "../theme/fonts.ts";
|
|
@@ -184,6 +187,19 @@ const renderUserAliases = (
|
|
|
184
187
|
const astroOutDir = (context: ProjectContext): string =>
|
|
185
188
|
context.distDir ?? `${context.root}/dist`;
|
|
186
189
|
|
|
190
|
+
/**
|
|
191
|
+
* The `react()` integration call. When `compilerPath` is set (the resolved
|
|
192
|
+
* absolute path to `babel-plugin-react-compiler`), react() carries the compiler
|
|
193
|
+
* as the first babel plugin — an absolute path, because @vitejs/plugin-react
|
|
194
|
+
* resolves babel plugins from the *project* root, not `.blume/`, so a bare
|
|
195
|
+
* specifier wouldn't resolve in a user project. `target: "19"` matches Blume's
|
|
196
|
+
* React pin. `null`/`undefined` (compiler off or unresolvable) emits bare react().
|
|
197
|
+
*/
|
|
198
|
+
const reactIntegration = (compilerPath: string | null | undefined): string =>
|
|
199
|
+
compilerPath
|
|
200
|
+
? `react({ babel: { plugins: [[${JSON.stringify(compilerPath)}, { target: "19" }]] } })`
|
|
201
|
+
: "react()";
|
|
202
|
+
|
|
187
203
|
export const astroConfigTemplate = (options: {
|
|
188
204
|
context: ProjectContext;
|
|
189
205
|
config: ResolvedConfig;
|
|
@@ -194,9 +210,17 @@ export const astroConfigTemplate = (options: {
|
|
|
194
210
|
contentRoutes: string[];
|
|
195
211
|
dataPath: string;
|
|
196
212
|
examplesPath: string;
|
|
213
|
+
/** The example-preview Tailwind entry (`blume:examples-theme`). */
|
|
214
|
+
examplesThemePath: string;
|
|
197
215
|
themePath: string;
|
|
198
216
|
searchClientPath: string;
|
|
199
217
|
openapiPath: string;
|
|
218
|
+
/**
|
|
219
|
+
* Absolute path to `babel-plugin-react-compiler` when the React Compiler is
|
|
220
|
+
* enabled (resolved from Blume's package root by the caller); null/absent
|
|
221
|
+
* disables the compiler and emits a bare `react()`.
|
|
222
|
+
*/
|
|
223
|
+
reactCompilerPath?: string | null;
|
|
200
224
|
/** Project tsconfig path aliases (`find` -> absolute dir), e.g. `@` -> src. */
|
|
201
225
|
aliases?: Record<string, string>;
|
|
202
226
|
}): string => {
|
|
@@ -204,6 +228,7 @@ export const astroConfigTemplate = (options: {
|
|
|
204
228
|
const {
|
|
205
229
|
contentRoutes,
|
|
206
230
|
examplesPath,
|
|
231
|
+
examplesThemePath,
|
|
207
232
|
needsSvelte,
|
|
208
233
|
needsVue,
|
|
209
234
|
openapiPath,
|
|
@@ -248,11 +273,17 @@ export const astroConfigTemplate = (options: {
|
|
|
248
273
|
})},`
|
|
249
274
|
: "";
|
|
250
275
|
|
|
276
|
+
// Base the redirect paths the same way routes are based, so a redirect lands
|
|
277
|
+
// under `basePath` too. Astro layers its own `base` (deployment.base) on top.
|
|
278
|
+
const basedRedirects = applyBaseToRedirects(
|
|
279
|
+
config.redirects,
|
|
280
|
+
config.basePath
|
|
281
|
+
);
|
|
251
282
|
const redirectsOption =
|
|
252
|
-
|
|
283
|
+
basedRedirects.length > 0
|
|
253
284
|
? `\n redirects: ${JSON.stringify(
|
|
254
285
|
Object.fromEntries(
|
|
255
|
-
|
|
286
|
+
basedRedirects.map((redirect) => [
|
|
256
287
|
redirect.from,
|
|
257
288
|
{ destination: redirect.to, status: redirect.status },
|
|
258
289
|
])
|
|
@@ -288,7 +319,7 @@ export const astroConfigTemplate = (options: {
|
|
|
288
319
|
const svelteImport = needsSvelte
|
|
289
320
|
? `import svelte from "@astrojs/svelte";\n`
|
|
290
321
|
: "";
|
|
291
|
-
const blumeImport = `import { blumeIntegration, prerenderDepsPlugin } from "blume/astro";\n`;
|
|
322
|
+
const blumeImport = `import { blumeIntegration, prerenderDepsPlugin, serverAppResolvePlugin } from "blume/astro";\n`;
|
|
292
323
|
|
|
293
324
|
// Twoslash runs first, before the always-on transformers, but only on fences
|
|
294
325
|
// with the `twoslash` meta (explicitTrigger) — so it's opt-in per block with
|
|
@@ -297,13 +328,24 @@ export const astroConfigTemplate = (options: {
|
|
|
297
328
|
const twoslashTransformer =
|
|
298
329
|
"transformerTwoslash({ explicitTrigger: true }), ";
|
|
299
330
|
|
|
331
|
+
// Content links are rewritten to their real served URL: the `deployment.base`
|
|
332
|
+
// subdirectory (Astro doesn't rewrite `<a href>`) layered over the site-wide
|
|
333
|
+
// `basePath` baked into routes. The layers are passed separately so a
|
|
334
|
+
// hand-written `basePath` link (`/docs/x`) isn't double-prefixed (see
|
|
335
|
+
// `withComposedBasePath`). The link checker validates the base-less authored
|
|
336
|
+
// path against `basePath` routes separately.
|
|
337
|
+
const deployBase = normalizeBasePath(deployment.base);
|
|
338
|
+
|
|
300
339
|
const integrations = [
|
|
301
340
|
`mdx({ processor: blumeMdxProcessor(${JSON.stringify({
|
|
341
|
+
basePath: config.basePath,
|
|
342
|
+
codeThemes: config.markdown.codeBlocks.theme,
|
|
343
|
+
deployBase,
|
|
302
344
|
headingAnchors: config.markdown.headingAnchors,
|
|
303
345
|
})}) })`,
|
|
304
346
|
];
|
|
305
347
|
if (needsReact) {
|
|
306
|
-
integrations.push(
|
|
348
|
+
integrations.push(reactIntegration(options.reactCompilerPath));
|
|
307
349
|
}
|
|
308
350
|
if (needsVue) {
|
|
309
351
|
integrations.push("vue()");
|
|
@@ -332,12 +374,15 @@ export default defineConfig({
|
|
|
332
374
|
integrations: [${integrations.join(", ")}],
|
|
333
375
|
markdown: {
|
|
334
376
|
processor: blumeMarkdownProcessor(${JSON.stringify({
|
|
377
|
+
basePath: config.basePath,
|
|
378
|
+
codeThemes: config.markdown.codeBlocks.theme,
|
|
379
|
+
deployBase,
|
|
335
380
|
headingAnchors: config.markdown.headingAnchors,
|
|
336
381
|
})}),
|
|
337
382
|
shikiConfig: {
|
|
338
383
|
themes: {
|
|
339
|
-
light:
|
|
340
|
-
dark:
|
|
384
|
+
light: ${JSON.stringify(config.markdown.codeBlocks.theme.light)},
|
|
385
|
+
dark: ${JSON.stringify(config.markdown.codeBlocks.theme.dark)},
|
|
341
386
|
},
|
|
342
387
|
defaultColor: false,
|
|
343
388
|
transformers: [${twoslashTransformer}...blumeShikiTransformers(${JSON.stringify(
|
|
@@ -347,7 +392,7 @@ export default defineConfig({
|
|
|
347
392
|
},
|
|
348
393
|
devToolbar: { enabled: false },
|
|
349
394
|
vite: {
|
|
350
|
-
plugins: [tailwindcss(), prerenderDepsPlugin()],
|
|
395
|
+
plugins: [tailwindcss(), prerenderDepsPlugin(), serverAppResolvePlugin()],
|
|
351
396
|
// Blume's render-time deps are forced external on both build environments so
|
|
352
397
|
// native bindings resolve at runtime and isolated linkers don't bundle
|
|
353
398
|
// symlinked store copies (which would surface their children as unresolvable
|
|
@@ -368,6 +413,7 @@ export default defineConfig({
|
|
|
368
413
|
alias: {
|
|
369
414
|
"blume:data": ${JSON.stringify(dataPath)},
|
|
370
415
|
"blume:examples": ${JSON.stringify(examplesPath)},
|
|
416
|
+
"blume:examples-theme": ${JSON.stringify(examplesThemePath)},
|
|
371
417
|
"blume:openapi": ${JSON.stringify(openapiPath)},
|
|
372
418
|
"blume:search-client": ${JSON.stringify(searchClientPath)},
|
|
373
419
|
"blume:theme": ${JSON.stringify(themePath)},${userAliasLines}
|
|
@@ -396,6 +442,17 @@ export default defineConfig({
|
|
|
396
442
|
export const stagedContentDir = (outDir: string): string =>
|
|
397
443
|
join(outDir, "content");
|
|
398
444
|
|
|
445
|
+
/**
|
|
446
|
+
* Astro's glob loader resolves `base` with `new URL(base, config.root)`. On
|
|
447
|
+
* Windows an absolute path like `C:\\docs\\content` makes `new URL` parse the
|
|
448
|
+
* drive letter as a URL scheme, so the result isn't a `file:` URL and Astro's
|
|
449
|
+
* subsequent `fileURLToPath` throws "The URL must be of scheme file". Emit an
|
|
450
|
+
* absolute base as a proper `file://` URL so the drive letter can't be mistaken
|
|
451
|
+
* for a scheme; relative bases resolve against `config.root` unchanged.
|
|
452
|
+
*/
|
|
453
|
+
const astroGlobBase = (base: string): string =>
|
|
454
|
+
isAbsolute(base) ? pathToFileURL(base).href : base;
|
|
455
|
+
|
|
399
456
|
/** Generate `.blume/src/content.config.ts`. */
|
|
400
457
|
export const contentConfigTemplate = (options: {
|
|
401
458
|
context: ProjectContext;
|
|
@@ -454,8 +511,8 @@ export const contentConfigTemplate = (options: {
|
|
|
454
511
|
// e.g. a prior `dist/*.mdx` render — and crash the content-module graph.
|
|
455
512
|
// The runtime dir (`.blume`, or a custom distDir) is excluded precisely
|
|
456
513
|
// by `outDirIgnore` instead, so it's left out of this baseline.
|
|
457
|
-
...BLUME_IGNORE_DIRS.
|
|
458
|
-
|
|
514
|
+
...BLUME_IGNORE_DIRS.flatMap((dir) =>
|
|
515
|
+
dir === ".blume" ? [] : [`!**/${dir}/**`]
|
|
459
516
|
),
|
|
460
517
|
...outDirIgnore,
|
|
461
518
|
]
|
|
@@ -468,7 +525,7 @@ export const contentConfigTemplate = (options: {
|
|
|
468
525
|
const staged = defineCollection({
|
|
469
526
|
loader: glob({
|
|
470
527
|
pattern: ["**/*.{md,mdx}"],
|
|
471
|
-
base: ${JSON.stringify(stagedBase)},
|
|
528
|
+
base: ${JSON.stringify(astroGlobBase(stagedBase))},
|
|
472
529
|
generateId: ({ entry }) => entry,
|
|
473
530
|
}),
|
|
474
531
|
});
|
|
@@ -482,7 +539,7 @@ import { glob } from "astro/loaders";
|
|
|
482
539
|
const docs = defineCollection({
|
|
483
540
|
loader: glob({
|
|
484
541
|
pattern: ${JSON.stringify(docsPattern)},
|
|
485
|
-
base: ${JSON.stringify(collectionBase)},
|
|
542
|
+
base: ${JSON.stringify(astroGlobBase(collectionBase))},
|
|
486
543
|
generateId: ({ entry }) => entry,
|
|
487
544
|
}),
|
|
488
545
|
});
|
|
@@ -562,6 +619,31 @@ export const askEndpointTemplate = (
|
|
|
562
619
|
content: m.content,
|
|
563
620
|
role: m.role,
|
|
564
621
|
}));`;
|
|
622
|
+
// `streamText` returns synchronously and defers provider/auth/network errors
|
|
623
|
+
// to stream consumption, so the handler's try/catch never sees them: without
|
|
624
|
+
// these the client gets a 200 whose stream aborts mid-flight and nothing is
|
|
625
|
+
// logged server-side. A missing credential is rejected up front as a real
|
|
626
|
+
// 500; everything else is at least logged via `onError`.
|
|
627
|
+
const keyCheck =
|
|
628
|
+
backend.kind === "gateway"
|
|
629
|
+
? ` // The AI Gateway authenticates with an API key or Vercel's OIDC token.
|
|
630
|
+
if (!(process.env.AI_GATEWAY_API_KEY || process.env.VERCEL_OIDC_TOKEN)) {
|
|
631
|
+
return new Response(
|
|
632
|
+
"Ask AI is not configured: set AI_GATEWAY_API_KEY (or deploy on Vercel with OIDC).",
|
|
633
|
+
{ status: 500 }
|
|
634
|
+
);
|
|
635
|
+
}`
|
|
636
|
+
: ` if (!process.env[${JSON.stringify(backend.apiKeyEnv)}]) {
|
|
637
|
+
return new Response(
|
|
638
|
+
${JSON.stringify(`Ask AI is not configured: set ${backend.apiKeyEnv}.`)},
|
|
639
|
+
{ status: 500 }
|
|
640
|
+
);
|
|
641
|
+
}`;
|
|
642
|
+
// Provider errors surface mid-stream, after the 200 is committed; this is
|
|
643
|
+
// the only place they can be observed server-side.
|
|
644
|
+
const onError = ` onError({ error }) {
|
|
645
|
+
console.error("Ask AI provider error:", error);
|
|
646
|
+
},`;
|
|
565
647
|
const stream = grounded
|
|
566
648
|
? ` const system =
|
|
567
649
|
(await ground(messages, body.page)) ??
|
|
@@ -570,15 +652,18 @@ export const askEndpointTemplate = (
|
|
|
570
652
|
model: ${modelExpr},
|
|
571
653
|
system,
|
|
572
654
|
messages,
|
|
655
|
+
${onError}
|
|
573
656
|
});`
|
|
574
657
|
: ` const result = streamText({
|
|
575
658
|
model: ${modelExpr},
|
|
576
659
|
system:
|
|
577
660
|
"You are a helpful documentation assistant. Answer using the project's documentation.",
|
|
578
661
|
messages,
|
|
662
|
+
${onError}
|
|
579
663
|
});`;
|
|
580
664
|
const handler = `export const POST: APIRoute = async ({ request }) => {
|
|
581
665
|
${validate}
|
|
666
|
+
${keyCheck}
|
|
582
667
|
try {
|
|
583
668
|
${stream}
|
|
584
669
|
return result.toTextStreamResponse();
|
|
@@ -751,9 +836,13 @@ export const POST: APIRoute = async ({ request }) => {
|
|
|
751
836
|
|
|
752
837
|
/**
|
|
753
838
|
* Generate the raw-Markdown endpoints (`[...slug].md.ts` and `[...slug].mdx.ts`).
|
|
754
|
-
*
|
|
839
|
+
* Both read `raw-markdown.json`, whose entries hold the verbatim source (`mdx`)
|
|
840
|
+
* plus a component-downleveled variant (`md`) when the page uses components:
|
|
841
|
+
* `/<route>.mdx` serves the source exactly as written, `/<route>.md` serves
|
|
842
|
+
* plain Markdown with `<TypeTable>`-style components converted for consumers
|
|
843
|
+
* that can't interpret JSX.
|
|
755
844
|
*/
|
|
756
|
-
export const rawMarkdownEndpointTemplate = (): string =>
|
|
845
|
+
export const rawMarkdownEndpointTemplate = (kind: "md" | "mdx"): string =>
|
|
757
846
|
`// Generated by Blume. Do not edit.
|
|
758
847
|
import raw from "../generated/raw-markdown.json";
|
|
759
848
|
|
|
@@ -767,7 +856,10 @@ export function getStaticPaths() {
|
|
|
767
856
|
}
|
|
768
857
|
|
|
769
858
|
export function GET({ props }) {
|
|
770
|
-
|
|
859
|
+
const entry = raw[props.route];
|
|
860
|
+
return new Response(entry ? ${
|
|
861
|
+
kind === "md" ? "(entry.md ?? entry.mdx)" : "entry.mdx"
|
|
862
|
+
} : "", {
|
|
771
863
|
headers: { "Content-Type": "text/markdown; charset=utf-8" },
|
|
772
864
|
});
|
|
773
865
|
}
|
|
@@ -946,6 +1038,7 @@ const configuration = ${JSON.stringify(options.configuration, null, 2)};
|
|
|
946
1038
|
searchEnabled={data.config.search.enabled}
|
|
947
1039
|
site={{ title: data.config.title, description: data.config.description }}
|
|
948
1040
|
themeMode={data.config.theme.mode}
|
|
1041
|
+
ui={data.ui}
|
|
949
1042
|
>
|
|
950
1043
|
<ScalarComponent configuration={configuration} renderMode="client" />
|
|
951
1044
|
</ReferenceLayout>
|
|
@@ -978,6 +1071,7 @@ export const catchAllPageTemplate = (options: {
|
|
|
978
1071
|
// Generated by Blume. Do not edit.
|
|
979
1072
|
import { getEntry, render } from "astro:content";
|
|
980
1073
|
import RootLayout from "blume/components/layout/RootLayout.astro";
|
|
1074
|
+
import { withBase } from "blume/components/islands/base-path.ts";
|
|
981
1075
|
import { resolveSlot } from "blume/components/layout/overrides.ts";
|
|
982
1076
|
${askImport}
|
|
983
1077
|
import Accordion from "blume/components/content/Accordion.astro";
|
|
@@ -1103,13 +1197,16 @@ const ogPath = data.config.og.enabled
|
|
|
1103
1197
|
? \`/og/\${route === "/" ? "index" : route.slice(1)}.png\`
|
|
1104
1198
|
: null;
|
|
1105
1199
|
const ogRel = seo.image ?? ogPath;
|
|
1106
|
-
//
|
|
1107
|
-
//
|
|
1200
|
+
// Absolute URLs also carry the deployment base (the page is served under it):
|
|
1201
|
+
// \`site + base + path\`. Only absolutize root-relative paths: \`seo.image\` may be
|
|
1202
|
+
// an external URL, which passes through verbatim (mirrors PageLayout).
|
|
1108
1203
|
const ogImage =
|
|
1109
|
-
ogRel && base && ogRel.startsWith("/") ? \`\${base}\${ogRel}\` : ogRel;
|
|
1204
|
+
ogRel && base && ogRel.startsWith("/") ? \`\${base}\${withBase(ogRel)}\` : ogRel;
|
|
1110
1205
|
|
|
1206
|
+
const basedRoute = withBase(route);
|
|
1111
1207
|
const canonical =
|
|
1112
|
-
seo.canonical ??
|
|
1208
|
+
seo.canonical ??
|
|
1209
|
+
(base ? \`\${base}\${basedRoute === "/" ? "" : basedRoute}\` : null);
|
|
1113
1210
|
|
|
1114
1211
|
// Locale resolution. With i18n on, pick the active locale's nav + dictionary,
|
|
1115
1212
|
// build hreflang alternates, and derive the language-switcher targets.
|
|
@@ -1142,7 +1239,10 @@ const contentLocale =
|
|
|
1142
1239
|
const contentDir = i18n
|
|
1143
1240
|
? (i18n.locales.find((l) => l.code === contentLocale)?.dir ?? "ltr")
|
|
1144
1241
|
: "ltr";
|
|
1145
|
-
const absolute = (path) =>
|
|
1242
|
+
const absolute = (path) => {
|
|
1243
|
+
const p = withBase(path);
|
|
1244
|
+
return base + (p === "/" ? "" : p);
|
|
1245
|
+
};
|
|
1146
1246
|
|
|
1147
1247
|
const localeAlternates =
|
|
1148
1248
|
i18n && base
|
|
@@ -1254,6 +1354,7 @@ export const changelogIndexTemplate = (options: {
|
|
|
1254
1354
|
import { getCollection, render } from "astro:content";
|
|
1255
1355
|
import RootLayout from "blume/components/layout/RootLayout.astro";
|
|
1256
1356
|
import Update from "blume/components/content/Update.astro";
|
|
1357
|
+
import { withBase } from "blume/components/islands/base-path.ts";
|
|
1257
1358
|
import { resolveSlot } from "blume/components/layout/overrides.ts";
|
|
1258
1359
|
import { layoutOverrides } from "../generated/components.ts";
|
|
1259
1360
|
${askImport}import data from "../generated/data.json";
|
|
@@ -1334,6 +1435,20 @@ const items = await Promise.all(
|
|
|
1334
1435
|
})
|
|
1335
1436
|
);
|
|
1336
1437
|
|
|
1438
|
+
// Repeated labels slug to the same id (e.g. two entries with neither a title
|
|
1439
|
+
// nor a version both falling back to "update"); suffix the later ones -2, -3,
|
|
1440
|
+
// ... so every heading deep-links to its own entry. The first keeps the plain
|
|
1441
|
+
// slug, and the rendered ids stay in lockstep with the \`headings\` list below.
|
|
1442
|
+
const seenIds = new Set();
|
|
1443
|
+
for (const item of items) {
|
|
1444
|
+
let uniqueId = item.id;
|
|
1445
|
+
for (let n = 2; seenIds.has(uniqueId); n += 1) {
|
|
1446
|
+
uniqueId = item.id + "-" + n;
|
|
1447
|
+
}
|
|
1448
|
+
seenIds.add(uniqueId);
|
|
1449
|
+
item.id = uniqueId;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1337
1452
|
// A changelog is semver-paginated only when every visible release parses as
|
|
1338
1453
|
// semver and they span more than one major line. Older majors then collapse
|
|
1339
1454
|
// into groups the reader reveals one at a time; otherwise the timeline is flat.
|
|
@@ -1354,7 +1469,20 @@ const headings = items.map((item) => ({
|
|
|
1354
1469
|
}));
|
|
1355
1470
|
|
|
1356
1471
|
const base = data.config.site ? data.config.site.replace(/\\/$/, "") : null;
|
|
1357
|
-
|
|
1472
|
+
// The canonical URL carries the deployment base (the page is served under it),
|
|
1473
|
+
// matching how the catch-all canonicalizes via \`withBase(route)\`.
|
|
1474
|
+
const basedRoute = withBase("/changelog");
|
|
1475
|
+
const canonical = base ? base + basedRoute : null;
|
|
1476
|
+
|
|
1477
|
+
// The changelog is an unlocalized route, so its chrome renders in the default
|
|
1478
|
+
// locale's dictionary and direction (\`data.ui\` is the default locale's resolved
|
|
1479
|
+
// dictionary), mirroring the catch-all's locale wiring.
|
|
1480
|
+
const i18n = data.config.i18n;
|
|
1481
|
+
const localeMeta = i18n
|
|
1482
|
+
? i18n.locales.find((l) => l.code === i18n.defaultLocale)
|
|
1483
|
+
: null;
|
|
1484
|
+
const dir = localeMeta?.dir ?? "ltr";
|
|
1485
|
+
const htmlLang = i18n ? i18n.defaultLocale : "en";
|
|
1358
1486
|
|
|
1359
1487
|
const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
|
|
1360
1488
|
---
|
|
@@ -1371,6 +1499,9 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
|
|
|
1371
1499
|
imageZoom={data.config.imageZoom}
|
|
1372
1500
|
codeWrap={data.config.codeWrap}
|
|
1373
1501
|
navigation={data.navigation}
|
|
1502
|
+
locale={htmlLang}
|
|
1503
|
+
dir={dir}
|
|
1504
|
+
ui={data.ui}
|
|
1374
1505
|
page={{
|
|
1375
1506
|
title: data.config.title + " changelog",
|
|
1376
1507
|
description: "Product updates and release notes.",
|
|
@@ -1398,7 +1529,10 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
|
|
|
1398
1529
|
items.length === 0 ? (
|
|
1399
1530
|
<p>No changelog entries yet.</p>
|
|
1400
1531
|
) : paginate ? (
|
|
1401
|
-
<blume-changelog
|
|
1532
|
+
<blume-changelog
|
|
1533
|
+
class="not-prose mt-8 block"
|
|
1534
|
+
data-i18n-more={data.ui.changelog?.showReleases}
|
|
1535
|
+
>
|
|
1402
1536
|
{majorGroups[0].items.map(({ Content, href, id, label, date, tags }) => (
|
|
1403
1537
|
<Update description={date} href={href} id={id} label={label} tags={tags}>
|
|
1404
1538
|
<Content />
|
|
@@ -1457,11 +1591,22 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
|
|
|
1457
1591
|
export const notFoundPageTemplate = (): string => `---
|
|
1458
1592
|
// Generated by Blume. Do not edit. Override by adding \`pages/404.astro\`.
|
|
1459
1593
|
import PageLayout from "blume/components/layout/PageLayout.astro";
|
|
1594
|
+
import { withBase } from "blume/components/islands/base-path.ts";
|
|
1460
1595
|
import data from "../generated/data.json";
|
|
1461
1596
|
|
|
1462
1597
|
export const prerender = true;
|
|
1463
1598
|
|
|
1464
1599
|
const nf = data.ui.notFound;
|
|
1600
|
+
|
|
1601
|
+
// The 404 page is an unlocalized route, so its chrome renders in the default
|
|
1602
|
+
// locale's dictionary and direction (\`data.ui\` is the default locale's resolved
|
|
1603
|
+
// dictionary), mirroring the catch-all's locale wiring.
|
|
1604
|
+
const i18n = data.config.i18n;
|
|
1605
|
+
const localeMeta = i18n
|
|
1606
|
+
? i18n.locales.find((l) => l.code === i18n.defaultLocale)
|
|
1607
|
+
: null;
|
|
1608
|
+
const dir = localeMeta?.dir ?? "ltr";
|
|
1609
|
+
const htmlLang = i18n ? i18n.defaultLocale : "en";
|
|
1465
1610
|
---
|
|
1466
1611
|
|
|
1467
1612
|
<PageLayout
|
|
@@ -1476,6 +1621,8 @@ const nf = data.ui.notFound;
|
|
|
1476
1621
|
themeMode={data.config.theme.mode}
|
|
1477
1622
|
fontCssVars={data.fontCssVars}
|
|
1478
1623
|
searchEnabled={data.config.search.enabled}
|
|
1624
|
+
locale={htmlLang}
|
|
1625
|
+
dir={dir}
|
|
1479
1626
|
ui={data.ui}
|
|
1480
1627
|
noindex={true}
|
|
1481
1628
|
>
|
|
@@ -1487,7 +1634,7 @@ const nf = data.ui.notFound;
|
|
|
1487
1634
|
<p class="text-muted-foreground">{nf.description}</p>
|
|
1488
1635
|
<a
|
|
1489
1636
|
class="mt-2 rounded-md bg-accent px-4 py-2 text-sm font-medium text-accent-foreground"
|
|
1490
|
-
href="/">{nf.home}</a
|
|
1637
|
+
href={withBase("/")}>{nf.home}</a
|
|
1491
1638
|
>
|
|
1492
1639
|
</div>
|
|
1493
1640
|
</PageLayout>
|
|
@@ -1575,15 +1722,31 @@ import Example from ${JSON.stringify(spec.file)};
|
|
|
1575
1722
|
<Example ${exampleDirective(spec)}{...Astro.props}><slot /></Example>
|
|
1576
1723
|
`;
|
|
1577
1724
|
|
|
1725
|
+
/**
|
|
1726
|
+
* The route prefix `<Component />` preview frames are served under:
|
|
1727
|
+
* `{basePath}/blume-examples/<example path>`. `deployment.base` is layered on
|
|
1728
|
+
* top by Astro (components apply it with `withBase`).
|
|
1729
|
+
*/
|
|
1730
|
+
export const examplesRouteBase = (basePath: string): string =>
|
|
1731
|
+
`${basePath}/blume-examples`;
|
|
1732
|
+
|
|
1578
1733
|
/**
|
|
1579
1734
|
* Generate `.blume/src/generated/examples.ts` — a map of example path to its live
|
|
1580
|
-
* wrapper component plus raw source and language for the code tab
|
|
1581
|
-
* shipped `Component.astro`
|
|
1735
|
+
* wrapper component plus raw source and language for the code tab, and the route
|
|
1736
|
+
* base preview iframes point at. Reached by the shipped `Component.astro` and the
|
|
1737
|
+
* generated preview page via the `blume:examples` alias. Always written (an
|
|
1582
1738
|
* empty object when there are no examples) so the alias resolves.
|
|
1583
1739
|
*/
|
|
1584
|
-
export const exampleMapTemplate = (
|
|
1740
|
+
export const exampleMapTemplate = (
|
|
1741
|
+
specs: ExampleSpec[],
|
|
1742
|
+
basePath: string
|
|
1743
|
+
): string => {
|
|
1744
|
+
const base = `export const examplesBase = ${JSON.stringify(
|
|
1745
|
+
examplesRouteBase(basePath)
|
|
1746
|
+
)};`;
|
|
1585
1747
|
if (specs.length === 0) {
|
|
1586
1748
|
return `// Generated by Blume. Do not edit.
|
|
1749
|
+
${base}
|
|
1587
1750
|
export const examples = {};
|
|
1588
1751
|
`;
|
|
1589
1752
|
}
|
|
@@ -1603,12 +1766,83 @@ export const examples = {};
|
|
|
1603
1766
|
.join("\n");
|
|
1604
1767
|
return `// Generated by Blume. Do not edit.
|
|
1605
1768
|
${imports}
|
|
1769
|
+
${base}
|
|
1606
1770
|
export const examples = {
|
|
1607
1771
|
${entries}
|
|
1608
1772
|
};
|
|
1609
1773
|
`;
|
|
1610
1774
|
};
|
|
1611
1775
|
|
|
1776
|
+
/**
|
|
1777
|
+
* Generate the `<Component />` preview page — one prerendered route per
|
|
1778
|
+
* example under `{basePath}/blume-examples/`, rendered as a bare document
|
|
1779
|
+
* (no layout) that an iframe in the docs page embeds. The iframe boundary is
|
|
1780
|
+
* what isolates examples from the docs CSS: the only stylesheet here is the
|
|
1781
|
+
* example entry (`blume:examples-theme` — Tailwind, the Blume tokens, and the
|
|
1782
|
+
* user's configured examples css), so users can preview components styled by
|
|
1783
|
+
* their own design system (e.g. shadcn) with no prose styles bleeding in.
|
|
1784
|
+
*
|
|
1785
|
+
* The inline script mirrors the docs theme before first paint — same-document
|
|
1786
|
+
* reads of the parent's `data-theme` (same origin) with a MutationObserver for
|
|
1787
|
+
* live toggles — and sets both `data-theme` and a `dark` class so either
|
|
1788
|
+
* dark-mode convention works in user CSS. When the page is opened directly
|
|
1789
|
+
* (no parent), it falls back to the stored preference, then the OS setting.
|
|
1790
|
+
*/
|
|
1791
|
+
export const examplesPageTemplate = (): string =>
|
|
1792
|
+
`---
|
|
1793
|
+
// Generated by Blume. Do not edit.
|
|
1794
|
+
import { examples } from "blume:examples";
|
|
1795
|
+
import "blume:examples-theme";
|
|
1796
|
+
|
|
1797
|
+
// Prerendered even in server output, like docs content.
|
|
1798
|
+
export const prerender = true;
|
|
1799
|
+
|
|
1800
|
+
export const getStaticPaths = () =>
|
|
1801
|
+
Object.keys(examples).map((path) => ({ params: { path } }));
|
|
1802
|
+
|
|
1803
|
+
const { path } = Astro.params;
|
|
1804
|
+
const entry = examples[path];
|
|
1805
|
+
const Example = entry.Component;
|
|
1806
|
+
---
|
|
1807
|
+
|
|
1808
|
+
<html lang="en">
|
|
1809
|
+
<head>
|
|
1810
|
+
<meta charset="utf-8" />
|
|
1811
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1812
|
+
<meta name="robots" content="noindex" />
|
|
1813
|
+
<title>{path}</title>
|
|
1814
|
+
<script is:inline>
|
|
1815
|
+
(() => {
|
|
1816
|
+
const root = document.documentElement;
|
|
1817
|
+
const apply = (theme) => {
|
|
1818
|
+
root.dataset.theme = theme;
|
|
1819
|
+
root.classList.toggle("dark", theme === "dark");
|
|
1820
|
+
};
|
|
1821
|
+
const stored = () =>
|
|
1822
|
+
localStorage.getItem("blume-theme") ??
|
|
1823
|
+
(matchMedia("(prefers-color-scheme: dark)").matches
|
|
1824
|
+
? "dark"
|
|
1825
|
+
: "light");
|
|
1826
|
+
try {
|
|
1827
|
+
const host = window.parent.document.documentElement;
|
|
1828
|
+
apply(host.dataset.theme ?? stored());
|
|
1829
|
+
new MutationObserver(() => {
|
|
1830
|
+
apply(host.dataset.theme ?? stored());
|
|
1831
|
+
}).observe(host, { attributeFilter: ["data-theme"] });
|
|
1832
|
+
} catch {
|
|
1833
|
+
apply(stored());
|
|
1834
|
+
}
|
|
1835
|
+
})();
|
|
1836
|
+
</script>
|
|
1837
|
+
</head>
|
|
1838
|
+
<!-- Flex + margin:auto centers the example and, unlike place-items, keeps
|
|
1839
|
+
the top edge reachable when the example outgrows the frame. -->
|
|
1840
|
+
<body style="display:flex;min-height:100svh;padding:1.5rem">
|
|
1841
|
+
<div style="margin:auto"><Example /></div>
|
|
1842
|
+
</body>
|
|
1843
|
+
</html>
|
|
1844
|
+
`;
|
|
1845
|
+
|
|
1612
1846
|
/** Generate `.blume/src/env.d.ts`. */
|
|
1613
1847
|
export const envTemplate =
|
|
1614
1848
|
(): string => `/// <reference path="../.astro/types.d.ts" />
|
package/src/blume-modules.d.ts
CHANGED
|
@@ -15,6 +15,14 @@ declare module "blume:search-client" {
|
|
|
15
15
|
export const createSearch: () => Fn | Promise<Fn>;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
declare module "blume:data" {
|
|
19
|
+
/** The generated per-project data snapshot (see `core/data.ts`). */
|
|
20
|
+
// biome-ignore lint/style/useImportType: ambient module must stay a global script
|
|
21
|
+
// oxlint-disable-next-line typescript/consistent-type-imports
|
|
22
|
+
const data: import("./core/data.ts").BlumeData;
|
|
23
|
+
export default data;
|
|
24
|
+
}
|
|
25
|
+
|
|
18
26
|
// Package-only shim so `components/props.ts` can extract `.astro` prop types with
|
|
19
27
|
// `ComponentProps<typeof import("./X.astro").default>` under the package's own
|
|
20
28
|
// `tsc` (where the Astro TS plugin isn't active). Not shipped in `dist/types`, so
|