blume 0.6.6 → 0.7.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/dist/cli/index.js +1180 -739
- package/dist/cli/index.js.map +52 -51
- package/dist/types/core/base-path.d.ts +38 -0
- package/dist/types/core/config-input.d.ts +74 -10
- package/dist/types/core/config.d.ts +3 -2
- package/dist/types/core/data.d.ts +2 -0
- package/dist/types/core/i18n-ui.d.ts +1 -3
- package/dist/types/core/schema.d.ts +95 -52
- package/dist/types/core/sources/types.d.ts +2 -0
- package/dist/types/core/types.d.ts +6 -1
- package/docs/02-deployment.mdx +16 -1
- package/docs/03-faq.mdx +8 -8
- package/docs/configuration/index.mdx +6 -0
- package/docs/content/components.mdx +29 -2
- package/docs/content/islands.mdx +8 -0
- package/docs/content/syntax.mdx +13 -0
- package/package.json +2 -1
- package/src/ai/agent-readability.ts +7 -2
- package/src/ai/ask.ts +12 -7
- package/src/ai/llms.ts +15 -4
- package/src/ai/mcp/data.ts +8 -4
- package/src/ai/mcp/server.ts +3 -0
- package/src/astro/component-slots.ts +5 -3
- package/src/astro/examples.ts +12 -7
- package/src/astro/generate.ts +317 -144
- package/src/astro/index.ts +5 -1
- package/src/astro/integration.ts +8 -4
- package/src/astro/islands.ts +11 -5
- package/src/astro/markdown-negotiation.ts +1 -1
- package/src/astro/pages.ts +8 -3
- package/src/astro/templates.ts +166 -19
- package/src/cli/commands/build.ts +32 -19
- package/src/cli/commands/dev.ts +48 -15
- package/src/cli/commands/doctor.ts +2 -2
- package/src/cli/commands/validate.ts +1 -0
- package/src/cli/dev-lock.ts +26 -15
- package/src/cli/required-secrets.ts +2 -1
- 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/Update.astro +1 -1
- package/src/components/content/auto-type-table.ts +18 -8
- package/src/components/content/diff.ts +12 -6
- package/src/components/content/mermaid-element.ts +3 -0
- package/src/components/index.ts +23 -1
- package/src/components/islands/ask-ai.tsx +12 -6
- package/src/components/islands/base-path.ts +28 -0
- package/src/components/islands/hooks.ts +16 -1
- package/src/components/layout/Banner.astro +2 -1
- package/src/components/layout/Breadcrumbs.astro +2 -1
- package/src/components/layout/Favicon.astro +3 -2
- package/src/components/layout/Header.astro +2 -1
- package/src/components/layout/LanguageSwitcher.astro +2 -1
- package/src/components/layout/Logo.astro +2 -1
- package/src/components/layout/NavSelector.astro +2 -1
- package/src/components/layout/NavTree.astro +5 -4
- package/src/components/layout/PageFeedback.astro +4 -1
- package/src/components/layout/PageLayout.astro +9 -4
- package/src/components/layout/Pagination.astro +3 -2
- package/src/components/layout/RootLayout.astro +7 -4
- package/src/components/layout/Search.astro +13 -5
- package/src/components/layout/nav-utils.ts +18 -10
- package/src/components/layout/search/pagefind.ts +3 -0
- package/src/components/layout/toc-element.ts +7 -1
- package/src/components/openapi/RequestPanel.astro +7 -1
- package/src/components/openapi/snippets.ts +25 -11
- package/src/core/base-path.ts +70 -0
- package/src/core/component-overrides.ts +103 -74
- package/src/core/config-input.ts +81 -15
- package/src/core/config.ts +5 -3
- package/src/core/content.ts +2 -0
- package/src/core/data.ts +2 -0
- package/src/core/diagnostics.ts +54 -34
- package/src/core/gitignore.ts +4 -1
- package/src/core/graph.ts +156 -88
- package/src/core/i18n-ui.ts +18 -3
- package/src/core/last-modified.ts +2 -0
- package/src/core/links.ts +38 -18
- package/src/core/manifest.ts +62 -45
- package/src/core/nav-diagnostics.ts +1 -1
- package/src/core/navigation.ts +116 -55
- package/src/core/project-graph.ts +10 -9
- package/src/core/schema.ts +572 -621
- package/src/core/sources/github-releases.ts +2 -1
- package/src/core/sources/mdx-remote.ts +58 -54
- package/src/core/sources/normalize.ts +116 -73
- package/src/core/sources/notion.ts +19 -10
- 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/deploy/redirects.ts +18 -0
- package/src/deploy/robots.ts +6 -1
- package/src/deploy/rss.ts +10 -3
- package/src/deploy/sitemap.ts +14 -10
- package/src/markdown/base-links.ts +58 -0
- package/src/markdown/code-title.ts +11 -14
- package/src/markdown/index.ts +34 -9
- package/src/markdown/inline-code.ts +7 -2
- package/src/markdown/themes.ts +24 -0
- package/src/openapi/model.ts +3 -1
- package/src/openapi/references.ts +41 -17
- package/src/openapi/render-mdx.ts +11 -6
- package/src/openapi/scalar.ts +32 -16
- package/src/registry/eject.ts +64 -8
- package/src/search/build.ts +3 -0
- package/src/search/documents.ts +2 -2
- package/src/search/sync/typesense.ts +6 -4
- package/src/seo/jsonld.ts +16 -6
- package/src/theme/entry.ts +86 -21
|
@@ -162,10 +162,14 @@ export interface NavSelectorItem {
|
|
|
162
162
|
icon?: string;
|
|
163
163
|
tag?: string;
|
|
164
164
|
}
|
|
165
|
+
/** Context-partition selector kinds (a versioned/localized/multi-product site). */
|
|
166
|
+
type NavSelectorContextKind = "product" | "version";
|
|
167
|
+
/** What a top-level partition selector switches between. */
|
|
168
|
+
type NavSelectorKind = "dropdown" | "language" | NavSelectorContextKind;
|
|
165
169
|
/** Top-level partition selectors (products, versions, languages). */
|
|
166
170
|
export interface NavSelector {
|
|
167
171
|
label: string;
|
|
168
|
-
kind:
|
|
172
|
+
kind: NavSelectorKind;
|
|
169
173
|
items: NavSelectorItem[];
|
|
170
174
|
}
|
|
171
175
|
/** A pinned link rendered above the sidebar sections (external or internal). */
|
|
@@ -252,3 +256,4 @@ export interface BlumeManifest {
|
|
|
252
256
|
output: ResolvedConfig["deployment"]["output"];
|
|
253
257
|
routes: RouteManifestEntry[];
|
|
254
258
|
}
|
|
259
|
+
export {};
|
package/docs/02-deployment.mdx
CHANGED
|
@@ -54,7 +54,7 @@ blume preview
|
|
|
54
54
|
|
|
55
55
|
## Subpath deploys
|
|
56
56
|
|
|
57
|
-
Serving docs under a path like `example.com/docs`? Set `deployment.base` — common for GitHub Pages project sites.
|
|
57
|
+
Serving docs under a path like `example.com/docs`? Set `deployment.base` — common for GitHub Pages project sites. The whole site, root included, moves under the base, and internal links and assets are rewritten to include it.
|
|
58
58
|
|
|
59
59
|
```ts blume.config.ts lineNumbers
|
|
60
60
|
deployment: {
|
|
@@ -62,6 +62,21 @@ deployment: {
|
|
|
62
62
|
}
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
## Mount the docs under a path
|
|
66
|
+
|
|
67
|
+
`basePath` mounts every generated route under a segment (`/docs/getting-started`) while leaving the sidebar untouched — the top level is your sections, not a wrapper group. Use it when the docs live at `/docs/*` but the site root stays yours (like Docusaurus `routeBasePath` or Fumadocs `baseUrl`).
|
|
68
|
+
|
|
69
|
+
```ts blume.config.ts lineNumbers
|
|
70
|
+
basePath: "/docs",
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Write links as if mounted at root (`/getting-started`); Blume rewrites them, along with redirects, the sitemap, canonical URLs, Open Graph images, `llms.txt`, and the search index. Public assets (images, files under `public/`) stay at the site root.
|
|
74
|
+
|
|
75
|
+
This is a distinct concept from the two paths above:
|
|
76
|
+
|
|
77
|
+
- A per-source [`prefix`](/docs/content/sources#multiple-sources) namespaces **one** source and **does** add a sidebar group.
|
|
78
|
+
- `deployment.base` is the host subdirectory the **whole** app is served from. The two compose — with both set, a page lands at `{deployment.base}/{basePath}/page`.
|
|
79
|
+
|
|
65
80
|
## Server rendering
|
|
66
81
|
|
|
67
82
|
Static output covers most docs. Switch to server output when you need request-time features — most notably the [Ask AI](/docs/configuration/ai) endpoint:
|
package/docs/03-faq.mdx
CHANGED
|
@@ -87,13 +87,13 @@ We reported it upstream in [oxc-project/oxc#24096](https://github.com/oxc-projec
|
|
|
87
87
|
|
|
88
88
|
Patch oxfmt so it preserves the line break that sits directly against a `:::` fence. Blume ships exactly this patch in its own repo, and you can apply the same one in any project.
|
|
89
89
|
|
|
90
|
-
1. Save the patch as `patches/oxfmt@0.
|
|
90
|
+
1. Save the patch as `patches/oxfmt@0.57.0.patch`:
|
|
91
91
|
|
|
92
|
-
```diff patches/oxfmt@0.
|
|
93
|
-
diff --git a/dist/markdown-
|
|
94
|
-
index
|
|
95
|
-
--- a/dist/markdown-
|
|
96
|
-
+++ b/dist/markdown-
|
|
92
|
+
```diff patches/oxfmt@0.57.0.patch
|
|
93
|
+
diff --git a/dist/markdown-B5hFVJKQ.js b/dist/markdown-B5hFVJKQ.js
|
|
94
|
+
index 58322b247b263f87975c2a97eecfe0b97d7143c4..0f39f1901603026a391c6d343cb838dfaccbfd03 100644
|
|
95
|
+
--- a/dist/markdown-B5hFVJKQ.js
|
|
96
|
+
+++ b/dist/markdown-B5hFVJKQ.js
|
|
97
97
|
@@ -1141,7 +1141,14 @@ function Yf(e, r, t) {
|
|
98
98
|
case "sentence": return Yi(e, t);
|
|
99
99
|
case "word": return Li(e);
|
|
@@ -117,7 +117,7 @@ Patch oxfmt so it preserves the line break that sits directly against a `:::` fe
|
|
|
117
117
|
```json package.json
|
|
118
118
|
{
|
|
119
119
|
"patchedDependencies": {
|
|
120
|
-
"oxfmt@0.
|
|
120
|
+
"oxfmt@0.57.0": "patches/oxfmt@0.57.0.patch"
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
```
|
|
@@ -128,5 +128,5 @@ Patch oxfmt so it preserves the line break that sits directly against a `:::` fe
|
|
|
128
128
|
bun install
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
-
:::warning[Version-pinned] The patch targets oxfmt `0.
|
|
131
|
+
:::warning[Version-pinned] The patch targets oxfmt `0.57.0` — its diff references a file hashed to that build (`dist/markdown-B5hFVJKQ.js`). When you bump oxfmt, regenerate the patch (e.g. `bun patch oxfmt`) or check whether the upstream fix has landed and the patch is no longer needed.
|
|
132
132
|
:::
|
|
@@ -53,6 +53,12 @@ export default defineConfig({
|
|
|
53
53
|
icons: true, // language icon in the code-block header
|
|
54
54
|
wrap: false, // wrap long lines instead of scrolling
|
|
55
55
|
},
|
|
56
|
+
codeBlocks: {
|
|
57
|
+
theme: {
|
|
58
|
+
light: "github-light", // any bundled Shiki theme
|
|
59
|
+
dark: "github-dark",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
56
62
|
},
|
|
57
63
|
|
|
58
64
|
// AI — see the AI guide
|
|
@@ -546,7 +546,33 @@ A card linking to a GitHub repository with its live star and fork counts. Counts
|
|
|
546
546
|
|
|
547
547
|
`Component` renders an example file from your project's `examples/` directory as a live preview alongside its highlighted source, in tabs. Point it at a file with `path` — its location under `examples/`, without the extension (so `examples/counter.tsx` is `path="counter"`). React, Vue, Svelte, and Astro examples are all supported; framework examples hydrate, Astro ones render statically. It keeps the preview and the code in sync from a single file.
|
|
548
548
|
|
|
549
|
-
The
|
|
549
|
+
The preview renders in an isolated frame that the docs styles never reach — no prose margins, typography, or theme chrome bleed into your component. The frame gets Tailwind (preflight + utilities scanned from your example files and anything they import), Blume's design tokens so classes like `bg-background` follow the site palette by default, and it follows the site's light/dark toggle live.
|
|
550
|
+
|
|
551
|
+
To style previews with your own design system — say, shadcn variables — point `examples.css` at a stylesheet. It's injected into every preview frame after Blume's defaults, so your tokens win. Don't `@import "tailwindcss"` in it; the frame already provides Tailwind. Both `.dark` and `[data-theme="dark"]` work for dark-mode overrides:
|
|
552
|
+
|
|
553
|
+
```ts
|
|
554
|
+
// blume.config.ts
|
|
555
|
+
export default defineConfig({
|
|
556
|
+
examples: { css: "examples/theme.css" },
|
|
557
|
+
});
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
```css
|
|
561
|
+
/* examples/theme.css */
|
|
562
|
+
:root {
|
|
563
|
+
--primary: oklch(0.6 0.2 260);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
.dark {
|
|
567
|
+
--primary: oklch(0.75 0.15 260);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
@theme inline {
|
|
571
|
+
--color-primary: var(--primary);
|
|
572
|
+
}
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
The directory is configurable too — set `source` (or use the string shorthand, `examples: "..."`) when your examples live elsewhere (e.g. a registry layout). `path` is always relative to it:
|
|
550
576
|
|
|
551
577
|
```ts
|
|
552
578
|
// blume.config.ts
|
|
@@ -593,6 +619,7 @@ An Astro example renders live with no client JavaScript:
|
|
|
593
619
|
|
|
594
620
|
<CodeBlock
|
|
595
621
|
lang="ts"
|
|
622
|
+
icons={false}
|
|
596
623
|
code={`export const greet = (name: string): string =>
|
|
597
624
|
\`Hello, \${name}!\`;`}
|
|
598
625
|
/>
|
|
@@ -602,7 +629,7 @@ An Astro example renders live with no client JavaScript:
|
|
|
602
629
|
import CodeBlock from "blume/components/content/CodeBlock.astro";
|
|
603
630
|
---
|
|
604
631
|
|
|
605
|
-
<CodeBlock lang="ts" code={source} />
|
|
632
|
+
<CodeBlock lang="ts" code={source} icons={false} />
|
|
606
633
|
```
|
|
607
634
|
|
|
608
635
|
To highlight to an HTML string yourself (e.g. inside your own component), import the underlying helper from `blume/markdown`:
|
package/docs/content/islands.mdx
CHANGED
|
@@ -77,6 +77,14 @@ export default function Chart() {
|
|
|
77
77
|
|
|
78
78
|
**React works out of the box** — Blume turns it on automatically the moment your project contains a `.tsx`/`.jsx` island.
|
|
79
79
|
|
|
80
|
+
The [React Compiler](https://react.dev/learn/react-compiler) is on by default whenever React is enabled, so your islands are auto-memoized — no hand-written `useMemo`/`useCallback` needed. It ships with Blume; there's nothing to install. Opt out in `blume.config.ts`:
|
|
81
|
+
|
|
82
|
+
```ts blume.config.ts
|
|
83
|
+
export default defineConfig({
|
|
84
|
+
react: { compiler: false },
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
80
88
|
**Vue and Svelte** are supported too; install the matching Astro integration and Blume wires up the renderer when it sees a `.vue` or `.svelte` island:
|
|
81
89
|
|
|
82
90
|
```bash
|
package/docs/content/syntax.mdx
CHANGED
|
@@ -33,6 +33,7 @@ For footnote markers, ordinals, and scientific or chemical notation inline.
|
|
|
33
33
|
|
|
34
34
|
E = mc^2^ and H~2~O.
|
|
35
35
|
|
|
36
|
+
{/* prettier-ignore */}
|
|
36
37
|
```md
|
|
37
38
|
E = mc^2^ and H~2~O.
|
|
38
39
|
```
|
|
@@ -141,6 +142,18 @@ export default defineConfig({
|
|
|
141
142
|
|
|
142
143
|
Inline code can be highlighted too: add a `{:lang}` marker inside a backtick span and it's colored like a tiny code block — `useState(){:js}` or `T extends object{:ts}`. It only kicks in when you add the marker, so plain inline code stays untouched — nothing to switch on.
|
|
143
144
|
|
|
145
|
+
Highlighting uses the `github-light`/`github-dark` themes by default. Swap in any [bundled Shiki theme](https://shiki.style/themes) per color mode with `markdown.codeBlocks.theme` — it colors every code surface at once (fences, inline snippets, `<CodeBlock>`, and `<Diff>`):
|
|
146
|
+
|
|
147
|
+
```ts blume.config.ts
|
|
148
|
+
export default defineConfig({
|
|
149
|
+
markdown: {
|
|
150
|
+
codeBlocks: {
|
|
151
|
+
theme: { light: "github-light", dark: "vesper" },
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
144
157
|
### Line numbers
|
|
145
158
|
|
|
146
159
|
Append `lineNumbers` to render a line-number gutter — on its own or alongside a title:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blume",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Documentation that's fast, AI-ready, and zero-config.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astro",
|
|
@@ -89,6 +89,7 @@
|
|
|
89
89
|
"@vercel/analytics": "^2.0.1",
|
|
90
90
|
"ai": "^5.0.0",
|
|
91
91
|
"astro": "^7.0.2",
|
|
92
|
+
"babel-plugin-react-compiler": "^1.0.0",
|
|
92
93
|
"citty": "^0.1.6",
|
|
93
94
|
"consola": "^3.4.0",
|
|
94
95
|
"deepmerge": "^4.3.1",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeBasePath, withBasePath } from "../core/base-path.ts";
|
|
1
2
|
import type { BlumeProject } from "../core/project-graph.ts";
|
|
2
3
|
import type { ContentSignalPolicy, ContentSignals } from "../core/schema.ts";
|
|
3
4
|
import { buildRssFeeds } from "../deploy/rss.ts";
|
|
@@ -42,9 +43,13 @@ export const buildAgentReadability = (
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
const site = config.deployment.site ?? null;
|
|
45
|
-
//
|
|
46
|
+
// Every artifact is served under `deployment.base`; concatenate rather than
|
|
47
|
+
// `new URL()` so the subpath is preserved.
|
|
48
|
+
const deployBase = normalizeBasePath(config.deployment.base);
|
|
46
49
|
const abs = (path: string): string =>
|
|
47
|
-
site
|
|
50
|
+
site
|
|
51
|
+
? `${site.replace(/\/+$/u, "")}${withBasePath(deployBase, path)}`
|
|
52
|
+
: path;
|
|
48
53
|
|
|
49
54
|
const artifacts: Record<string, unknown> = {
|
|
50
55
|
markdown: {
|
package/src/ai/ask.ts
CHANGED
|
@@ -30,20 +30,25 @@ interface AskPreset {
|
|
|
30
30
|
* dedicated AI SDK provider; LLMGateway and Inkeep are OpenAI-compatible
|
|
31
31
|
* endpoints with no dedicated provider, so they reuse `@ai-sdk/openai-compatible`.
|
|
32
32
|
*/
|
|
33
|
+
/** The discriminant/name shared by the OpenAI-compatible providers. */
|
|
34
|
+
const OPENAI_COMPATIBLE = "openai-compatible";
|
|
35
|
+
/** The AI SDK provider package the OpenAI-compatible providers install. */
|
|
36
|
+
const OPENAI_COMPATIBLE_DEP = "@ai-sdk/openai-compatible";
|
|
37
|
+
|
|
33
38
|
const ASK_PRESETS: Record<string, AskPreset> = {
|
|
34
39
|
inkeep: {
|
|
35
40
|
apiKeyEnv: "INKEEP_API_KEY",
|
|
36
41
|
baseUrl: "https://api.inkeep.com/v1",
|
|
37
|
-
kind:
|
|
42
|
+
kind: OPENAI_COMPATIBLE,
|
|
38
43
|
name: "inkeep",
|
|
39
|
-
runtimeDep:
|
|
44
|
+
runtimeDep: OPENAI_COMPATIBLE_DEP,
|
|
40
45
|
},
|
|
41
46
|
llmgateway: {
|
|
42
47
|
apiKeyEnv: "LLMGATEWAY_API_KEY",
|
|
43
48
|
baseUrl: "https://api.llmgateway.io/v1",
|
|
44
|
-
kind:
|
|
49
|
+
kind: OPENAI_COMPATIBLE,
|
|
45
50
|
name: "llmgateway",
|
|
46
|
-
runtimeDep:
|
|
51
|
+
runtimeDep: OPENAI_COMPATIBLE_DEP,
|
|
47
52
|
},
|
|
48
53
|
openrouter: {
|
|
49
54
|
apiKeyEnv: "OPENROUTER_API_KEY",
|
|
@@ -73,9 +78,9 @@ export const resolveAskBackend = (ask?: AskAiConfig): AskBackend => {
|
|
|
73
78
|
return {
|
|
74
79
|
apiKeyEnv,
|
|
75
80
|
baseUrl: ask?.baseUrl ?? preset?.baseUrl ?? "",
|
|
76
|
-
kind:
|
|
81
|
+
kind: OPENAI_COMPATIBLE,
|
|
77
82
|
model,
|
|
78
|
-
name: preset?.name ??
|
|
83
|
+
name: preset?.name ?? OPENAI_COMPATIBLE,
|
|
79
84
|
};
|
|
80
85
|
};
|
|
81
86
|
|
|
@@ -89,5 +94,5 @@ export const askBackendRuntimeDep = (ask?: AskAiConfig): string | undefined => {
|
|
|
89
94
|
if (provider === "gateway") {
|
|
90
95
|
return undefined;
|
|
91
96
|
}
|
|
92
|
-
return ASK_PRESETS[provider]?.runtimeDep ??
|
|
97
|
+
return ASK_PRESETS[provider]?.runtimeDep ?? OPENAI_COMPATIBLE_DEP;
|
|
93
98
|
};
|
package/src/ai/llms.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import { normalizeBasePath, withBasePath } from "../core/base-path.ts";
|
|
1
2
|
import matter from "../core/frontmatter.ts";
|
|
2
3
|
import type { BlumeProject } from "../core/project-graph.ts";
|
|
3
4
|
import { readEntryText } from "../core/sources/read.ts";
|
|
4
5
|
import type { PageRecord } from "../core/types.ts";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
// Routes carry `basePath`; a `deployment.base` subdirectory is layered on top so
|
|
8
|
+
// the emitted URL matches where the page is served.
|
|
9
|
+
const pageUrl = (route: string, site?: string, base = ""): string => {
|
|
7
10
|
if (!site) {
|
|
8
11
|
return route;
|
|
9
12
|
}
|
|
10
|
-
return `${site.replace(/\/$/u, "")}${route}`;
|
|
13
|
+
return `${site.replace(/\/$/u, "")}${withBasePath(base, route)}`;
|
|
11
14
|
};
|
|
12
15
|
|
|
13
16
|
const orderedPages = (project: BlumeProject): PageRecord[] =>
|
|
@@ -26,7 +29,11 @@ const buildIndex = (project: BlumeProject): string => {
|
|
|
26
29
|
lines.push("", "## Docs", "");
|
|
27
30
|
|
|
28
31
|
for (const page of orderedPages(project)) {
|
|
29
|
-
const url = pageUrl(
|
|
32
|
+
const url = pageUrl(
|
|
33
|
+
page.route,
|
|
34
|
+
site,
|
|
35
|
+
normalizeBasePath(config.deployment.base)
|
|
36
|
+
);
|
|
30
37
|
const summary = page.description ? `: ${page.description}` : "";
|
|
31
38
|
lines.push(`- [${page.title}](${url})${summary}`);
|
|
32
39
|
}
|
|
@@ -43,7 +50,11 @@ const buildFull = async (project: BlumeProject): Promise<string> => {
|
|
|
43
50
|
pages.map(async (page) => {
|
|
44
51
|
const raw = await readEntryText(project, page);
|
|
45
52
|
const body = matter(raw).content.trim();
|
|
46
|
-
const url = pageUrl(
|
|
53
|
+
const url = pageUrl(
|
|
54
|
+
page.route,
|
|
55
|
+
config.deployment.site,
|
|
56
|
+
normalizeBasePath(config.deployment.base)
|
|
57
|
+
);
|
|
47
58
|
return [`# ${page.title}`, `Source: ${url}`, "", body].join("\n");
|
|
48
59
|
})
|
|
49
60
|
);
|
package/src/ai/mcp/data.ts
CHANGED
|
@@ -45,16 +45,20 @@ export const buildMcpData = async (project: BlumeProject): Promise<McpData> => {
|
|
|
45
45
|
graph.pages.map((page) => [page.id, page.description])
|
|
46
46
|
);
|
|
47
47
|
|
|
48
|
-
const routes: McpRoute[] =
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
const routes: McpRoute[] = [];
|
|
49
|
+
for (const route of manifest.routes) {
|
|
50
|
+
if (route.hidden) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
routes.push({
|
|
51
54
|
contentType: route.contentType,
|
|
52
55
|
description: descriptionById.get(route.id),
|
|
53
56
|
indexable: route.indexable,
|
|
54
57
|
lastModified: route.lastModified ?? null,
|
|
55
58
|
route: route.path,
|
|
56
59
|
title: route.title,
|
|
57
|
-
})
|
|
60
|
+
});
|
|
61
|
+
}
|
|
58
62
|
|
|
59
63
|
return {
|
|
60
64
|
documents: documents.map((doc) => ({
|
package/src/ai/mcp/server.ts
CHANGED
|
@@ -209,6 +209,9 @@ export const createMcpFetchHandler = (
|
|
|
209
209
|
const server = buildServer(data, index);
|
|
210
210
|
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
211
211
|
enableJsonResponse: true,
|
|
212
|
+
// The SDK enables stateless mode only when this is `undefined`; `null` is
|
|
213
|
+
// not an accepted value for the `(() => string) | undefined` option.
|
|
214
|
+
// oxlint-disable-next-line sonarjs/no-undefined-assignment
|
|
212
215
|
sessionIdGenerator: undefined,
|
|
213
216
|
});
|
|
214
217
|
await server.connect(transport);
|
|
@@ -41,6 +41,8 @@ export const layoutOverrides = {};
|
|
|
41
41
|
const attributeValue = (value: string): string =>
|
|
42
42
|
value.replaceAll(/["\n\r]/gu, " ").trim();
|
|
43
43
|
|
|
44
|
+
const CLIENT_LOAD = "client:load";
|
|
45
|
+
|
|
44
46
|
/** Astro client directive for a hydrated override. */
|
|
45
47
|
const directiveFor = (override: NormalizedOverride): string => {
|
|
46
48
|
const framework = override.source?.framework;
|
|
@@ -54,15 +56,15 @@ const directiveFor = (override: NormalizedOverride): string => {
|
|
|
54
56
|
case "media": {
|
|
55
57
|
return override.media
|
|
56
58
|
? `client:media="${attributeValue(override.media)}"`
|
|
57
|
-
:
|
|
59
|
+
: CLIENT_LOAD;
|
|
58
60
|
}
|
|
59
61
|
case "only": {
|
|
60
62
|
return framework
|
|
61
63
|
? `client:only="${attributeValue(framework)}"`
|
|
62
|
-
:
|
|
64
|
+
: CLIENT_LOAD;
|
|
63
65
|
}
|
|
64
66
|
default: {
|
|
65
|
-
return
|
|
67
|
+
return CLIENT_LOAD;
|
|
66
68
|
}
|
|
67
69
|
}
|
|
68
70
|
};
|
package/src/astro/examples.ts
CHANGED
|
@@ -59,10 +59,10 @@ const GLOB_MAGIC = /[!*?[\]{}]/u;
|
|
|
59
59
|
*/
|
|
60
60
|
const splitGlobBase = (pattern: string): { base: string; rest: string } => {
|
|
61
61
|
const segments = pattern.split("/");
|
|
62
|
+
// Only called when the pattern contains glob magic (see the caller), and `/`
|
|
63
|
+
// is never magic, so the magic char always lands in a segment — `findIndex`
|
|
64
|
+
// is never -1 here.
|
|
62
65
|
const firstMagic = segments.findIndex((segment) => GLOB_MAGIC.test(segment));
|
|
63
|
-
if (firstMagic === -1) {
|
|
64
|
-
return { base: pattern, rest: "" };
|
|
65
|
-
}
|
|
66
66
|
return {
|
|
67
67
|
base: segments.slice(0, firstMagic).join("/"),
|
|
68
68
|
rest: segments.slice(firstMagic).join("/"),
|
|
@@ -109,11 +109,13 @@ export const discoverExamples = async (
|
|
|
109
109
|
const warnings: string[] = [];
|
|
110
110
|
const seen = new Map<string, string>();
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
// Extracted so the two skip paths become early `return`s (one `continue`
|
|
113
|
+
// budget per loop under the lint rule) instead of `continue` statements.
|
|
114
|
+
const collectExample = (file: string, source: string): void => {
|
|
113
115
|
const ext = file.match(EXAMPLE_FILE)?.groups?.ext;
|
|
114
116
|
const framework = ext ? FRAMEWORK_BY_EXT[ext] : undefined;
|
|
115
117
|
if (!(ext && framework)) {
|
|
116
|
-
|
|
118
|
+
return;
|
|
117
119
|
}
|
|
118
120
|
// Strip the trailing `.<ext>` to form the `<Component path>` key.
|
|
119
121
|
const path = relative(dir, file).slice(0, -(ext.length + 1));
|
|
@@ -122,10 +124,9 @@ export const discoverExamples = async (
|
|
|
122
124
|
warnings.push(
|
|
123
125
|
`Two examples both resolve to "${path}" ("${existing}" and "${file}"); ignoring the second. Give them distinct paths.`
|
|
124
126
|
);
|
|
125
|
-
|
|
127
|
+
return;
|
|
126
128
|
}
|
|
127
129
|
seen.set(path, file);
|
|
128
|
-
const source = sources[index] ?? "";
|
|
129
130
|
examples.push({
|
|
130
131
|
client:
|
|
131
132
|
framework === "astro"
|
|
@@ -137,6 +138,10 @@ export const discoverExamples = async (
|
|
|
137
138
|
path,
|
|
138
139
|
source,
|
|
139
140
|
});
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
for (const [index, file] of files.entries()) {
|
|
144
|
+
collectExample(file, sources[index] ?? "");
|
|
140
145
|
}
|
|
141
146
|
|
|
142
147
|
return { examples, warnings };
|