blume 0.5.3 → 0.6.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 +3349 -7024
- package/dist/cli/index.js.map +39 -69
- package/dist/types/core/config.d.ts +0 -8
- package/dist/types/core/data.d.ts +6 -2
- package/dist/types/core/i18n-ui.d.ts +50 -0
- package/dist/types/core/schema.d.ts +379 -485
- package/dist/types/core/types.d.ts +8 -6
- package/docs/advanced/meta.ts +1 -8
- package/docs/advanced/skills.mdx +28 -0
- package/docs/configuration/ai.mdx +58 -0
- package/docs/configuration/index.mdx +13 -17
- package/docs/configuration/seo.mdx +59 -1
- package/docs/configuration/theming.mdx +15 -18
- package/docs/content/components.mdx +2 -53
- package/docs/content/i18n.mdx +0 -4
- package/docs/content/meta.mdx +3 -17
- package/docs/content/navigation.mdx +41 -4
- package/docs/content/syntax.mdx +1 -1
- package/docs/index.mdx +0 -1
- package/docs/reference/cli.mdx +12 -13
- package/package.json +6 -6
- package/skills/blume/SKILL.md +71 -0
- package/skills/blume-update-docs/SKILL.md +52 -0
- package/skills/blume-update-docs/references/audit-checklist.md +46 -0
- package/src/ai/agent-readability.ts +97 -0
- package/src/ai/ask-context.ts +131 -8
- package/src/ai/ask-data.ts +4 -1
- package/src/astro/generate.ts +19 -12
- package/src/astro/integration.ts +0 -21
- package/src/astro/templates.ts +33 -21
- package/src/cli/commands/build.ts +15 -0
- package/src/cli/commands/dev.ts +31 -20
- package/src/cli/commands/validate.ts +0 -2
- package/src/cli/dev-lock.ts +94 -21
- package/src/cli/index.ts +0 -2
- package/src/components/BlumePage.astro +0 -6
- package/src/components/Icon.astro +1 -12
- package/src/components/content/AccordionItem.astro +3 -6
- package/src/components/content/Badge.astro +1 -3
- package/src/components/content/Callout.astro +3 -9
- package/src/components/content/Card.astro +2 -3
- package/src/components/content/ColorItem.astro +2 -2
- package/src/components/content/Column.astro +1 -1
- package/src/components/content/GithubInfo.astro +11 -10
- package/src/components/content/Prompt.astro +1 -1
- package/src/components/content/Step.astro +3 -4
- package/src/components/content/Tab.astro +2 -3
- package/src/components/content/TypeTable.astro +13 -8
- package/src/components/content/Update.astro +1 -1
- package/src/components/islands/AskAI.astro +66 -2
- package/src/components/islands/ask-ai.tsx +289 -53
- package/src/components/layout/Header.astro +27 -4
- package/src/components/layout/Logo.astro +5 -1
- package/src/components/layout/NavSelector.astro +1 -1
- package/src/components/layout/NavTree.astro +15 -15
- package/src/components/layout/PageActions.astro +73 -30
- package/src/components/layout/PageLayout.astro +42 -0
- package/src/components/layout/ReferenceLayout.astro +1 -0
- package/src/components/layout/RootLayout.astro +79 -4
- package/src/components/layout/Search.astro +5 -5
- package/src/components/layout/nav-utils.ts +9 -4
- package/src/components/openapi/ApiOverview.astro +4 -50
- package/src/components/openapi/ApiTagOperations.astro +42 -0
- package/src/core/builtin-tags.ts +1 -3
- package/src/core/config.ts +5 -28
- package/src/core/data.ts +6 -2
- package/src/core/graph.ts +8 -6
- package/src/core/i18n-ui.ts +5 -0
- package/src/core/links.ts +5 -19
- package/src/core/meta.ts +1 -1
- package/src/core/nav-diagnostics.ts +7 -0
- package/src/core/navigation.ts +38 -17
- package/src/core/project-graph.ts +0 -5
- package/src/core/schema.ts +133 -95
- package/src/core/sources/filesystem.ts +5 -1
- package/src/core/sources/resolve.ts +0 -13
- package/src/core/sources/watch.ts +43 -11
- package/src/core/types.ts +8 -6
- package/src/deploy/robots.ts +37 -4
- package/src/openapi/parse.ts +197 -14
- package/src/openapi/render-mdx.ts +44 -10
- package/src/openapi/scalar.ts +1 -1
- package/src/openapi/source.ts +19 -2
- package/src/search/documents.ts +9 -2
- package/src/theme/entry.ts +45 -17
- package/src/theme/icons.ts +18 -109
- package/src/theme/palette.ts +25 -51
- package/src/theme/twoslash.ts +6 -1
- package/dist/types/core/bridge.d.ts +0 -24
- package/dist/types/core/package-json.d.ts +0 -12
- package/dist/types/migrate/mintlify/assets.d.ts +0 -8
- package/dist/types/migrate/mintlify/config.d.ts +0 -16
- package/dist/types/migrate/mintlify/i18n.d.ts +0 -7
- package/dist/types/migrate/shared.d.ts +0 -153
- package/docs/advanced/bridge.mdx +0 -76
- package/docs/advanced/migrate.mdx +0 -124
- package/src/astro/static-assets.ts +0 -124
- package/src/cli/commands/migrate.ts +0 -39
- package/src/components/content/ApiField.astro +0 -75
- package/src/components/content/ParamField.astro +0 -39
- package/src/components/content/RequestField.astro +0 -23
- package/src/components/content/ResponseField.astro +0 -23
- package/src/components/content/Warning.astro +0 -9
- package/src/core/assets.ts +0 -31
- package/src/core/bridge.ts +0 -102
- package/src/core/sources/mintlify.ts +0 -190
- package/src/migrate/fumadocs/config.ts +0 -155
- package/src/migrate/fumadocs/content.ts +0 -376
- package/src/migrate/fumadocs/frontmatter.ts +0 -18
- package/src/migrate/fumadocs/groups.ts +0 -237
- package/src/migrate/fumadocs/index.ts +0 -355
- package/src/migrate/fumadocs/meta.ts +0 -244
- package/src/migrate/migrate.ts +0 -53
- package/src/migrate/mintlify/assets.ts +0 -46
- package/src/migrate/mintlify/config.ts +0 -954
- package/src/migrate/mintlify/content.ts +0 -120
- package/src/migrate/mintlify/frontmatter.ts +0 -126
- package/src/migrate/mintlify/i18n.ts +0 -51
- package/src/migrate/mintlify/icons.ts +0 -128
- package/src/migrate/mintlify/index.ts +0 -459
- package/src/migrate/mintlify/snippets.ts +0 -315
- package/src/migrate/mintlify/transform.ts +0 -82
- package/src/migrate/nextra/content.ts +0 -46
- package/src/migrate/nextra/frontmatter.ts +0 -40
- package/src/migrate/nextra/index.ts +0 -389
- package/src/migrate/nextra/meta.ts +0 -266
- package/src/migrate/shared.ts +0 -801
- package/src/migrate/starlight/config.ts +0 -455
- package/src/migrate/starlight/content.ts +0 -75
- package/src/migrate/starlight/frontmatter.ts +0 -111
- package/src/migrate/starlight/i18n.ts +0 -54
- package/src/migrate/starlight/index.ts +0 -131
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blume",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Documentation that's fast, AI-ready, and zero-config.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astro",
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"dist",
|
|
29
29
|
"src",
|
|
30
30
|
"bin",
|
|
31
|
-
"docs"
|
|
31
|
+
"docs",
|
|
32
|
+
"skills"
|
|
32
33
|
],
|
|
33
34
|
"type": "module",
|
|
34
35
|
"types": "./dist/types/index.d.ts",
|
|
@@ -70,11 +71,7 @@
|
|
|
70
71
|
"@astrojs/node": "^11.0.0",
|
|
71
72
|
"@astrojs/react": "^6.0.0",
|
|
72
73
|
"@astrojs/vercel": "^11.0.0",
|
|
73
|
-
"@iconify-json/fa6-brands": "^1.2.6",
|
|
74
|
-
"@iconify-json/fa6-regular": "^1.2.4",
|
|
75
|
-
"@iconify-json/fa6-solid": "^1.2.4",
|
|
76
74
|
"@iconify-json/lucide": "^1.2.115",
|
|
77
|
-
"@iconify-json/tabler": "^1.2.35",
|
|
78
75
|
"@iconify/types": "^2.0.0",
|
|
79
76
|
"@iconify/utils": "^3.1.3",
|
|
80
77
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
@@ -95,12 +92,14 @@
|
|
|
95
92
|
"citty": "^0.1.6",
|
|
96
93
|
"consola": "^3.4.0",
|
|
97
94
|
"deepmerge": "^4.3.1",
|
|
95
|
+
"dompurify": "^3.4.11",
|
|
98
96
|
"epub-gen-memory": "^1.1.2",
|
|
99
97
|
"github-slugger": "^2.0.0",
|
|
100
98
|
"gray-matter": "^4.0.3",
|
|
101
99
|
"jiti": "^2.4.0",
|
|
102
100
|
"js-yaml": "^4.1.0",
|
|
103
101
|
"katex": "^0.17.0",
|
|
102
|
+
"marked": "^18.0.5",
|
|
104
103
|
"mermaid": "^11.15.0",
|
|
105
104
|
"pagefind": "^1.3.0",
|
|
106
105
|
"pathe": "^2.0.0",
|
|
@@ -111,6 +110,7 @@
|
|
|
111
110
|
"tailwindcss": "^4",
|
|
112
111
|
"tinyglobby": "^0.2.10",
|
|
113
112
|
"typescript": "^5.7.0",
|
|
113
|
+
"undici": "^8.6.0",
|
|
114
114
|
"zod": "^3.24.0"
|
|
115
115
|
},
|
|
116
116
|
"devDependencies": {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: blume
|
|
3
|
+
description: Build and maintain documentation sites with Blume, the markdown-first docs framework on Astro and Vite. Use when working in a project that depends on `blume`, when scaffolding or configuring a docs site, writing Markdown/MDX content, tuning navigation/search/theming/SEO/AI features, running the `blume` CLI (init, dev, build, eject), or editing `blume.config.ts` and `meta.ts` files.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Blume
|
|
7
|
+
|
|
8
|
+
Blume is an open-source, **markdown-first** documentation framework built on Astro and Vite. Drop Markdown or MDX into a folder, run `blume dev`, and get a production-grade docs site — navigation, search, theming, Open Graph images, and a rich component library — with no app boilerplate to write or maintain.
|
|
9
|
+
|
|
10
|
+
The core idea: **the framework _is_ the template.** There's no starter to clone and no project to own before you've written a word. The only thing you touch is your content. When you outgrow the defaults, you add configuration one file at a time — and you can `blume eject` to a plain Astro project the day you want full control.
|
|
11
|
+
|
|
12
|
+
## What makes it different
|
|
13
|
+
|
|
14
|
+
- **Fast by default** — Static HTML on Astro/Vite. The core theme ships no client framework JS so pages score well on Core Web Vitals out of the box. You opt into server features only when you need them.
|
|
15
|
+
- **AI-ready out of the box** — Emits `llms.txt`/`llms-full.txt`, serves any page's raw Markdown by appending `.md` to its URL, offers **Copy as Markdown** and **Open in chat** on every page, and can host an optional **Ask AI** assistant or an **MCP server** so coding agents read your docs directly.
|
|
16
|
+
- **Zero configuration — even the template** — A folder of docs is a complete project. Navigation is inferred from files, search works in dev and production with no hosted service, and theming is a handful of tokens.
|
|
17
|
+
- **Type-safe to the core** — `blume.config.ts` and every `meta.ts` are real TypeScript, validated by a schema and authored with `defineConfig` and `defineMeta`. Your editor autocompletes options and catches mistakes before a build.
|
|
18
|
+
|
|
19
|
+
## Quickstart
|
|
20
|
+
|
|
21
|
+
Blume needs **Node.js 22 or newer**. From an empty or existing project:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm i blume # install the package
|
|
25
|
+
blume init # scaffold: docs/index.mdx + blume.config.ts
|
|
26
|
+
blume dev # dev server with hot reload
|
|
27
|
+
blume build # static HTML to dist/, with a local search index
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Blume works with any package manager and never requires you to set up Astro or Tailwind yourself.
|
|
31
|
+
|
|
32
|
+
### Writing a page
|
|
33
|
+
|
|
34
|
+
Every page is Markdown or MDX with a little frontmatter. The `title` and `description` render as the page heading and intro automatically; built-in components (callouts, cards, tabs, steps, and more) need **no imports**.
|
|
35
|
+
|
|
36
|
+
```mdx
|
|
37
|
+
---
|
|
38
|
+
title: Introduction
|
|
39
|
+
description: Welcome to my docs.
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
Welcome! Use **Markdown** and built-in components — no imports required:
|
|
43
|
+
|
|
44
|
+
:::note Blume ships callouts, cards, tabs, steps, and more. :::
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Navigation, search, and page metadata are inferred from your files as you add them.
|
|
48
|
+
|
|
49
|
+
## What's included
|
|
50
|
+
|
|
51
|
+
- **Components** — callouts, cards, steps, tabs, accordions, badges, file trees, and parameter tables, usable in MDX with no imports.
|
|
52
|
+
- **Local search** — Orama in dev and production; Pagefind is one flag away for large sites. No hosted index.
|
|
53
|
+
- **AI** — `llms.txt`, raw Markdown URLs, Copy as Markdown, Open in chat, an Ask AI assistant, and a hosted MCP server.
|
|
54
|
+
- **Navigation** — inferred from files, refined with `meta.ts` or config.
|
|
55
|
+
- **SEO** — metadata, Open Graph images, RSS feeds, and JSON-LD.
|
|
56
|
+
- **Customization** — component overrides, React islands, custom pages, theme tokens, and a source-component registry via `blume add`.
|
|
57
|
+
- **Eject** — `blume eject` produces a standalone Astro project that still uses the `blume` package.
|
|
58
|
+
|
|
59
|
+
## How it works
|
|
60
|
+
|
|
61
|
+
The Blume CLI discovers your content, builds a content graph, and generates a hidden Astro project under `.blume/` that it drives for dev and build. The generated runtime is an implementation detail — you write Markdown, Blume handles the rest — until you choose to eject and own it.
|
|
62
|
+
|
|
63
|
+
## Full documentation
|
|
64
|
+
|
|
65
|
+
This is a high-level overview. For complete, authoritative docs — configuration reference, every CLI command and flag, component APIs, content authoring, navigation, search, SEO, AI features, theming, and deployment — read the bundled docs in the installed package:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
node_modules/blume/docs
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Start with `node_modules/blume/docs/index.mdx` (Introduction) and `node_modules/blume/docs/01-quickstart.mdx`, then browse the `configuration/`, `content/`, `reference/`, and `advanced/` sections for specifics.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: blume-update-docs
|
|
3
|
+
description: Keep a Blume docs site in sync with the product it documents. Audit recently merged pull requests, changelogs, config schemas, CLI help, and public APIs against the docs content, update only pages that are factually stale, verify the docs build, and open (or update) a maintenance pull request — or report a clean no-op. Use when asked to check docs for drift, refresh stale documentation, run a scheduled docs audit, or keep docs current after a release.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Update Blume Docs
|
|
7
|
+
|
|
8
|
+
Blume is a **markdown-first** documentation framework on Astro/Vite: content lives as Markdown/MDX under a content root (default `docs/`), navigation derives from the file tree plus optional `meta.ts` files, and `blume build` validates links, anchors, frontmatter, and routes.
|
|
9
|
+
|
|
10
|
+
Your job is **docs maintenance, not docs authorship**: find where shipped, user-facing behavior has drifted from what the docs claim, fix exactly that, prove the site still builds, and deliver the result as a pull request. A run that finds nothing actionable ends with a short report and **no branch, no commit, no PR** — prefer a no-op over a noisy PR.
|
|
11
|
+
|
|
12
|
+
## Ground rules
|
|
13
|
+
|
|
14
|
+
- **Only document what shipped.** Never invent features, timelines, pricing, APIs, or compatibility claims. Work behind a feature flag is not ready for docs unless the flag is enabled for the documented audience or the repo explicitly documents unreleased behavior.
|
|
15
|
+
- **Facts over polish.** Edit when a command, option, default, route, prop, or workflow is wrong or missing. Skip subjective rewording, marketing polish, restructuring, and formatting-only churn.
|
|
16
|
+
- **Smallest correct diff.** Touch the fewest pages that remove the drift. Preserve the site's voice, frontmatter style, component usage, and `meta.ts` navigation patterns.
|
|
17
|
+
- **Exact source-of-truth wording** for commands, flags, config keys, environment variables, routes, and version numbers — copy them from code, don't paraphrase from memory.
|
|
18
|
+
- **Respect the repo.** Follow `AGENTS.md`/`CLAUDE.md` conventions, don't touch generated output (`.blume/`, `dist/`), and never overwrite unrelated local changes.
|
|
19
|
+
|
|
20
|
+
## Workflow
|
|
21
|
+
|
|
22
|
+
1. **Establish context.**
|
|
23
|
+
- Read the repo's agent/contributor instructions (`AGENTS.md`, `CLAUDE.md`, contribution docs) and honor them.
|
|
24
|
+
- Locate the docs app and content root: `blume.config.ts` (`content.root`), the directory of `.md`/`.mdx` pages, `meta.ts` files, and the package manager + docs build command.
|
|
25
|
+
- If this run was configured with a trigger, lookback window, docs path, target branch, or PR policy, honor those. Use the defaults below only where the prompt is silent.
|
|
26
|
+
|
|
27
|
+
2. **Reuse or create a maintenance branch.**
|
|
28
|
+
- If an open docs-maintenance PR from a previous run exists (head branch starting with `blume/`), check out and update that branch instead of opening a duplicate.
|
|
29
|
+
- Otherwise branch from the default branch as `blume/docs-refresh-YYYY-MM-DD`. Create the branch only once you know an edit is needed.
|
|
30
|
+
|
|
31
|
+
3. **Find drift.** Read `references/audit-checklist.md` for the full source list and change criteria, then:
|
|
32
|
+
- Review PRs merged into the default branch within the lookback window (default: the last 7 days) and extract the user-facing changes.
|
|
33
|
+
- Compare those changes — plus changelogs, release notes, config schemas, exported APIs, CLI help, and examples — against the docs content.
|
|
34
|
+
- Check external links only when a checked page depends on them; prefer official docs and release notes over secondary sources.
|
|
35
|
+
- Keep notes: what you checked, what changed upstream, and why each edit is (or isn't) needed.
|
|
36
|
+
|
|
37
|
+
4. **Update the docs.**
|
|
38
|
+
- Fix the stale pages. Add, rename, or remove `meta.ts` entries when pages are added, renamed, or deleted.
|
|
39
|
+
- Match the surrounding pages: frontmatter shape, Blume components already in use, code-fence style, root-relative internal links.
|
|
40
|
+
|
|
41
|
+
5. **Verify.**
|
|
42
|
+
- Run the docs build (`blume build` or the repo's documented docs QA) — it validates links, anchors, frontmatter, and duplicate routes.
|
|
43
|
+
- Run lint/format/typecheck when the repo's conventions call for them on docs changes.
|
|
44
|
+
- Fix failures your edits caused; report pre-existing failures separately instead of fixing them in this PR.
|
|
45
|
+
|
|
46
|
+
6. **Deliver.**
|
|
47
|
+
- **Changes made:** commit only the maintenance edits, push the `blume/*` branch, and open or update a PR against the default branch titled like `blume: refresh docs for YYYY-MM-DD`. In the body list sources checked, docs changed, verification commands and results, skipped checks, and residual risk.
|
|
48
|
+
- **No changes needed:** report the PRs and docs areas checked and the no-op result. Do not create a branch, commit, or PR.
|
|
49
|
+
|
|
50
|
+
## Resources
|
|
51
|
+
|
|
52
|
+
- `references/audit-checklist.md` — the source checklist, edit/skip criteria, and Blume-specific editing guidance. Read it before making docs changes.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Audit checklist
|
|
2
|
+
|
|
3
|
+
Use this checklist to decide whether a docs-maintenance run should make changes.
|
|
4
|
+
|
|
5
|
+
## Sources to check
|
|
6
|
+
|
|
7
|
+
- **Repo instructions:** `AGENTS.md`, `CLAUDE.md`, contribution docs, release docs, PR templates, and package scripts.
|
|
8
|
+
- **Blume config:** `blume.config.ts` — `content.root`, navigation (`meta.ts` files, tabs, selectors), site settings, AI/MCP settings, search, OpenAPI/AsyncAPI sources, theme, and export settings.
|
|
9
|
+
- **Public surface area:** exported package entrypoints, config schemas, component props, CLI commands and help text, route handlers, environment variables, and registry items.
|
|
10
|
+
- **User workflows:** quickstarts, examples, migration guides, deployment guides, screenshots, sample projects, and README snippets.
|
|
11
|
+
- **Recent merge signals:** PRs merged within the lookback window (default 7 days), changelogs, release notes, changesets, tags, and package version bumps.
|
|
12
|
+
- **Docs content:** every `.md`/`.mdx` page under the configured content root, plus custom pages and blog/changelog entries.
|
|
13
|
+
- **External dependencies:** official provider docs and release notes for linked integrations — only when the docs mention them or the dependency changed.
|
|
14
|
+
- **Generated docs surfaces:** `llms.txt`, raw Markdown URLs, MCP tools, OpenAPI pages, search, sitemap, robots, RSS, and OG behavior when relevant.
|
|
15
|
+
|
|
16
|
+
## Change criteria
|
|
17
|
+
|
|
18
|
+
Make a docs edit when at least one condition is true:
|
|
19
|
+
|
|
20
|
+
- A command, config option, environment variable, route, CLI flag, component prop, or default value changed.
|
|
21
|
+
- A documented workflow no longer works or misses a required step.
|
|
22
|
+
- A page promises a feature, provider, adapter, or integration the code no longer supports.
|
|
23
|
+
- A new user-facing capability shipped but is absent from the appropriate docs page.
|
|
24
|
+
- A link points at moved, removed, or outdated primary documentation.
|
|
25
|
+
- A changelog or release page needs an entry for shipped user-facing behavior.
|
|
26
|
+
|
|
27
|
+
Skip the edit when the only available change is subjective polish, wording preference, duplicated information, speculative future work, or behavior still hidden behind a feature flag.
|
|
28
|
+
|
|
29
|
+
## Blume editing guidance
|
|
30
|
+
|
|
31
|
+
- Keep frontmatter short and factual; use `title` and `description` consistently with nearby pages. Blume's frontmatter schema is **strict** — unknown keys are build errors.
|
|
32
|
+
- Preserve existing page order and `defineMeta` style; update `pages` arrays when adding, renaming, or removing pages.
|
|
33
|
+
- Use the Blume components already present in the docs (callout directives, steps, cards) instead of inventing new markup patterns.
|
|
34
|
+
- Match nearby code fences: filenames, language tags, and line numbers where the surrounding docs use them.
|
|
35
|
+
- Keep internal links root-relative (`/docs/...`).
|
|
36
|
+
- Do not edit generated `.blume/` or `dist/` output.
|
|
37
|
+
|
|
38
|
+
## PR notes
|
|
39
|
+
|
|
40
|
+
Include these sections in the PR body or no-op summary:
|
|
41
|
+
|
|
42
|
+
- Sources checked
|
|
43
|
+
- Docs changed
|
|
44
|
+
- Verification run (commands and results)
|
|
45
|
+
- Skipped checks, with reasons
|
|
46
|
+
- Remaining risk or follow-up
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { BlumeProject } from "../core/project-graph.ts";
|
|
2
|
+
import type { ContentSignalPolicy, ContentSignals } from "../core/schema.ts";
|
|
3
|
+
import { buildRssFeeds } from "../deploy/rss.ts";
|
|
4
|
+
|
|
5
|
+
/** Token map for the machine-readable content-usage echo. */
|
|
6
|
+
const USAGE_TOKENS: [keyof ContentSignalPolicy, string][] = [
|
|
7
|
+
["search", "search"],
|
|
8
|
+
["aiInput", "ai-input"],
|
|
9
|
+
["aiTrain", "ai-train"],
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The configured usage preferences as a `{ token: allowed }` object, or null
|
|
14
|
+
* when the declaration is disabled (`contentSignals: false`). Mirrors the
|
|
15
|
+
* robots.txt `Content-Signal` line so an agent that reads the manifest instead
|
|
16
|
+
* of robots.txt sees the same policy.
|
|
17
|
+
*/
|
|
18
|
+
const usagePolicy = (
|
|
19
|
+
signals: ContentSignals
|
|
20
|
+
): Record<string, boolean> | null => {
|
|
21
|
+
if (!signals) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return Object.fromEntries(
|
|
25
|
+
USAGE_TOKENS.map(([key, token]) => [token, signals[key]] as const)
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Build `agent-readability.json`: a root manifest that indexes the project's
|
|
31
|
+
* agent-facing surface — llms.txt, the raw-Markdown mirrors, the MCP server,
|
|
32
|
+
* Ask AI, sitemap, and feeds — so agents can discover and cite the docs without
|
|
33
|
+
* scraping HTML. URLs are absolute when a `site` is configured and root-relative
|
|
34
|
+
* otherwise. Returns null when the manifest is disabled.
|
|
35
|
+
*/
|
|
36
|
+
export const buildAgentReadability = (
|
|
37
|
+
project: BlumeProject
|
|
38
|
+
): Record<string, unknown> | null => {
|
|
39
|
+
const { config } = project;
|
|
40
|
+
if (!config.seo.agentReadability) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const site = config.deployment.site ?? null;
|
|
45
|
+
// Concatenate rather than `new URL()` so a subpath deployment's base is kept.
|
|
46
|
+
const abs = (path: string): string =>
|
|
47
|
+
site ? `${site.replace(/\/+$/u, "")}${path}` : path;
|
|
48
|
+
|
|
49
|
+
const artifacts: Record<string, unknown> = {
|
|
50
|
+
markdown: {
|
|
51
|
+
contentNegotiation: "text/markdown",
|
|
52
|
+
pattern: abs("/{route}.md"),
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
if (config.ai.llmsTxt) {
|
|
56
|
+
artifacts.llmsFullTxt = abs("/llms-full.txt");
|
|
57
|
+
artifacts.llmsTxt = abs("/llms.txt");
|
|
58
|
+
}
|
|
59
|
+
if (config.mcp.enabled) {
|
|
60
|
+
artifacts.mcp = {
|
|
61
|
+
discovery: abs("/.well-known/mcp.json"),
|
|
62
|
+
url: abs(config.mcp.route),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (config.ai.ask?.enabled) {
|
|
66
|
+
artifacts.askApi = abs("/api/ask");
|
|
67
|
+
}
|
|
68
|
+
if (site && config.seo.sitemap) {
|
|
69
|
+
artifacts.sitemap = abs("/sitemap.xml");
|
|
70
|
+
}
|
|
71
|
+
const feeds =
|
|
72
|
+
site && config.seo.rss.enabled
|
|
73
|
+
? buildRssFeeds(project).map((feed) => abs(feed.path))
|
|
74
|
+
: [];
|
|
75
|
+
if (feeds.length > 0) {
|
|
76
|
+
artifacts.feeds = feeds;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const version = project.manifest?.blumeVersion;
|
|
80
|
+
const manifest: Record<string, unknown> = {
|
|
81
|
+
artifacts,
|
|
82
|
+
description: config.description,
|
|
83
|
+
generator: version ? `blume@${version}` : undefined,
|
|
84
|
+
name: config.mcp.name ?? config.title,
|
|
85
|
+
site,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const usage = usagePolicy(config.seo.contentSignals);
|
|
89
|
+
if (usage) {
|
|
90
|
+
manifest.contentUsage = usage;
|
|
91
|
+
}
|
|
92
|
+
if (config.github) {
|
|
93
|
+
manifest.repository = `https://github.com/${config.github.owner}/${config.github.repo}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return manifest;
|
|
97
|
+
};
|
package/src/ai/ask-context.ts
CHANGED
|
@@ -26,16 +26,77 @@ export interface AskData {
|
|
|
26
26
|
/** Documents retrieved per question and injected into the system prompt. */
|
|
27
27
|
const MAX_RESULTS = 6;
|
|
28
28
|
/** Characters kept per injected excerpt. */
|
|
29
|
-
const EXCERPT_CHARS =
|
|
29
|
+
const EXCERPT_CHARS = 2000;
|
|
30
30
|
/** Overall cap on injected documentation characters. */
|
|
31
|
-
const CONTEXT_BUDGET =
|
|
31
|
+
const CONTEXT_BUDGET = 10_000;
|
|
32
|
+
/** Chars of lead-in kept before the matched region, for heading/sentence context. */
|
|
33
|
+
const EXCERPT_LEAD = 160;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Common words dropped from the retrieval query before locating the relevant
|
|
37
|
+
* excerpt region, so short filler ("how does…", "what is…") doesn't drag the
|
|
38
|
+
* window toward incidental matches instead of the meaningful terms.
|
|
39
|
+
*/
|
|
40
|
+
const STOPWORDS = new Set([
|
|
41
|
+
"about",
|
|
42
|
+
"and",
|
|
43
|
+
"are",
|
|
44
|
+
"as",
|
|
45
|
+
"at",
|
|
46
|
+
"be",
|
|
47
|
+
"but",
|
|
48
|
+
"by",
|
|
49
|
+
"can",
|
|
50
|
+
"do",
|
|
51
|
+
"does",
|
|
52
|
+
"for",
|
|
53
|
+
"from",
|
|
54
|
+
"how",
|
|
55
|
+
"in",
|
|
56
|
+
"into",
|
|
57
|
+
"is",
|
|
58
|
+
"it",
|
|
59
|
+
"its",
|
|
60
|
+
"my",
|
|
61
|
+
"of",
|
|
62
|
+
"on",
|
|
63
|
+
"or",
|
|
64
|
+
"our",
|
|
65
|
+
"that",
|
|
66
|
+
"the",
|
|
67
|
+
"these",
|
|
68
|
+
"this",
|
|
69
|
+
"those",
|
|
70
|
+
"to",
|
|
71
|
+
"use",
|
|
72
|
+
"used",
|
|
73
|
+
"using",
|
|
74
|
+
"was",
|
|
75
|
+
"were",
|
|
76
|
+
"what",
|
|
77
|
+
"when",
|
|
78
|
+
"where",
|
|
79
|
+
"which",
|
|
80
|
+
"who",
|
|
81
|
+
"why",
|
|
82
|
+
"with",
|
|
83
|
+
"you",
|
|
84
|
+
"your",
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
/** Distinct, meaningful lowercase terms from a query (drops stopwords). */
|
|
88
|
+
const queryTerms = (query: string): string[] =>
|
|
89
|
+
[...new Set(query.toLowerCase().match(/[a-z0-9]+/gu))].filter(
|
|
90
|
+
(term) => term.length >= 2 && !STOPWORDS.has(term)
|
|
91
|
+
);
|
|
32
92
|
|
|
33
93
|
/**
|
|
34
94
|
* The grounding preamble. The model is told to answer strictly from the injected
|
|
35
|
-
* excerpts and to cite the pages it used
|
|
95
|
+
* excerpts and to cite the pages it used as Markdown links (each excerpt is
|
|
96
|
+
* headed by `## Title (/route)`), so citations render as real links in the panel.
|
|
36
97
|
*/
|
|
37
98
|
const BASE_INSTRUCTION =
|
|
38
|
-
"You are a helpful documentation assistant for this project. Answer the user's question using ONLY the documentation excerpts below. If the answer is not covered by
|
|
99
|
+
"You are a helpful documentation assistant for this project. Answer the user's question using ONLY the documentation excerpts below. Each excerpt is headed by its page as `## Page Title (/route)`. If the answer is not covered by the excerpts, say you don't know and suggest where in the docs to look — do not invent details. Always cite the pages you drew from, and write every citation as a Markdown link to that page using its route, e.g. [Page Title](/route).";
|
|
39
100
|
|
|
40
101
|
/** Normalize a page path to a document `route` (`/`, `/a/b`, no trailing slash). */
|
|
41
102
|
const normalizeRoute = (input: string): string => {
|
|
@@ -55,10 +116,68 @@ const lastUserMessage = (messages: AskMessage[]): string => {
|
|
|
55
116
|
return "";
|
|
56
117
|
};
|
|
57
118
|
|
|
58
|
-
/**
|
|
59
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Excerpt the region of `content` most relevant to `query`, not just its head.
|
|
121
|
+
*
|
|
122
|
+
* Pages are indexed whole (one document each), so a naive head slice of a long
|
|
123
|
+
* page returns its intro and misses sections below the fold — the exact failure
|
|
124
|
+
* where "How does Ask AI work?" retrieves the right page but only sees its
|
|
125
|
+
* opening paragraph. This centers the window on the densest cluster of query
|
|
126
|
+
* terms so the injected text is the part that actually answers the question.
|
|
127
|
+
*/
|
|
128
|
+
const relevantExcerpt = (
|
|
129
|
+
content: string,
|
|
130
|
+
query: string,
|
|
131
|
+
max: number
|
|
132
|
+
): string => {
|
|
60
133
|
const trimmed = content.trim();
|
|
61
|
-
|
|
134
|
+
if (trimmed.length <= max) {
|
|
135
|
+
return trimmed;
|
|
136
|
+
}
|
|
137
|
+
const withEllipsis = (start: number): string => {
|
|
138
|
+
const slice = trimmed.slice(start, start + max).trim();
|
|
139
|
+
const prefix = start > 0 ? "…" : "";
|
|
140
|
+
const suffix = start + max < trimmed.length ? "…" : "";
|
|
141
|
+
return `${prefix}${slice}${suffix}`;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const lower = trimmed.toLowerCase();
|
|
145
|
+
const positions: number[] = [];
|
|
146
|
+
for (const term of queryTerms(query)) {
|
|
147
|
+
let idx = lower.indexOf(term);
|
|
148
|
+
while (idx !== -1) {
|
|
149
|
+
positions.push(idx);
|
|
150
|
+
idx = lower.indexOf(term, idx + term.length);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// No query terms hit this doc — nothing to center on, so keep the head.
|
|
154
|
+
if (positions.length === 0) {
|
|
155
|
+
return withEllipsis(0);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Pick the term hit whose following `max`-char window covers the most hits.
|
|
159
|
+
// `positions` is non-empty here, so the first window (count ≥ 1) always wins
|
|
160
|
+
// over the initial 0 and assigns a real offset to `best`.
|
|
161
|
+
positions.sort((a, b) => a - b);
|
|
162
|
+
let best = 0;
|
|
163
|
+
let bestCount = 0;
|
|
164
|
+
for (const start of positions) {
|
|
165
|
+
const end = start + max;
|
|
166
|
+
let count = 0;
|
|
167
|
+
for (const pos of positions) {
|
|
168
|
+
if (pos >= end) {
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
if (pos >= start) {
|
|
172
|
+
count += 1;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (count > bestCount) {
|
|
176
|
+
bestCount = count;
|
|
177
|
+
best = start;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return withEllipsis(Math.max(0, best - EXCERPT_LEAD));
|
|
62
181
|
};
|
|
63
182
|
|
|
64
183
|
/**
|
|
@@ -111,7 +230,11 @@ export const createAskContext = (
|
|
|
111
230
|
return;
|
|
112
231
|
}
|
|
113
232
|
seen.add(doc.route);
|
|
114
|
-
const body =
|
|
233
|
+
const body = relevantExcerpt(
|
|
234
|
+
doc.content,
|
|
235
|
+
query,
|
|
236
|
+
Math.min(EXCERPT_CHARS, budget)
|
|
237
|
+
);
|
|
115
238
|
budget -= body.length;
|
|
116
239
|
sections.push(`## ${doc.title} (${doc.route})${label}\n${body}`);
|
|
117
240
|
};
|
package/src/ai/ask-data.ts
CHANGED
|
@@ -6,10 +6,13 @@ import type { AskData } from "./ask-context.ts";
|
|
|
6
6
|
* Build the grounding snapshot the Ask AI endpoint serves. Like the MCP server,
|
|
7
7
|
* Ask AI is independent of on-page search, so documents are indexed even when the
|
|
8
8
|
* search provider is `none` (`includeWhenDisabled`). `locale` is kept (unlike the
|
|
9
|
-
* MCP snapshot) so retrieval can be filtered to the current page's language
|
|
9
|
+
* MCP snapshot) so retrieval can be filtered to the current page's language, and
|
|
10
|
+
* content is kept as Markdown so grounding sees fenced code examples — the model
|
|
11
|
+
* answers "what does the config look like?" from the docs instead of declining.
|
|
10
12
|
*/
|
|
11
13
|
export const buildAskData = async (project: BlumeProject): Promise<AskData> => {
|
|
12
14
|
const documents = await buildSearchDocuments(project, {
|
|
15
|
+
content: "markdown",
|
|
13
16
|
includeWhenDisabled: true,
|
|
14
17
|
});
|
|
15
18
|
return {
|
package/src/astro/generate.ts
CHANGED
|
@@ -432,11 +432,16 @@ const resolveLogo = (project: BlumeProject): BlumeLogo | null => {
|
|
|
432
432
|
if (!logo) {
|
|
433
433
|
return null;
|
|
434
434
|
}
|
|
435
|
-
const config = typeof logo === "string" ? {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
const href = config
|
|
435
|
+
const config = typeof logo === "string" ? { image: logo } : logo;
|
|
436
|
+
// `text` is passed through verbatim: `undefined` lets the brand fall back to
|
|
437
|
+
// the site title, `""` renders the mark alone (a logo with the wordmark baked
|
|
438
|
+
// in).
|
|
439
|
+
const { href, image: source, text } = config;
|
|
440
|
+
const image = typeof source === "string" ? { light: source } : source;
|
|
441
|
+
const light = image?.light ?? image?.dark;
|
|
442
|
+
const dark = image?.dark ?? image?.light;
|
|
443
|
+
const alt = image?.alt ?? "";
|
|
444
|
+
const brandHref = href ?? "/";
|
|
440
445
|
|
|
441
446
|
if (light && light === dark && light.toLowerCase().endsWith(".svg")) {
|
|
442
447
|
const rel = light.replace(/^\//u, "");
|
|
@@ -445,10 +450,10 @@ const resolveLogo = (project: BlumeProject): BlumeLogo | null => {
|
|
|
445
450
|
join(project.context.root, rel),
|
|
446
451
|
].find((path) => existsSync(path));
|
|
447
452
|
if (file) {
|
|
448
|
-
return { alt, href, svg: readFileSync(file, "utf-8") };
|
|
453
|
+
return { alt, href: brandHref, svg: readFileSync(file, "utf-8"), text };
|
|
449
454
|
}
|
|
450
455
|
}
|
|
451
|
-
return { alt, dark, href, light };
|
|
456
|
+
return { alt, dark, href: brandHref, light, text };
|
|
452
457
|
};
|
|
453
458
|
|
|
454
459
|
/**
|
|
@@ -615,7 +620,7 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
615
620
|
code,
|
|
616
621
|
withReferenceTabs(
|
|
617
622
|
graph.navigationByLocale[code] ?? {
|
|
618
|
-
|
|
623
|
+
featured: [],
|
|
619
624
|
selectors: [],
|
|
620
625
|
sidebar: [],
|
|
621
626
|
tabs: [],
|
|
@@ -629,6 +634,9 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
629
634
|
config: {
|
|
630
635
|
analytics: config.analytics ?? null,
|
|
631
636
|
appleIcon: resolveAppleIcon(project),
|
|
637
|
+
ask: config.ai.ask?.enabled
|
|
638
|
+
? { suggestions: config.ai.ask.suggestions }
|
|
639
|
+
: null,
|
|
632
640
|
banner: resolveBanner(config),
|
|
633
641
|
codeWrap: config.markdown.code.wrap,
|
|
634
642
|
description: config.description,
|
|
@@ -648,7 +656,6 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
648
656
|
})),
|
|
649
657
|
}
|
|
650
658
|
: null,
|
|
651
|
-
icons: config.icons,
|
|
652
659
|
imageZoom: config.markdown.imageZoom,
|
|
653
660
|
logo: resolveLogo(project),
|
|
654
661
|
mcp: config.mcp.enabled
|
|
@@ -974,9 +981,9 @@ export const generateRuntime = async (
|
|
|
974
981
|
const staged = collectStaged(project);
|
|
975
982
|
const hasStaged = staged.size > 0;
|
|
976
983
|
// Only emit a project-scanning `docs` collection when a filesystem source
|
|
977
|
-
// actually feeds it.
|
|
978
|
-
// `docs` glob would otherwise scan (and watch) the whole
|
|
979
|
-
// nothing — see contentConfigTemplate.
|
|
984
|
+
// actually feeds it. An all-staged project (openapi/notion/…) has only staged
|
|
985
|
+
// sources, so the `docs` glob would otherwise scan (and watch) the whole
|
|
986
|
+
// project root for nothing — see contentConfigTemplate.
|
|
980
987
|
const hasFilesystemSource = project.sources.some((source) => !source.staged);
|
|
981
988
|
|
|
982
989
|
const structural = await Promise.all([
|
package/src/astro/integration.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
2
|
|
|
4
3
|
import type { AstroIntegration } from "astro";
|
|
5
4
|
|
|
6
|
-
import type { AssetMount } from "../core/assets.ts";
|
|
7
5
|
import { enrichDiagnostic } from "../core/diagnostics.ts";
|
|
8
6
|
import type { Diagnostic } from "../core/types.ts";
|
|
9
7
|
import { markdownVariantUrl, prefersMarkdown } from "./markdown-negotiation.ts";
|
|
10
|
-
import { copyAssetMounts, serveAssetMounts } from "./static-assets.ts";
|
|
11
8
|
|
|
12
9
|
/** The dev server's HMR channel — either `.ws` (Vite ≤5) or `.hot` (Vite 6+). */
|
|
13
10
|
interface OverlayChannel {
|
|
@@ -77,11 +74,6 @@ export interface BlumeIntegrationOptions {
|
|
|
77
74
|
contentRoutes: string[];
|
|
78
75
|
/** Configured `deployment.base`, stripped from dev URLs before matching. */
|
|
79
76
|
base?: string;
|
|
80
|
-
/**
|
|
81
|
-
* `content.assets` mounts: top-level dirs served at the site root in dev and
|
|
82
|
-
* copied into the build output, so root-served assets need no relocation.
|
|
83
|
-
*/
|
|
84
|
-
assets?: AssetMount[];
|
|
85
77
|
}
|
|
86
78
|
|
|
87
79
|
/**
|
|
@@ -118,12 +110,6 @@ export const blumeIntegration = (
|
|
|
118
110
|
options: BlumeIntegrationOptions
|
|
119
111
|
): AstroIntegration => ({
|
|
120
112
|
hooks: {
|
|
121
|
-
"astro:build:done": async ({ dir }) => {
|
|
122
|
-
// Copy in-place asset mounts into the build output, mirroring publicDir.
|
|
123
|
-
if (options.assets?.length) {
|
|
124
|
-
await copyAssetMounts(options.assets, fileURLToPath(dir));
|
|
125
|
-
}
|
|
126
|
-
},
|
|
127
113
|
"astro:config:setup": ({ injectRoute }) => {
|
|
128
114
|
for (const page of options.pages) {
|
|
129
115
|
injectRoute({
|
|
@@ -143,13 +129,6 @@ export const blumeIntegration = (
|
|
|
143
129
|
handle: negotiateMarkdown(new Set(options.contentRoutes), options.base),
|
|
144
130
|
route: "",
|
|
145
131
|
});
|
|
146
|
-
// Serve `content.assets` mounts (Astro only serves publicDir in dev).
|
|
147
|
-
if (options.assets?.length) {
|
|
148
|
-
server.middlewares.stack.unshift({
|
|
149
|
-
handle: serveAssetMounts(options.assets),
|
|
150
|
-
route: "",
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
132
|
},
|
|
154
133
|
},
|
|
155
134
|
name: "blume",
|