blume 0.7.0 → 1.0.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 +666 -0
- package/LICENSE +21 -0
- package/README.md +107 -0
- package/dist/cli/index.js +1852 -380
- package/dist/cli/index.js.map +98 -91
- 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 +9 -0
- package/dist/types/core/config-input.d.ts +47 -2
- package/dist/types/core/config.d.ts +3 -2
- package/dist/types/core/data.d.ts +7 -0
- package/dist/types/core/i18n-ui.d.ts +526 -132
- package/dist/types/core/schema.d.ts +293 -146
- package/dist/types/index.d.ts +1 -0
- package/dist/types/openapi/references.d.ts +60 -0
- package/dist/types/seo/x-handle.d.ts +12 -0
- package/docs/01-quickstart.mdx +5 -2
- package/docs/02-deployment.mdx +8 -8
- package/docs/03-faq.mdx +46 -16
- package/docs/advanced/api-reference.mdx +1 -1
- package/docs/advanced/changelog.mdx +1 -1
- 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/export.mdx +1 -1
- package/docs/configuration/index.mdx +27 -3
- package/docs/configuration/seo.mdx +35 -5
- package/docs/content/components.mdx +2 -2
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/navigation.mdx +3 -3
- package/docs/content/sources.mdx +1 -1
- package/docs/content/syntax.mdx +6 -4
- package/docs/index.mdx +2 -2
- package/docs/reference/cli.mdx +9 -7
- package/docs/reference/frontmatter.mdx +1 -1
- package/package.json +22 -4
- package/skills/blume/SKILL.md +5 -3
- package/skills/blume-update-docs/SKILL.md +3 -2
- package/src/ai/agent-readability.ts +9 -8
- package/src/ai/ask-context.ts +7 -2
- package/src/ai/ask-data.ts +3 -0
- package/src/ai/component-markdown.ts +461 -0
- package/src/ai/llms.ts +135 -26
- package/src/ai/markdown.ts +35 -6
- package/src/ai/mcp/data.ts +25 -4
- package/src/ai/mcp/discovery.ts +10 -3
- package/src/ai/mcp/server.ts +21 -7
- package/src/ai/mcp/tools.ts +1 -1
- package/src/ai/visibility.ts +74 -0
- package/src/astro/component-slots.ts +11 -1
- package/src/astro/generate.ts +77 -45
- package/src/astro/integration.ts +1 -1
- package/src/astro/markdown-negotiation.ts +1 -1
- package/src/astro/pages.ts +81 -19
- package/src/astro/templates.ts +150 -19
- package/src/blume-modules.d.ts +8 -0
- package/src/cli/commands/build.ts +120 -23
- package/src/cli/commands/check.ts +1 -1
- package/src/cli/commands/dev.ts +26 -5
- 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 +43 -2
- package/src/cli/dev-lock.ts +8 -4
- 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/internal-error.ts +9 -4
- package/src/cli/prepare.ts +3 -2
- package/src/components/Icon.astro +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/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 +7 -1
- package/src/components/content/base-href.ts +33 -0
- package/src/components/content/changelog-element.ts +9 -2
- package/src/components/content/mermaid-element.ts +7 -2
- package/src/components/islands/AskAI.astro +5 -2
- package/src/components/islands/ask-ai.tsx +86 -11
- package/src/components/islands/hooks.ts +28 -8
- package/src/components/layout/Banner.astro +10 -2
- package/src/components/layout/Breadcrumbs.astro +11 -2
- package/src/components/layout/Header.astro +13 -4
- package/src/components/layout/Logo.astro +11 -3
- package/src/components/layout/NavTree.astro +19 -5
- package/src/components/layout/PageActions.astro +25 -10
- package/src/components/layout/PageLayout.astro +85 -9
- package/src/components/layout/Pagination.astro +10 -4
- package/src/components/layout/ReferenceLayout.astro +20 -2
- package/src/components/layout/RootLayout.astro +142 -12
- package/src/components/layout/Search.astro +117 -27
- 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/types.ts +5 -1
- package/src/components/layout/search/typesense.ts +4 -1
- package/src/components/layout/toc-element.ts +1 -1
- package/src/components/openapi/ApiTagOperations.astro +2 -1
- package/src/components/openapi/Operation.astro +47 -40
- package/src/components/openapi/RequestPanel.astro +1 -1
- package/src/components/openapi/helpers.ts +71 -3
- package/src/components/openapi/panel.ts +1 -1
- package/src/core/base-path.ts +24 -0
- package/src/core/builtin-tags.ts +2 -0
- package/src/core/config-input.ts +48 -2
- package/src/core/config.ts +3 -2
- package/src/core/data.ts +4 -0
- package/src/core/frontmatter.ts +7 -0
- package/src/core/graph.ts +15 -5
- package/src/core/i18n-ui.ts +54 -0
- package/src/core/i18n.ts +16 -8
- package/src/core/last-modified.ts +13 -6
- package/src/core/links.ts +32 -8
- package/src/core/navigation.ts +29 -4
- package/src/core/package-json.ts +17 -2
- package/src/core/project-graph.ts +15 -6
- package/src/core/schema.ts +71 -2
- package/src/core/sources/assets.ts +6 -1
- package/src/core/sources/filesystem.ts +4 -0
- package/src/core/sources/mdx-remote.ts +23 -14
- package/src/core/sources/normalize.ts +152 -50
- package/src/core/sources/notion.ts +8 -8
- package/src/core/ui-packs/ar.ts +8 -0
- package/src/core/ui-packs/bg.ts +8 -0
- package/src/core/ui-packs/bn.ts +8 -0
- package/src/core/ui-packs/ca.ts +8 -0
- package/src/core/ui-packs/cs.ts +8 -0
- package/src/core/ui-packs/da.ts +8 -0
- package/src/core/ui-packs/de.ts +8 -0
- package/src/core/ui-packs/el.ts +8 -0
- package/src/core/ui-packs/es.ts +8 -0
- package/src/core/ui-packs/fa.ts +8 -0
- package/src/core/ui-packs/fi.ts +8 -0
- package/src/core/ui-packs/fr.ts +9 -1
- package/src/core/ui-packs/he.ts +8 -0
- package/src/core/ui-packs/hi.ts +8 -0
- package/src/core/ui-packs/hr.ts +8 -0
- package/src/core/ui-packs/hu.ts +8 -0
- package/src/core/ui-packs/id.ts +8 -0
- package/src/core/ui-packs/it.ts +8 -0
- package/src/core/ui-packs/ja.ts +8 -0
- package/src/core/ui-packs/ko.ts +8 -0
- package/src/core/ui-packs/nl.ts +8 -0
- package/src/core/ui-packs/no.ts +8 -0
- package/src/core/ui-packs/pl.ts +8 -0
- package/src/core/ui-packs/pt-br.ts +8 -0
- package/src/core/ui-packs/pt.ts +8 -0
- package/src/core/ui-packs/ro.ts +8 -0
- package/src/core/ui-packs/ru.ts +8 -0
- package/src/core/ui-packs/sk.ts +8 -0
- package/src/core/ui-packs/sr.ts +8 -0
- package/src/core/ui-packs/sv.ts +8 -0
- package/src/core/ui-packs/th.ts +8 -0
- package/src/core/ui-packs/tr.ts +8 -0
- package/src/core/ui-packs/uk.ts +8 -0
- package/src/core/ui-packs/vi.ts +8 -0
- package/src/core/ui-packs/zh-tw.ts +8 -0
- package/src/core/ui-packs/zh.ts +8 -0
- package/src/deploy/adapter-output.ts +18 -8
- package/src/deploy/redirects.ts +7 -2
- package/src/deploy/sitemap.ts +53 -11
- package/src/index.ts +5 -0
- package/src/markdown/base-links.ts +10 -8
- package/src/markdown/index.ts +15 -3
- package/src/markdown/inline-code.ts +7 -2
- package/src/markdown/package-commands.ts +10 -4
- package/src/og/card.ts +4 -2
- package/src/og/dimensions.ts +12 -0
- package/src/openapi/model.ts +12 -4
- package/src/openapi/parse.ts +21 -0
- package/src/openapi/references.ts +38 -8
- package/src/openapi/render-mdx.ts +62 -1
- package/src/openapi/source.ts +59 -10
- package/src/registry/eject.ts +184 -12
- package/src/registry/registry.ts +0 -3
- package/src/search/documents.ts +34 -2
- package/src/seo/jsonld.ts +20 -13
- package/src/seo/x-handle.ts +18 -0
package/src/astro/pages.ts
CHANGED
|
@@ -1,23 +1,14 @@
|
|
|
1
1
|
import { extname, relative } from "pathe";
|
|
2
|
-
import { glob } from "tinyglobby";
|
|
2
|
+
import { glob, globSync } from "tinyglobby";
|
|
3
3
|
|
|
4
|
+
import type { BlumeProject } from "../core/project-graph.ts";
|
|
4
5
|
import type { BlumePageRoute } from "./integration.ts";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
* Discover user `.astro` pages and map them to route patterns. Files keep their
|
|
8
|
-
* original location; only the route pattern is derived (index -> parent,
|
|
9
|
-
* dynamic `[param]` segments preserved).
|
|
10
|
-
*/
|
|
11
|
-
export const discoverPages = async (
|
|
12
|
-
pagesRoot: string
|
|
13
|
-
): Promise<BlumePageRoute[]> => {
|
|
14
|
-
const files = await glob(["**/*.astro"], {
|
|
15
|
-
absolute: true,
|
|
16
|
-
cwd: pagesRoot,
|
|
17
|
-
onlyFiles: true,
|
|
18
|
-
});
|
|
19
|
-
files.sort();
|
|
7
|
+
const PAGE_GLOB = ["**/*.astro"];
|
|
20
8
|
|
|
9
|
+
/** Map discovered page files to routes; shared by the async/sync discoverers. */
|
|
10
|
+
const toPageRoutes = (pagesRoot: string, files: string[]): BlumePageRoute[] => {
|
|
11
|
+
files.sort();
|
|
21
12
|
return files.map((file) => {
|
|
22
13
|
const rel = relative(pagesRoot, file);
|
|
23
14
|
const withoutExt = rel.slice(0, rel.length - extname(rel).length);
|
|
@@ -32,6 +23,26 @@ export const discoverPages = async (
|
|
|
32
23
|
});
|
|
33
24
|
};
|
|
34
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Discover user `.astro` pages and map them to route patterns. Files keep their
|
|
28
|
+
* original location; only the route pattern is derived (index -> parent,
|
|
29
|
+
* dynamic `[param]` segments preserved).
|
|
30
|
+
*/
|
|
31
|
+
export const discoverPages = async (
|
|
32
|
+
pagesRoot: string
|
|
33
|
+
): Promise<BlumePageRoute[]> =>
|
|
34
|
+
toPageRoutes(
|
|
35
|
+
pagesRoot,
|
|
36
|
+
await glob(PAGE_GLOB, { absolute: true, cwd: pagesRoot, onlyFiles: true })
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
/** {@link discoverPages} for synchronous callers (e.g. the sitemap builder). */
|
|
40
|
+
export const discoverPagesSync = (pagesRoot: string): BlumePageRoute[] =>
|
|
41
|
+
toPageRoutes(
|
|
42
|
+
pagesRoot,
|
|
43
|
+
globSync(PAGE_GLOB, { absolute: true, cwd: pagesRoot, onlyFiles: true })
|
|
44
|
+
);
|
|
45
|
+
|
|
35
46
|
/**
|
|
36
47
|
* Whether the project already owns `route` — through a custom `.astro` page
|
|
37
48
|
* (injected, so matched on `pattern`) or a content page (matched on `route`).
|
|
@@ -57,6 +68,59 @@ export interface OgCustomRoute {
|
|
|
57
68
|
/** Skip private (`_partial`, `.well-known`) and Astro dynamic (`[param]`) parts. */
|
|
58
69
|
const PRIVATE_SEGMENT = /^[._]/u;
|
|
59
70
|
|
|
71
|
+
/** Segments of a static, shareable page pattern; null for dynamic/private ones. */
|
|
72
|
+
const staticSegments = (pattern: string): string[] | null => {
|
|
73
|
+
const segments = pattern.split("/").filter(Boolean);
|
|
74
|
+
return segments.some(
|
|
75
|
+
(part) => PRIVATE_SEGMENT.test(part) || part.includes("[")
|
|
76
|
+
)
|
|
77
|
+
? null
|
|
78
|
+
: segments;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The static routes served by custom `.astro` pages — the same filtering as
|
|
83
|
+
* {@link customOgRoutes}, but yielding the routes themselves. Feeds the route
|
|
84
|
+
* sets that must know every servable page beyond the content graph (the link
|
|
85
|
+
* checker, the sitemap); dynamic (`[param]`) and private segments are skipped
|
|
86
|
+
* because their concrete URLs can't be enumerated statically.
|
|
87
|
+
*/
|
|
88
|
+
export const customStaticRoutes = (pages: { pattern: string }[]): string[] => {
|
|
89
|
+
const routes = new Set<string>();
|
|
90
|
+
for (const { pattern } of pages) {
|
|
91
|
+
const segments = staticSegments(pattern);
|
|
92
|
+
if (segments !== null) {
|
|
93
|
+
routes.add(segments.length === 0 ? "/" : `/${segments.join("/")}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return [...routes];
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Whether the generated `/changelog` index route exists for this project —
|
|
101
|
+
* `generate.ts` (which writes the page) and the sitemap/link validator all
|
|
102
|
+
* share this check: there are visible `type: changelog` entries — or a
|
|
103
|
+
* release-backed changelog source, whose route must resolve even when a fetch
|
|
104
|
+
* fails — and no user content or custom page already owns `/changelog`.
|
|
105
|
+
*/
|
|
106
|
+
export const hasGeneratedChangelog = (
|
|
107
|
+
project: BlumeProject,
|
|
108
|
+
userPages: { pattern: string }[]
|
|
109
|
+
): boolean => {
|
|
110
|
+
const hasChangelog = project.graph.pages.some(
|
|
111
|
+
(page) =>
|
|
112
|
+
page.contentType === "changelog" &&
|
|
113
|
+
!(page.meta.draft || page.meta.sidebar.hidden)
|
|
114
|
+
);
|
|
115
|
+
const hasChangelogSource = (project.config.content.sources ?? []).some(
|
|
116
|
+
(source) => source.type === "github-releases"
|
|
117
|
+
);
|
|
118
|
+
return (
|
|
119
|
+
(hasChangelog || hasChangelogSource) &&
|
|
120
|
+
!routeIsTaken(userPages, project.graph.pages, "/changelog")
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
|
|
60
124
|
const humanizeSegment = (segment: string): string =>
|
|
61
125
|
segment
|
|
62
126
|
.split(/[-_]/u)
|
|
@@ -83,10 +147,8 @@ export const customOgRoutes = (
|
|
|
83
147
|
// Extracted so the skip paths become early `return`s (one `continue` budget
|
|
84
148
|
// per loop under the lint rule) instead of `continue` statements.
|
|
85
149
|
const collectRoute = (pattern: string): void => {
|
|
86
|
-
const segments = pattern
|
|
87
|
-
if (
|
|
88
|
-
segments.some((part) => PRIVATE_SEGMENT.test(part) || part.includes("["))
|
|
89
|
-
) {
|
|
150
|
+
const segments = staticSegments(pattern);
|
|
151
|
+
if (segments === null) {
|
|
90
152
|
return;
|
|
91
153
|
}
|
|
92
154
|
const slug = segments.length === 0 ? "index" : segments.join("/");
|
package/src/astro/templates.ts
CHANGED
|
@@ -187,18 +187,31 @@ const renderUserAliases = (
|
|
|
187
187
|
const astroOutDir = (context: ProjectContext): string =>
|
|
188
188
|
context.distDir ?? `${context.root}/dist`;
|
|
189
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Excludes Vite's pre-bundled dep cache from @vitejs/plugin-react. Astro's
|
|
192
|
+
* react() replaces the plugin's default `/node_modules/` exclude with just
|
|
193
|
+
* `/\.astro$/`, so without this Babel re-parses every optimized dep chunk
|
|
194
|
+
* served from `.vite/deps` — a 500KB+ vendor bundle per chunk, re-done on each
|
|
195
|
+
* re-optimization. A blanket `/node_modules/` exclude would instead switch the
|
|
196
|
+
* React Compiler off for Blume's own components in published installs (they
|
|
197
|
+
* resolve under `node_modules/blume/src`, and exclude beats include in the
|
|
198
|
+
* plugin's filter), so only the pre-bundle cache is excluded.
|
|
199
|
+
*/
|
|
200
|
+
const REACT_EXCLUDE = String.raw`exclude: [/\/node_modules\/\.vite\//]`;
|
|
201
|
+
|
|
190
202
|
/**
|
|
191
203
|
* The `react()` integration call. When `compilerPath` is set (the resolved
|
|
192
204
|
* absolute path to `babel-plugin-react-compiler`), react() carries the compiler
|
|
193
205
|
* as the first babel plugin — an absolute path, because @vitejs/plugin-react
|
|
194
206
|
* resolves babel plugins from the *project* root, not `.blume/`, so a bare
|
|
195
207
|
* specifier wouldn't resolve in a user project. `target: "19"` matches Blume's
|
|
196
|
-
* React pin. `null`/`undefined` (compiler off or unresolvable)
|
|
208
|
+
* React pin. `null`/`undefined` (compiler off or unresolvable) omits the babel
|
|
209
|
+
* block. Both variants carry the pre-bundle exclude above.
|
|
197
210
|
*/
|
|
198
211
|
const reactIntegration = (compilerPath: string | null | undefined): string =>
|
|
199
212
|
compilerPath
|
|
200
|
-
? `react({ babel: { plugins: [[${JSON.stringify(compilerPath)}, { target: "19" }]] } })`
|
|
201
|
-
:
|
|
213
|
+
? `react({ babel: { plugins: [[${JSON.stringify(compilerPath)}, { target: "19" }]] }, ${REACT_EXCLUDE} })`
|
|
214
|
+
: `react({ ${REACT_EXCLUDE} })`;
|
|
202
215
|
|
|
203
216
|
export const astroConfigTemplate = (options: {
|
|
204
217
|
context: ProjectContext;
|
|
@@ -329,15 +342,18 @@ export const astroConfigTemplate = (options: {
|
|
|
329
342
|
"transformerTwoslash({ explicitTrigger: true }), ";
|
|
330
343
|
|
|
331
344
|
// Content links are rewritten to their real served URL: the `deployment.base`
|
|
332
|
-
// subdirectory (Astro doesn't rewrite `<a href>`)
|
|
333
|
-
// `basePath` baked into routes. The
|
|
334
|
-
//
|
|
335
|
-
|
|
345
|
+
// subdirectory (Astro doesn't rewrite `<a href>`) layered over the site-wide
|
|
346
|
+
// `basePath` baked into routes. The layers are passed separately so a
|
|
347
|
+
// hand-written `basePath` link (`/docs/x`) isn't double-prefixed (see
|
|
348
|
+
// `withComposedBasePath`). The link checker validates the base-less authored
|
|
349
|
+
// path against `basePath` routes separately.
|
|
350
|
+
const deployBase = normalizeBasePath(deployment.base);
|
|
336
351
|
|
|
337
352
|
const integrations = [
|
|
338
353
|
`mdx({ processor: blumeMdxProcessor(${JSON.stringify({
|
|
339
|
-
basePath:
|
|
354
|
+
basePath: config.basePath,
|
|
340
355
|
codeThemes: config.markdown.codeBlocks.theme,
|
|
356
|
+
deployBase,
|
|
341
357
|
headingAnchors: config.markdown.headingAnchors,
|
|
342
358
|
})}) })`,
|
|
343
359
|
];
|
|
@@ -371,8 +387,9 @@ export default defineConfig({
|
|
|
371
387
|
integrations: [${integrations.join(", ")}],
|
|
372
388
|
markdown: {
|
|
373
389
|
processor: blumeMarkdownProcessor(${JSON.stringify({
|
|
374
|
-
basePath:
|
|
390
|
+
basePath: config.basePath,
|
|
375
391
|
codeThemes: config.markdown.codeBlocks.theme,
|
|
392
|
+
deployBase,
|
|
376
393
|
headingAnchors: config.markdown.headingAnchors,
|
|
377
394
|
})}),
|
|
378
395
|
shikiConfig: {
|
|
@@ -615,6 +632,31 @@ export const askEndpointTemplate = (
|
|
|
615
632
|
content: m.content,
|
|
616
633
|
role: m.role,
|
|
617
634
|
}));`;
|
|
635
|
+
// `streamText` returns synchronously and defers provider/auth/network errors
|
|
636
|
+
// to stream consumption, so the handler's try/catch never sees them: without
|
|
637
|
+
// these the client gets a 200 whose stream aborts mid-flight and nothing is
|
|
638
|
+
// logged server-side. A missing credential is rejected up front as a real
|
|
639
|
+
// 500; everything else is at least logged via `onError`.
|
|
640
|
+
const keyCheck =
|
|
641
|
+
backend.kind === "gateway"
|
|
642
|
+
? ` // The AI Gateway authenticates with an API key or Vercel's OIDC token.
|
|
643
|
+
if (!(process.env.AI_GATEWAY_API_KEY || process.env.VERCEL_OIDC_TOKEN)) {
|
|
644
|
+
return new Response(
|
|
645
|
+
"Ask AI is not configured: set AI_GATEWAY_API_KEY (or deploy on Vercel with OIDC).",
|
|
646
|
+
{ status: 500 }
|
|
647
|
+
);
|
|
648
|
+
}`
|
|
649
|
+
: ` if (!process.env[${JSON.stringify(backend.apiKeyEnv)}]) {
|
|
650
|
+
return new Response(
|
|
651
|
+
${JSON.stringify(`Ask AI is not configured: set ${backend.apiKeyEnv}.`)},
|
|
652
|
+
{ status: 500 }
|
|
653
|
+
);
|
|
654
|
+
}`;
|
|
655
|
+
// Provider errors surface mid-stream, after the 200 is committed; this is
|
|
656
|
+
// the only place they can be observed server-side.
|
|
657
|
+
const onError = ` onError({ error }) {
|
|
658
|
+
console.error("Ask AI provider error:", error);
|
|
659
|
+
},`;
|
|
618
660
|
const stream = grounded
|
|
619
661
|
? ` const system =
|
|
620
662
|
(await ground(messages, body.page)) ??
|
|
@@ -623,15 +665,18 @@ export const askEndpointTemplate = (
|
|
|
623
665
|
model: ${modelExpr},
|
|
624
666
|
system,
|
|
625
667
|
messages,
|
|
668
|
+
${onError}
|
|
626
669
|
});`
|
|
627
670
|
: ` const result = streamText({
|
|
628
671
|
model: ${modelExpr},
|
|
629
672
|
system:
|
|
630
673
|
"You are a helpful documentation assistant. Answer using the project's documentation.",
|
|
631
674
|
messages,
|
|
675
|
+
${onError}
|
|
632
676
|
});`;
|
|
633
677
|
const handler = `export const POST: APIRoute = async ({ request }) => {
|
|
634
678
|
${validate}
|
|
679
|
+
${keyCheck}
|
|
635
680
|
try {
|
|
636
681
|
${stream}
|
|
637
682
|
return result.toTextStreamResponse();
|
|
@@ -804,9 +849,13 @@ export const POST: APIRoute = async ({ request }) => {
|
|
|
804
849
|
|
|
805
850
|
/**
|
|
806
851
|
* Generate the raw-Markdown endpoints (`[...slug].md.ts` and `[...slug].mdx.ts`).
|
|
807
|
-
*
|
|
852
|
+
* Both read `raw-markdown.json`, whose entries hold the verbatim source (`mdx`)
|
|
853
|
+
* plus a component-downleveled variant (`md`) when the page uses components:
|
|
854
|
+
* `/<route>.mdx` serves the source exactly as written, `/<route>.md` serves
|
|
855
|
+
* plain Markdown with `<TypeTable>`-style components converted for consumers
|
|
856
|
+
* that can't interpret JSX.
|
|
808
857
|
*/
|
|
809
|
-
export const rawMarkdownEndpointTemplate = (): string =>
|
|
858
|
+
export const rawMarkdownEndpointTemplate = (kind: "md" | "mdx"): string =>
|
|
810
859
|
`// Generated by Blume. Do not edit.
|
|
811
860
|
import raw from "../generated/raw-markdown.json";
|
|
812
861
|
|
|
@@ -820,7 +869,10 @@ export function getStaticPaths() {
|
|
|
820
869
|
}
|
|
821
870
|
|
|
822
871
|
export function GET({ props }) {
|
|
823
|
-
|
|
872
|
+
const entry = raw[props.route];
|
|
873
|
+
return new Response(entry ? ${
|
|
874
|
+
kind === "md" ? "(entry.md ?? entry.mdx)" : "entry.mdx"
|
|
875
|
+
} : "", {
|
|
824
876
|
headers: { "Content-Type": "text/markdown; charset=utf-8" },
|
|
825
877
|
});
|
|
826
878
|
}
|
|
@@ -984,12 +1036,24 @@ import data from ${JSON.stringify(options.dataImport)};
|
|
|
984
1036
|
export const prerender = true;
|
|
985
1037
|
|
|
986
1038
|
const configuration = ${JSON.stringify(options.configuration, null, 2)};
|
|
1039
|
+
|
|
1040
|
+
// The reference is an unlocalized route, so its chrome renders in the default
|
|
1041
|
+
// locale's language and direction (\`data.ui\` is the default locale's resolved
|
|
1042
|
+
// dictionary), mirroring the changelog index's locale wiring.
|
|
1043
|
+
const i18n = data.config.i18n;
|
|
1044
|
+
const localeMeta = i18n
|
|
1045
|
+
? i18n.locales.find((l) => l.code === i18n.defaultLocale)
|
|
1046
|
+
: null;
|
|
1047
|
+
const dir = localeMeta?.dir ?? "ltr";
|
|
1048
|
+
const htmlLang = i18n ? i18n.defaultLocale : "en";
|
|
987
1049
|
---
|
|
988
1050
|
|
|
989
1051
|
<ReferenceLayout
|
|
990
1052
|
analytics={data.config.analytics}
|
|
991
1053
|
banner={data.config.banner}
|
|
1054
|
+
dir={dir}
|
|
992
1055
|
fontCssVars={data.fontCssVars}
|
|
1056
|
+
locale={htmlLang}
|
|
993
1057
|
logo={data.config.logo}
|
|
994
1058
|
favicon={data.config.favicon}
|
|
995
1059
|
appleIcon={data.config.appleIcon}
|
|
@@ -999,6 +1063,7 @@ const configuration = ${JSON.stringify(options.configuration, null, 2)};
|
|
|
999
1063
|
searchEnabled={data.config.search.enabled}
|
|
1000
1064
|
site={{ title: data.config.title, description: data.config.description }}
|
|
1001
1065
|
themeMode={data.config.theme.mode}
|
|
1066
|
+
ui={data.ui}
|
|
1002
1067
|
>
|
|
1003
1068
|
<ScalarComponent configuration={configuration} renderMode="client" />
|
|
1004
1069
|
</ReferenceLayout>
|
|
@@ -1162,6 +1227,13 @@ const ogRel = seo.image ?? ogPath;
|
|
|
1162
1227
|
// an external URL, which passes through verbatim (mirrors PageLayout).
|
|
1163
1228
|
const ogImage =
|
|
1164
1229
|
ogRel && base && ogRel.startsWith("/") ? \`\${base}\${withBase(ogRel)}\` : ogRel;
|
|
1230
|
+
// Blume's generated card has known dimensions the layout can declare; a user's
|
|
1231
|
+
// \`seo.image\` could be any size or format, so it gets none.
|
|
1232
|
+
const ogGenerated = !seo.image && Boolean(ogPath);
|
|
1233
|
+
|
|
1234
|
+
// X attribution: the site's account, plus a creator the page can claim for
|
|
1235
|
+
// itself (a guest post crediting its own author) over the configured default.
|
|
1236
|
+
const x = { ...data.config.x, ...(seo.x?.creator ? { creator: seo.x.creator } : {}) };
|
|
1165
1237
|
|
|
1166
1238
|
const basedRoute = withBase(route);
|
|
1167
1239
|
const canonical =
|
|
@@ -1258,6 +1330,8 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
|
|
|
1258
1330
|
searchEnabled={data.config.search.enabled}
|
|
1259
1331
|
indexable={indexable}
|
|
1260
1332
|
ogImage={ogImage}
|
|
1333
|
+
ogGenerated={ogGenerated}
|
|
1334
|
+
x={x}
|
|
1261
1335
|
canonical={canonical}
|
|
1262
1336
|
editUrl={editUrl}
|
|
1263
1337
|
feedback={data.config.feedback}
|
|
@@ -1301,7 +1375,7 @@ export const changelogIndexTemplate = (options: {
|
|
|
1301
1375
|
? '\n <AskAI slot="ask" strings={data.ui.ask} suggestions={data.config.ask?.suggestions ?? []} />'
|
|
1302
1376
|
: "";
|
|
1303
1377
|
const clientData = options.needsReact
|
|
1304
|
-
? '\n clientData={{ config: data.config, navigation: data.navigation, page: { route: "/changelog", title:
|
|
1378
|
+
? '\n clientData={{ config: data.config, navigation: data.navigation, page: { route: "/changelog", title: pageTitle } }}'
|
|
1305
1379
|
: "";
|
|
1306
1380
|
// Staged sources (e.g. GitHub Releases) render through a parallel collection,
|
|
1307
1381
|
// so fold them in alongside filesystem entries when one exists.
|
|
@@ -1314,6 +1388,7 @@ export const changelogIndexTemplate = (options: {
|
|
|
1314
1388
|
import { getCollection, render } from "astro:content";
|
|
1315
1389
|
import RootLayout from "blume/components/layout/RootLayout.astro";
|
|
1316
1390
|
import Update from "blume/components/content/Update.astro";
|
|
1391
|
+
import { withBase } from "blume/components/islands/base-path.ts";
|
|
1317
1392
|
import { resolveSlot } from "blume/components/layout/overrides.ts";
|
|
1318
1393
|
import { layoutOverrides } from "../generated/components.ts";
|
|
1319
1394
|
${askImport}import data from "../generated/data.json";
|
|
@@ -1394,6 +1469,20 @@ const items = await Promise.all(
|
|
|
1394
1469
|
})
|
|
1395
1470
|
);
|
|
1396
1471
|
|
|
1472
|
+
// Repeated labels slug to the same id (e.g. two entries with neither a title
|
|
1473
|
+
// nor a version both falling back to "update"); suffix the later ones -2, -3,
|
|
1474
|
+
// ... so every heading deep-links to its own entry. The first keeps the plain
|
|
1475
|
+
// slug, and the rendered ids stay in lockstep with the \`headings\` list below.
|
|
1476
|
+
const seenIds = new Set();
|
|
1477
|
+
for (const item of items) {
|
|
1478
|
+
let uniqueId = item.id;
|
|
1479
|
+
for (let n = 2; seenIds.has(uniqueId); n += 1) {
|
|
1480
|
+
uniqueId = item.id + "-" + n;
|
|
1481
|
+
}
|
|
1482
|
+
seenIds.add(uniqueId);
|
|
1483
|
+
item.id = uniqueId;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1397
1486
|
// A changelog is semver-paginated only when every visible release parses as
|
|
1398
1487
|
// semver and they span more than one major line. Older majors then collapse
|
|
1399
1488
|
// into groups the reader reveals one at a time; otherwise the timeline is flat.
|
|
@@ -1414,7 +1503,29 @@ const headings = items.map((item) => ({
|
|
|
1414
1503
|
}));
|
|
1415
1504
|
|
|
1416
1505
|
const base = data.config.site ? data.config.site.replace(/\\/$/, "") : null;
|
|
1417
|
-
|
|
1506
|
+
// The canonical URL carries the deployment base (the page is served under it),
|
|
1507
|
+
// matching how the catch-all canonicalizes via \`withBase(route)\`.
|
|
1508
|
+
const basedRoute = withBase("/changelog");
|
|
1509
|
+
const canonical = base ? base + basedRoute : null;
|
|
1510
|
+
|
|
1511
|
+
// The changelog is an unlocalized route, so its chrome renders in the default
|
|
1512
|
+
// locale's dictionary and direction (\`data.ui\` is the default locale's resolved
|
|
1513
|
+
// dictionary), mirroring the catch-all's locale wiring.
|
|
1514
|
+
const i18n = data.config.i18n;
|
|
1515
|
+
const localeMeta = i18n
|
|
1516
|
+
? i18n.locales.find((l) => l.code === i18n.defaultLocale)
|
|
1517
|
+
: null;
|
|
1518
|
+
const dir = localeMeta?.dir ?? "ltr";
|
|
1519
|
+
const htmlLang = i18n ? i18n.defaultLocale : "en";
|
|
1520
|
+
|
|
1521
|
+
// The page chrome (h1, title, description) comes from the same translatable
|
|
1522
|
+
// \`changelog\` group as the reveal button; optional chaining tolerates a
|
|
1523
|
+
// not-yet-regenerated data snapshot from before these keys existed.
|
|
1524
|
+
const changelogTitle = data.ui.changelog?.title ?? "Changelog";
|
|
1525
|
+
const changelogDescription =
|
|
1526
|
+
data.ui.changelog?.description ??
|
|
1527
|
+
"Product updates, new features, and fixes from every release.";
|
|
1528
|
+
const pageTitle = data.config.title + " " + changelogTitle;
|
|
1418
1529
|
|
|
1419
1530
|
const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
|
|
1420
1531
|
---
|
|
@@ -1431,9 +1542,12 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
|
|
|
1431
1542
|
imageZoom={data.config.imageZoom}
|
|
1432
1543
|
codeWrap={data.config.codeWrap}
|
|
1433
1544
|
navigation={data.navigation}
|
|
1545
|
+
locale={htmlLang}
|
|
1546
|
+
dir={dir}
|
|
1547
|
+
ui={data.ui}
|
|
1434
1548
|
page={{
|
|
1435
|
-
title:
|
|
1436
|
-
description:
|
|
1549
|
+
title: pageTitle,
|
|
1550
|
+
description: changelogDescription,
|
|
1437
1551
|
route: "/changelog",
|
|
1438
1552
|
}}
|
|
1439
1553
|
headings={headings}
|
|
@@ -1444,6 +1558,7 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
|
|
|
1444
1558
|
searchEnabled={data.config.search.enabled}
|
|
1445
1559
|
indexable={true}
|
|
1446
1560
|
ogImage={null}
|
|
1561
|
+
x={data.config.x}
|
|
1447
1562
|
canonical={canonical}
|
|
1448
1563
|
askEnabled={${options.askEnabled}}
|
|
1449
1564
|
exportPdf={${options.exportPdf}}
|
|
@@ -1453,12 +1568,15 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
|
|
|
1453
1568
|
noindex={false}
|
|
1454
1569
|
structuredDataEnabled={data.config.structuredData}
|
|
1455
1570
|
>${askSlot}
|
|
1456
|
-
<h1>
|
|
1571
|
+
<h1>{changelogTitle}</h1>
|
|
1457
1572
|
{
|
|
1458
1573
|
items.length === 0 ? (
|
|
1459
1574
|
<p>No changelog entries yet.</p>
|
|
1460
1575
|
) : paginate ? (
|
|
1461
|
-
<blume-changelog
|
|
1576
|
+
<blume-changelog
|
|
1577
|
+
class="not-prose mt-8 block"
|
|
1578
|
+
data-i18n-more={data.ui.changelog?.showReleases}
|
|
1579
|
+
>
|
|
1462
1580
|
{majorGroups[0].items.map(({ Content, href, id, label, date, tags }) => (
|
|
1463
1581
|
<Update description={date} href={href} id={id} label={label} tags={tags}>
|
|
1464
1582
|
<Content />
|
|
@@ -1517,11 +1635,22 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
|
|
|
1517
1635
|
export const notFoundPageTemplate = (): string => `---
|
|
1518
1636
|
// Generated by Blume. Do not edit. Override by adding \`pages/404.astro\`.
|
|
1519
1637
|
import PageLayout from "blume/components/layout/PageLayout.astro";
|
|
1638
|
+
import { withBase } from "blume/components/islands/base-path.ts";
|
|
1520
1639
|
import data from "../generated/data.json";
|
|
1521
1640
|
|
|
1522
1641
|
export const prerender = true;
|
|
1523
1642
|
|
|
1524
1643
|
const nf = data.ui.notFound;
|
|
1644
|
+
|
|
1645
|
+
// The 404 page is an unlocalized route, so its chrome renders in the default
|
|
1646
|
+
// locale's dictionary and direction (\`data.ui\` is the default locale's resolved
|
|
1647
|
+
// dictionary), mirroring the catch-all's locale wiring.
|
|
1648
|
+
const i18n = data.config.i18n;
|
|
1649
|
+
const localeMeta = i18n
|
|
1650
|
+
? i18n.locales.find((l) => l.code === i18n.defaultLocale)
|
|
1651
|
+
: null;
|
|
1652
|
+
const dir = localeMeta?.dir ?? "ltr";
|
|
1653
|
+
const htmlLang = i18n ? i18n.defaultLocale : "en";
|
|
1525
1654
|
---
|
|
1526
1655
|
|
|
1527
1656
|
<PageLayout
|
|
@@ -1536,6 +1665,8 @@ const nf = data.ui.notFound;
|
|
|
1536
1665
|
themeMode={data.config.theme.mode}
|
|
1537
1666
|
fontCssVars={data.fontCssVars}
|
|
1538
1667
|
searchEnabled={data.config.search.enabled}
|
|
1668
|
+
locale={htmlLang}
|
|
1669
|
+
dir={dir}
|
|
1539
1670
|
ui={data.ui}
|
|
1540
1671
|
noindex={true}
|
|
1541
1672
|
>
|
|
@@ -1547,7 +1678,7 @@ const nf = data.ui.notFound;
|
|
|
1547
1678
|
<p class="text-muted-foreground">{nf.description}</p>
|
|
1548
1679
|
<a
|
|
1549
1680
|
class="mt-2 rounded-md bg-accent px-4 py-2 text-sm font-medium text-accent-foreground"
|
|
1550
|
-
href="/">{nf.home}</a
|
|
1681
|
+
href={withBase("/")}>{nf.home}</a
|
|
1551
1682
|
>
|
|
1552
1683
|
</div>
|
|
1553
1684
|
</PageLayout>
|
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
|