blume 0.6.7 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +618 -0
- package/LICENSE +21 -0
- package/README.md +107 -0
- package/dist/cli/index.js +2609 -1041
- package/dist/cli/index.js.map +110 -103
- package/dist/types/ai/component-markdown.d.ts +34 -0
- package/dist/types/components/content/youtube.d.ts +18 -0
- package/dist/types/core/base-path.d.ts +47 -0
- package/dist/types/core/config-input.d.ts +110 -12
- package/dist/types/core/config.d.ts +6 -4
- package/dist/types/core/data.d.ts +4 -0
- package/dist/types/core/i18n-ui.d.ts +477 -135
- package/dist/types/core/schema.d.ts +309 -195
- package/dist/types/core/sources/types.d.ts +2 -0
- package/dist/types/core/types.d.ts +6 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/openapi/references.d.ts +60 -0
- package/docs/01-quickstart.mdx +5 -2
- package/docs/02-deployment.mdx +24 -9
- package/docs/03-faq.mdx +46 -16
- package/docs/advanced/custom-pages.mdx +1 -1
- package/docs/advanced/skills.mdx +1 -1
- package/docs/configuration/ai.mdx +49 -10
- package/docs/configuration/customization.mdx +11 -0
- package/docs/configuration/index.mdx +33 -3
- package/docs/configuration/seo.mdx +2 -2
- package/docs/content/components.mdx +30 -3
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/islands.mdx +8 -0
- package/docs/content/navigation.mdx +3 -3
- package/docs/content/sources.mdx +1 -1
- package/docs/content/syntax.mdx +17 -2
- package/docs/index.mdx +2 -2
- package/docs/reference/cli.mdx +8 -6
- package/package.json +15 -4
- package/skills/blume/SKILL.md +5 -3
- package/skills/blume-update-docs/SKILL.md +3 -2
- package/src/ai/agent-readability.ts +11 -5
- package/src/ai/ask-context.ts +7 -2
- package/src/ai/ask-data.ts +3 -0
- package/src/ai/ask.ts +12 -7
- package/src/ai/component-markdown.ts +461 -0
- package/src/ai/llms.ts +143 -23
- package/src/ai/markdown.ts +35 -6
- package/src/ai/mcp/data.ts +33 -8
- package/src/ai/mcp/discovery.ts +10 -3
- package/src/ai/mcp/server.ts +24 -7
- package/src/ai/visibility.ts +74 -0
- package/src/astro/component-slots.ts +16 -4
- package/src/astro/examples.ts +12 -7
- package/src/astro/generate.ts +393 -189
- package/src/astro/index.ts +5 -1
- package/src/astro/integration.ts +9 -5
- package/src/astro/islands.ts +11 -5
- package/src/astro/markdown-negotiation.ts +2 -2
- package/src/astro/pages.ts +89 -22
- package/src/astro/templates.ts +259 -25
- package/src/blume-modules.d.ts +8 -0
- package/src/cli/commands/build.ts +131 -38
- package/src/cli/commands/check.ts +1 -1
- package/src/cli/commands/dev.ts +71 -17
- package/src/cli/commands/doctor.ts +2 -2
- package/src/cli/commands/eject.ts +47 -19
- package/src/cli/commands/init.ts +120 -180
- package/src/cli/commands/preview.ts +4 -1
- package/src/cli/commands/validate.ts +44 -2
- package/src/cli/dev-lock.ts +34 -19
- package/src/cli/eject-scripts.ts +72 -0
- package/src/cli/env.ts +15 -5
- package/src/cli/init/questions.ts +158 -0
- package/src/cli/init/scaffold.ts +380 -0
- package/src/cli/required-secrets.ts +2 -1
- package/src/components/content/AccordionItem.astro +23 -4
- package/src/components/content/Badge.astro +3 -1
- package/src/components/content/Card.astro +4 -2
- package/src/components/content/CodeBlock.astro +3 -0
- package/src/components/content/Component.astro +30 -16
- package/src/components/content/Diff.astro +3 -1
- package/src/components/content/Step.astro +10 -1
- package/src/components/content/Tabs.astro +15 -3
- package/src/components/content/Tile.astro +2 -1
- package/src/components/content/Tooltip.astro +3 -1
- package/src/components/content/Update.astro +9 -2
- package/src/components/content/auto-type-table.ts +25 -9
- package/src/components/content/base-href.ts +33 -0
- package/src/components/content/changelog-element.ts +9 -2
- package/src/components/content/diff.ts +12 -6
- package/src/components/content/mermaid-element.ts +10 -2
- package/src/components/index.ts +23 -1
- package/src/components/islands/AskAI.astro +5 -2
- package/src/components/islands/ask-ai.tsx +68 -12
- package/src/components/islands/base-path.ts +28 -0
- package/src/components/islands/hooks.ts +44 -9
- package/src/components/layout/Banner.astro +12 -3
- package/src/components/layout/Breadcrumbs.astro +2 -1
- package/src/components/layout/Favicon.astro +3 -2
- package/src/components/layout/Header.astro +15 -5
- package/src/components/layout/LanguageSwitcher.astro +2 -1
- package/src/components/layout/Logo.astro +13 -4
- package/src/components/layout/NavSelector.astro +2 -1
- package/src/components/layout/NavTree.astro +22 -7
- package/src/components/layout/PageActions.astro +25 -10
- package/src/components/layout/PageFeedback.astro +4 -1
- package/src/components/layout/PageLayout.astro +51 -9
- package/src/components/layout/Pagination.astro +3 -2
- package/src/components/layout/ReferenceLayout.astro +8 -1
- package/src/components/layout/RootLayout.astro +74 -13
- package/src/components/layout/Search.astro +107 -27
- package/src/components/layout/nav-utils.ts +18 -10
- package/src/components/layout/search/algolia.ts +11 -2
- package/src/components/layout/search/endpoint.ts +11 -5
- package/src/components/layout/search/orama-cloud.ts +8 -2
- package/src/components/layout/search/pagefind.ts +3 -0
- package/src/components/layout/search/types.ts +5 -1
- package/src/components/layout/search/typesense.ts +4 -1
- package/src/components/layout/toc-element.ts +8 -2
- package/src/components/openapi/ApiTagOperations.astro +2 -1
- package/src/components/openapi/Operation.astro +47 -40
- package/src/components/openapi/RequestPanel.astro +8 -2
- package/src/components/openapi/helpers.ts +71 -3
- package/src/components/openapi/panel.ts +1 -1
- package/src/components/openapi/snippets.ts +25 -11
- package/src/core/base-path.ts +94 -0
- package/src/core/builtin-tags.ts +2 -0
- package/src/core/component-overrides.ts +103 -74
- package/src/core/config-input.ts +118 -17
- package/src/core/config.ts +8 -5
- package/src/core/content.ts +2 -0
- package/src/core/data.ts +4 -0
- package/src/core/diagnostics.ts +54 -34
- package/src/core/gitignore.ts +4 -1
- package/src/core/graph.ts +166 -88
- package/src/core/i18n-ui.ts +63 -3
- package/src/core/last-modified.ts +15 -6
- package/src/core/links.ts +69 -25
- package/src/core/manifest.ts +62 -45
- package/src/core/nav-diagnostics.ts +1 -1
- package/src/core/navigation.ts +144 -58
- package/src/core/package-json.ts +17 -2
- package/src/core/project-graph.ts +25 -15
- package/src/core/schema.ts +605 -620
- package/src/core/sources/assets.ts +6 -1
- package/src/core/sources/filesystem.ts +4 -0
- package/src/core/sources/github-releases.ts +2 -1
- package/src/core/sources/mdx-remote.ts +76 -63
- package/src/core/sources/normalize.ts +236 -91
- package/src/core/sources/notion.ts +27 -18
- package/src/core/sources/types.ts +2 -0
- package/src/core/tsconfig-aliases.ts +59 -30
- package/src/core/types.ts +6 -1
- package/src/core/ui-packs/ar.ts +1 -0
- package/src/core/ui-packs/bg.ts +1 -0
- package/src/core/ui-packs/bn.ts +1 -0
- package/src/core/ui-packs/ca.ts +1 -0
- package/src/core/ui-packs/cs.ts +1 -0
- package/src/core/ui-packs/da.ts +1 -0
- package/src/core/ui-packs/de.ts +1 -0
- package/src/core/ui-packs/el.ts +1 -0
- package/src/core/ui-packs/es.ts +1 -0
- package/src/core/ui-packs/fa.ts +1 -0
- package/src/core/ui-packs/fi.ts +1 -0
- package/src/core/ui-packs/fr.ts +2 -1
- package/src/core/ui-packs/he.ts +1 -0
- package/src/core/ui-packs/hi.ts +1 -0
- package/src/core/ui-packs/hr.ts +1 -0
- package/src/core/ui-packs/hu.ts +1 -0
- package/src/core/ui-packs/id.ts +1 -0
- package/src/core/ui-packs/it.ts +1 -0
- package/src/core/ui-packs/ja.ts +1 -0
- package/src/core/ui-packs/ko.ts +1 -0
- package/src/core/ui-packs/nl.ts +1 -0
- package/src/core/ui-packs/no.ts +1 -0
- package/src/core/ui-packs/pl.ts +1 -0
- package/src/core/ui-packs/pt-br.ts +1 -0
- package/src/core/ui-packs/pt.ts +1 -0
- package/src/core/ui-packs/ro.ts +1 -0
- package/src/core/ui-packs/ru.ts +1 -0
- package/src/core/ui-packs/sk.ts +1 -0
- package/src/core/ui-packs/sr.ts +1 -0
- package/src/core/ui-packs/sv.ts +1 -0
- package/src/core/ui-packs/th.ts +1 -0
- package/src/core/ui-packs/tr.ts +1 -0
- package/src/core/ui-packs/uk.ts +1 -0
- package/src/core/ui-packs/vi.ts +1 -0
- package/src/core/ui-packs/zh-tw.ts +1 -0
- package/src/core/ui-packs/zh.ts +1 -0
- package/src/deploy/adapter-output.ts +18 -8
- package/src/deploy/redirects.ts +25 -2
- package/src/deploy/robots.ts +6 -1
- package/src/deploy/rss.ts +10 -3
- package/src/deploy/sitemap.ts +59 -13
- package/src/index.ts +5 -0
- package/src/markdown/base-links.ts +60 -0
- package/src/markdown/code-title.ts +11 -14
- package/src/markdown/index.ts +46 -9
- package/src/markdown/inline-code.ts +14 -4
- package/src/markdown/package-commands.ts +10 -4
- package/src/markdown/themes.ts +24 -0
- package/src/openapi/model.ts +15 -5
- package/src/openapi/parse.ts +21 -0
- package/src/openapi/references.ts +75 -21
- package/src/openapi/render-mdx.ts +11 -6
- package/src/openapi/scalar.ts +32 -16
- package/src/openapi/source.ts +59 -10
- package/src/registry/eject.ts +247 -19
- package/src/registry/registry.ts +0 -3
- package/src/search/build.ts +3 -0
- package/src/search/documents.ts +36 -4
- package/src/search/sync/typesense.ts +6 -4
- package/src/seo/jsonld.ts +28 -17
- package/src/theme/entry.ts +85 -20
package/src/registry/eject.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { join, relative } from "pathe";
|
|
|
6
6
|
import { buildAskData } from "../ai/ask-data.ts";
|
|
7
7
|
import { resolveAskBackend } from "../ai/ask.ts";
|
|
8
8
|
import { buildRawMarkdown } from "../ai/markdown.ts";
|
|
9
|
+
import { buildMcpData } from "../ai/mcp/data.ts";
|
|
10
|
+
import { buildMcpDiscovery, buildMcpServerCard } from "../ai/mcp/discovery.ts";
|
|
9
11
|
import { planComponentSlots } from "../astro/component-slots.ts";
|
|
10
12
|
import { discoverExamples } from "../astro/examples.ts";
|
|
11
13
|
import {
|
|
@@ -20,13 +22,17 @@ import {
|
|
|
20
22
|
askEndpointTemplate,
|
|
21
23
|
astroConfigTemplate,
|
|
22
24
|
catchAllPageTemplate,
|
|
25
|
+
changelogIndexTemplate,
|
|
23
26
|
contentConfigTemplate,
|
|
24
27
|
envTemplate,
|
|
25
28
|
exampleMapTemplate,
|
|
26
29
|
exampleWrapperTemplate,
|
|
30
|
+
examplesPageTemplate,
|
|
27
31
|
exampleSlug,
|
|
28
32
|
islandMapTemplate,
|
|
29
33
|
islandWrapperTemplate,
|
|
34
|
+
mcpEndpointTemplate,
|
|
35
|
+
mcpPageFile,
|
|
30
36
|
mixedbreadSearchEndpointTemplate,
|
|
31
37
|
notFoundPageTemplate,
|
|
32
38
|
ogEndpointTemplate,
|
|
@@ -35,7 +41,9 @@ import {
|
|
|
35
41
|
runtimeTsconfigTemplate,
|
|
36
42
|
searchClientTemplate,
|
|
37
43
|
searchEndpointTemplate,
|
|
44
|
+
staticJsonEndpointTemplate,
|
|
38
45
|
} from "../astro/templates.ts";
|
|
46
|
+
import { packageRoot } from "../core/package-root.ts";
|
|
39
47
|
import { scanProject } from "../core/project-graph.ts";
|
|
40
48
|
import type { BlumeProject } from "../core/project-graph.ts";
|
|
41
49
|
import type { ProjectContext } from "../core/types.ts";
|
|
@@ -45,11 +53,47 @@ import { buildReferenceFiles } from "../openapi/scalar.ts";
|
|
|
45
53
|
import { isOpenApiSource } from "../openapi/source.ts";
|
|
46
54
|
import { buildSearchDocuments } from "../search/documents.ts";
|
|
47
55
|
import { servesStaticIndex } from "../search/providers.ts";
|
|
48
|
-
import {
|
|
56
|
+
import {
|
|
57
|
+
examplesEntryTemplate,
|
|
58
|
+
tailwindEntryTemplate,
|
|
59
|
+
} from "../theme/entry.ts";
|
|
49
60
|
import { buildThemeCss } from "../theme/palette.ts";
|
|
50
61
|
import { twoslashCss } from "../theme/twoslash.ts";
|
|
51
62
|
|
|
52
|
-
const
|
|
63
|
+
const toPosix = (path: string): string => path.split("\\").join("/");
|
|
64
|
+
|
|
65
|
+
/** The portable `@source` guess: blume in the project's own node_modules. */
|
|
66
|
+
const LOCAL_BLUME_SOURCE = "../../node_modules/blume/src/**/*.{astro,ts,tsx}";
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The `@source` glob pointing Tailwind at Blume's own source, relative to the
|
|
70
|
+
* ejected `src/generated/app.css`. The project-local `node_modules/blume` is
|
|
71
|
+
* preferred (portable, and under pnpm the symlink survives version bumps), but
|
|
72
|
+
* hoisted installs (npm/yarn workspaces lift blume into the workspace root's
|
|
73
|
+
* node_modules) would make that guess match nothing and silently drop utility
|
|
74
|
+
* classes — so fall back to the package's real installed location, and when
|
|
75
|
+
* even that fails, warn instead of failing silently.
|
|
76
|
+
*
|
|
77
|
+
* Exported for testing.
|
|
78
|
+
*/
|
|
79
|
+
export const blumeSourceGlob = (
|
|
80
|
+
root: string,
|
|
81
|
+
genDir: string,
|
|
82
|
+
resolveBlumeRoot: () => string = packageRoot
|
|
83
|
+
): string => {
|
|
84
|
+
if (existsSync(join(root, "node_modules", "blume"))) {
|
|
85
|
+
return LOCAL_BLUME_SOURCE;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const src = join(resolveBlumeRoot(), "src");
|
|
89
|
+
return `${toPosix(relative(genDir, src))}/**/*.{astro,ts,tsx}`;
|
|
90
|
+
} catch {
|
|
91
|
+
console.warn(
|
|
92
|
+
'blume: could not locate the installed blume package; src/generated/app.css keeps its default `@source "../../node_modules/blume/..."` glob. If blume is hoisted elsewhere, point that glob at its install location or Blume\'s utility classes will be missing.'
|
|
93
|
+
);
|
|
94
|
+
return LOCAL_BLUME_SOURCE;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
53
97
|
|
|
54
98
|
/** The `blume:openapi` payload for the ejected app (`{}` when none). */
|
|
55
99
|
const ejectOpenApiData = (project: BlumeProject): unknown => {
|
|
@@ -86,14 +130,158 @@ const askFiles = async (
|
|
|
86
130
|
return files;
|
|
87
131
|
};
|
|
88
132
|
|
|
133
|
+
/** Whether the ejected app hosts the MCP server (enabled and route free). */
|
|
134
|
+
const hostsMcp = (
|
|
135
|
+
project: BlumeProject,
|
|
136
|
+
userPages: { pattern: string }[]
|
|
137
|
+
): boolean =>
|
|
138
|
+
project.config.mcp.enabled &&
|
|
139
|
+
!routeIsTaken(userPages, project.graph.pages, project.config.mcp.route);
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The `.well-known` MCP discovery routes, injected as prerendered pages
|
|
143
|
+
* alongside the user's own so the ejected Astro config wires them in. Empty
|
|
144
|
+
* when the server is disabled or its route is already owned by a page.
|
|
145
|
+
*/
|
|
146
|
+
const mcpDiscoveryPages = (
|
|
147
|
+
project: BlumeProject,
|
|
148
|
+
userPages: { pattern: string }[]
|
|
149
|
+
): { entrypoint: string; pattern: string }[] =>
|
|
150
|
+
hostsMcp(project, userPages)
|
|
151
|
+
? [
|
|
152
|
+
{
|
|
153
|
+
entrypoint: "src/blume-mcp/discovery.ts",
|
|
154
|
+
pattern: "/.well-known/mcp.json",
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
entrypoint: "src/blume-mcp/server-card.ts",
|
|
158
|
+
pattern: "/.well-known/mcp/server-card.json",
|
|
159
|
+
},
|
|
160
|
+
]
|
|
161
|
+
: [];
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The MCP data snapshot, server endpoint, and `.well-known` discovery
|
|
165
|
+
* documents, mirroring `writeMcpFiles` in generate.ts. Empty when the server
|
|
166
|
+
* is disabled or its route is already owned by a page.
|
|
167
|
+
*/
|
|
168
|
+
const mcpFiles = async (
|
|
169
|
+
project: BlumeProject,
|
|
170
|
+
userPages: { pattern: string }[],
|
|
171
|
+
srcDir: string,
|
|
172
|
+
genDir: string
|
|
173
|
+
): Promise<{ content: string; path: string }[]> => {
|
|
174
|
+
if (!hostsMcp(project, userPages)) {
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
const { route } = project.config.mcp;
|
|
178
|
+
const data = await buildMcpData(project);
|
|
179
|
+
const discoveryInput = {
|
|
180
|
+
base: data.base,
|
|
181
|
+
name: data.name,
|
|
182
|
+
route,
|
|
183
|
+
site: data.site,
|
|
184
|
+
version: data.version,
|
|
185
|
+
};
|
|
186
|
+
return [
|
|
187
|
+
{
|
|
188
|
+
content: `${JSON.stringify(data)}\n`,
|
|
189
|
+
path: join(genDir, "mcp-data.json"),
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
content: mcpEndpointTemplate(route),
|
|
193
|
+
path: join(srcDir, "pages", mcpPageFile(route)),
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
content: staticJsonEndpointTemplate(buildMcpDiscovery(discoveryInput)),
|
|
197
|
+
path: join(srcDir, "blume-mcp", "discovery.ts"),
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
content: staticJsonEndpointTemplate(buildMcpServerCard(discoveryInput)),
|
|
201
|
+
path: join(srcDir, "blume-mcp", "server-card.ts"),
|
|
202
|
+
},
|
|
203
|
+
];
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The `/changelog` index page, mirroring `shouldGenerateChangelog` in
|
|
208
|
+
* generate.ts: emitted when `type: changelog` entries or a release-backed
|
|
209
|
+
* changelog source exist, unless a user page already owns the route.
|
|
210
|
+
*/
|
|
211
|
+
const changelogFiles = (
|
|
212
|
+
project: BlumeProject,
|
|
213
|
+
userPages: { pattern: string }[],
|
|
214
|
+
srcDir: string,
|
|
215
|
+
options: Parameters<typeof changelogIndexTemplate>[0]
|
|
216
|
+
): { content: string; path: string }[] => {
|
|
217
|
+
const hasChangelog = project.graph.pages.some(
|
|
218
|
+
(page) =>
|
|
219
|
+
page.contentType === "changelog" &&
|
|
220
|
+
!(page.meta.draft || page.meta.sidebar.hidden)
|
|
221
|
+
);
|
|
222
|
+
const hasChangelogSource = (project.config.content.sources ?? []).some(
|
|
223
|
+
(source) => source.type === "github-releases"
|
|
224
|
+
);
|
|
225
|
+
if (
|
|
226
|
+
!(hasChangelog || hasChangelogSource) ||
|
|
227
|
+
routeIsTaken(userPages, project.graph.pages, "/changelog")
|
|
228
|
+
) {
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
return [
|
|
232
|
+
{
|
|
233
|
+
content: changelogIndexTemplate(options),
|
|
234
|
+
path: join(srcDir, "pages", "changelog.astro"),
|
|
235
|
+
},
|
|
236
|
+
];
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
/** Contents of the configured `examples.css`, or `""` when unset/absent. */
|
|
240
|
+
const readExamplesCss = (
|
|
241
|
+
root: string,
|
|
242
|
+
css: string | undefined
|
|
243
|
+
): Promise<string> =>
|
|
244
|
+
css && existsSync(join(root, css))
|
|
245
|
+
? readFile(join(root, css), "utf-8")
|
|
246
|
+
: Promise.resolve("");
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The per-example preview route `<Component />` iframes embed, nested under
|
|
250
|
+
* `basePath` so it stays reachable behind a proxy that only forwards the
|
|
251
|
+
* base. Empty when the project has no examples.
|
|
252
|
+
*/
|
|
253
|
+
const examplesPreviewFiles = (
|
|
254
|
+
srcDir: string,
|
|
255
|
+
basePath: string,
|
|
256
|
+
hasExamples: boolean
|
|
257
|
+
): { content: string; path: string }[] =>
|
|
258
|
+
hasExamples
|
|
259
|
+
? [
|
|
260
|
+
{
|
|
261
|
+
content: examplesPageTemplate(),
|
|
262
|
+
path: join(
|
|
263
|
+
srcDir,
|
|
264
|
+
"pages",
|
|
265
|
+
...basePath.split("/").filter(Boolean),
|
|
266
|
+
"blume-examples",
|
|
267
|
+
"[...path].astro"
|
|
268
|
+
),
|
|
269
|
+
},
|
|
270
|
+
]
|
|
271
|
+
: [];
|
|
272
|
+
|
|
89
273
|
/**
|
|
90
274
|
* Promote the generated runtime into the project as an owned Astro app. After
|
|
91
275
|
* eject the project has a normal `astro.config.mjs` and `src/`, the `blume` CLI
|
|
92
276
|
* is no longer required, and the `blume` package remains importable.
|
|
93
277
|
*
|
|
94
|
-
* Returns the
|
|
278
|
+
* Returns the written files plus non-fatal warnings, mirroring the generated
|
|
279
|
+
* runtime (e.g. a Scalar reference spec that wasn't found, or a reference
|
|
280
|
+
* route colliding with a content page).
|
|
95
281
|
*/
|
|
96
|
-
export const eject = async (
|
|
282
|
+
export const eject = async (
|
|
283
|
+
root: string
|
|
284
|
+
): Promise<{ files: string[]; warnings: string[] }> => {
|
|
97
285
|
const project = await scanProject(root, { mode: "build" });
|
|
98
286
|
const { context, config } = project;
|
|
99
287
|
|
|
@@ -108,6 +296,7 @@ export const eject = async (root: string): Promise<string[]> => {
|
|
|
108
296
|
needsReactRaw,
|
|
109
297
|
usesMath,
|
|
110
298
|
userTheme,
|
|
299
|
+
userExamplesCss,
|
|
111
300
|
rawMarkdown,
|
|
112
301
|
islands,
|
|
113
302
|
examples,
|
|
@@ -118,9 +307,10 @@ export const eject = async (root: string): Promise<string[]> => {
|
|
|
118
307
|
context.themeFile
|
|
119
308
|
? readFile(context.themeFile, "utf-8")
|
|
120
309
|
: Promise.resolve(""),
|
|
310
|
+
readExamplesCss(root, config.examples.css),
|
|
121
311
|
buildRawMarkdown(project),
|
|
122
312
|
discoverIslands(root),
|
|
123
|
-
discoverExamples(root, config.examples),
|
|
313
|
+
discoverExamples(root, config.examples.source),
|
|
124
314
|
]);
|
|
125
315
|
// Island/example frameworks drive which Astro renderers the ejected config
|
|
126
316
|
// wires in; React also switches on for project `.tsx`/`.jsx` and Ask AI.
|
|
@@ -135,18 +325,21 @@ export const eject = async (root: string): Promise<string[]> => {
|
|
|
135
325
|
// A project-relative context so generated files use portable paths.
|
|
136
326
|
const relContext: ProjectContext = {
|
|
137
327
|
...context,
|
|
138
|
-
contentRoot:
|
|
328
|
+
contentRoot: toPosix(relative(root, context.contentRoot)),
|
|
139
329
|
outDir: ".",
|
|
140
330
|
root: ".",
|
|
141
331
|
};
|
|
142
332
|
|
|
143
333
|
const componentsImport = context.componentsFile
|
|
144
|
-
? `../../${
|
|
334
|
+
? `../../${toPosix(relative(root, context.componentsFile))}`
|
|
145
335
|
: null;
|
|
146
|
-
const relPages =
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
336
|
+
const relPages = [
|
|
337
|
+
...pages.map((page) => ({
|
|
338
|
+
entrypoint: toPosix(relative(root, page.entrypoint)),
|
|
339
|
+
pattern: page.pattern,
|
|
340
|
+
})),
|
|
341
|
+
...mcpDiscoveryPages(project, pages),
|
|
342
|
+
];
|
|
150
343
|
|
|
151
344
|
// Non-filesystem sources eject their materialized MDX into `<root>/blume-staged`
|
|
152
345
|
// (a dedicated dir so it never clashes with a content root literally named
|
|
@@ -168,6 +361,7 @@ export const eject = async (root: string): Promise<string[]> => {
|
|
|
168
361
|
context: relContext,
|
|
169
362
|
dataPath: "./src/generated/data.json",
|
|
170
363
|
examplesPath: "./src/generated/examples.ts",
|
|
364
|
+
examplesThemePath: "./src/generated/examples.css",
|
|
171
365
|
needsReact,
|
|
172
366
|
needsSvelte,
|
|
173
367
|
needsVue,
|
|
@@ -218,15 +412,27 @@ export const eject = async (root: string): Promise<string[]> => {
|
|
|
218
412
|
path: join(genDir, "islands.ts"),
|
|
219
413
|
},
|
|
220
414
|
{
|
|
221
|
-
content: exampleMapTemplate(examples.examples),
|
|
415
|
+
content: exampleMapTemplate(examples.examples, config.basePath),
|
|
222
416
|
path: join(genDir, "examples.ts"),
|
|
223
417
|
},
|
|
418
|
+
{
|
|
419
|
+
// The isolated Tailwind entry for `<Component />` preview frames.
|
|
420
|
+
// Relative sources keep the ejected app portable.
|
|
421
|
+
content: examplesEntryTemplate({
|
|
422
|
+
configTokens: buildThemeCss(config.theme),
|
|
423
|
+
sources: ["../../**/*.{astro,jsx,svelte,ts,tsx,vue}"],
|
|
424
|
+
userCss: userExamplesCss,
|
|
425
|
+
}),
|
|
426
|
+
path: join(genDir, "examples.css"),
|
|
427
|
+
},
|
|
224
428
|
{
|
|
225
429
|
content: tailwindEntryTemplate({
|
|
226
430
|
configTokens: buildThemeCss(config.theme),
|
|
227
|
-
// Relative paths from src/generated/app.css keep the ejected app
|
|
431
|
+
// Relative paths from src/generated/app.css keep the ejected app
|
|
432
|
+
// portable; the blume glob resolves the real install location when
|
|
433
|
+
// the package is hoisted out of the project's own node_modules.
|
|
228
434
|
sources: [
|
|
229
|
-
|
|
435
|
+
blumeSourceGlob(root, genDir),
|
|
230
436
|
"../../**/*.{astro,mdx,ts,tsx}",
|
|
231
437
|
],
|
|
232
438
|
twoslashCss: twoslashCss(),
|
|
@@ -244,11 +450,11 @@ export const eject = async (root: string): Promise<string[]> => {
|
|
|
244
450
|
path: join(genDir, "raw-markdown.json"),
|
|
245
451
|
},
|
|
246
452
|
{
|
|
247
|
-
content: rawMarkdownEndpointTemplate(),
|
|
453
|
+
content: rawMarkdownEndpointTemplate("md"),
|
|
248
454
|
path: join(srcDir, "pages", "[...slug].md.ts"),
|
|
249
455
|
},
|
|
250
456
|
{
|
|
251
|
-
content: rawMarkdownEndpointTemplate(),
|
|
457
|
+
content: rawMarkdownEndpointTemplate("mdx"),
|
|
252
458
|
path: join(srcDir, "pages", "[...slug].mdx.ts"),
|
|
253
459
|
},
|
|
254
460
|
];
|
|
@@ -264,6 +470,19 @@ export const eject = async (root: string): Promise<string[]> => {
|
|
|
264
470
|
});
|
|
265
471
|
}
|
|
266
472
|
|
|
473
|
+
// The hosted MCP server and the `/changelog` index, mirrored from the
|
|
474
|
+
// generated runtime (each helper returns `[]` when its feature is off).
|
|
475
|
+
files.push(
|
|
476
|
+
...(await mcpFiles(project, pages, srcDir, genDir)),
|
|
477
|
+
...changelogFiles(project, pages, srcDir, {
|
|
478
|
+
askEnabled,
|
|
479
|
+
exportEpub,
|
|
480
|
+
exportPdf,
|
|
481
|
+
needsReact,
|
|
482
|
+
staged: hasStaged,
|
|
483
|
+
})
|
|
484
|
+
);
|
|
485
|
+
|
|
267
486
|
// Default 404 page, unless the project already owns `/404` (a custom
|
|
268
487
|
// `pages/404.astro` or a `404.md` content page). The ejected project owns the
|
|
269
488
|
// file afterwards and can edit or remove it.
|
|
@@ -321,13 +540,17 @@ export const eject = async (root: string): Promise<string[]> => {
|
|
|
321
540
|
}
|
|
322
541
|
|
|
323
542
|
// Scalar API/AsyncAPI reference pages, mirrored from the generated runtime so
|
|
324
|
-
// the ejected app keeps its reference routes
|
|
543
|
+
// the ejected app keeps its reference routes — including the warnings (a
|
|
544
|
+
// missing spec file, a route collision), which the caller surfaces exactly
|
|
545
|
+
// like the generated-runtime path does.
|
|
546
|
+
const warnings: string[] = [];
|
|
325
547
|
if (hasScalarReferences(config)) {
|
|
326
548
|
const references = await buildReferenceFiles({
|
|
327
549
|
config,
|
|
328
550
|
contentRoutes: new Set(project.graph.pages.map((page) => page.route)),
|
|
329
551
|
root,
|
|
330
552
|
});
|
|
553
|
+
warnings.push(...references.warnings);
|
|
331
554
|
for (const file of references.files) {
|
|
332
555
|
files.push({
|
|
333
556
|
content: file.content,
|
|
@@ -345,7 +568,12 @@ export const eject = async (root: string): Promise<string[]> => {
|
|
|
345
568
|
...examples.examples.map((example) => ({
|
|
346
569
|
content: exampleWrapperTemplate(example),
|
|
347
570
|
path: join(genDir, "examples", `${exampleSlug(example.path)}.astro`),
|
|
348
|
-
}))
|
|
571
|
+
})),
|
|
572
|
+
...examplesPreviewFiles(
|
|
573
|
+
srcDir,
|
|
574
|
+
config.basePath,
|
|
575
|
+
examples.examples.length > 0
|
|
576
|
+
)
|
|
349
577
|
);
|
|
350
578
|
|
|
351
579
|
// Materialize staged source bodies under `<root>/blume-staged/<source>/<ref>`,
|
|
@@ -377,5 +605,5 @@ export const eject = async (root: string): Promise<string[]> => {
|
|
|
377
605
|
// The hidden runtime is no longer the source of truth.
|
|
378
606
|
await rm(context.outDir, { force: true, recursive: true });
|
|
379
607
|
|
|
380
|
-
return written.map((file) => file.path);
|
|
608
|
+
return { files: written.map((file) => file.path), warnings };
|
|
381
609
|
};
|
package/src/registry/registry.ts
CHANGED
|
@@ -23,9 +23,6 @@ export interface RegistryItem {
|
|
|
23
23
|
/** Absolute path to the blume package `src` directory (the copy source root). */
|
|
24
24
|
export const packageSrc = join(packageRoot(), "src");
|
|
25
25
|
|
|
26
|
-
/** Absolute path to the bundled registry item sources. */
|
|
27
|
-
export const itemsRoot = join(packageRoot(), "src", "registry", "items");
|
|
28
|
-
|
|
29
26
|
/**
|
|
30
27
|
* A built-in layout component offered as editable source. `blume add` rewrites
|
|
31
28
|
* its relative imports to `blume/*`, so it renders identically to the built-in
|
package/src/search/build.ts
CHANGED
|
@@ -15,6 +15,9 @@ export const buildSearchIndex = async (outDir: string): Promise<number> => {
|
|
|
15
15
|
throw new Error("Failed to create Pagefind index.");
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
// These awaits are strictly ordered, not independent: the directory must be
|
|
19
|
+
// indexed before its files are written, and the index closed only after.
|
|
20
|
+
// oxlint-disable-next-line react-doctor/async-parallel
|
|
18
21
|
const result = await index.addDirectory({ path: outDir });
|
|
19
22
|
await index.writeFiles({ outputPath: join(outDir, "pagefind") });
|
|
20
23
|
await pagefind.close();
|
package/src/search/documents.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { applyAudienceVisibility } from "../ai/visibility.ts";
|
|
2
|
+
import type { VisibilityAudience } from "../ai/visibility.ts";
|
|
1
3
|
import matter from "../core/frontmatter.ts";
|
|
2
4
|
import { contentIndexable } from "../core/manifest.ts";
|
|
3
5
|
import type { BlumeProject } from "../core/project-graph.ts";
|
|
@@ -65,9 +67,9 @@ const toPlainText = (markdown: string): string => {
|
|
|
65
67
|
for (const match of withoutBlocks.matchAll(INLINE_CODE)) {
|
|
66
68
|
const start = match.index ?? 0;
|
|
67
69
|
pieces.push(
|
|
68
|
-
withoutBlocks.slice(cursor, start).replaceAll(HTML_OR_JSX, " ")
|
|
70
|
+
withoutBlocks.slice(cursor, start).replaceAll(HTML_OR_JSX, " "),
|
|
71
|
+
match.groups?.code ?? ""
|
|
69
72
|
);
|
|
70
|
-
pieces.push(match.groups?.code ?? "");
|
|
71
73
|
cursor = start + match[0].length;
|
|
72
74
|
}
|
|
73
75
|
pieces.push(withoutBlocks.slice(cursor).replaceAll(HTML_OR_JSX, " "));
|
|
@@ -93,9 +95,21 @@ interface Crumbs {
|
|
|
93
95
|
*/
|
|
94
96
|
const buildCrumbIndex = (sidebar: NavNode[]): Map<string, Crumbs> => {
|
|
95
97
|
const index = new Map<string, Crumbs>();
|
|
98
|
+
// A config-sidebar section's landing page (the group's `root`) lives on the
|
|
99
|
+
// *group* node, not on any page leaf — record it under the group's own label
|
|
100
|
+
// so the section's landing page carries the same facet as its children. A
|
|
101
|
+
// real page leaf for the route (filesystem sidebars emit index pages as
|
|
102
|
+
// leaves) wins, so group routes are merged in only where no leaf claimed one.
|
|
103
|
+
const groupRoutes = new Map<string, Crumbs>();
|
|
96
104
|
const walk = (nodes: NavNode[], trail: string[]): void => {
|
|
97
105
|
for (const node of nodes) {
|
|
98
106
|
if (node.kind === "group") {
|
|
107
|
+
if (node.route && !groupRoutes.has(node.route)) {
|
|
108
|
+
groupRoutes.set(node.route, {
|
|
109
|
+
breadcrumb: [...trail, node.label],
|
|
110
|
+
section: node.label,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
99
113
|
walk(node.children, [...trail, node.label]);
|
|
100
114
|
} else if (node.route) {
|
|
101
115
|
index.set(node.route, {
|
|
@@ -106,6 +120,11 @@ const buildCrumbIndex = (sidebar: NavNode[]): Map<string, Crumbs> => {
|
|
|
106
120
|
}
|
|
107
121
|
};
|
|
108
122
|
walk(sidebar, []);
|
|
123
|
+
for (const [route, crumbs] of groupRoutes) {
|
|
124
|
+
if (!index.has(route)) {
|
|
125
|
+
index.set(route, crumbs);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
109
128
|
return index;
|
|
110
129
|
};
|
|
111
130
|
|
|
@@ -122,10 +141,19 @@ const buildCrumbIndex = (sidebar: NavNode[]): Map<string, Crumbs> => {
|
|
|
122
141
|
* searchable text; `"markdown"` keeps the body's Markdown — code blocks, lists,
|
|
123
142
|
* headings — for Ask AI grounding, where fenced examples are often the answer
|
|
124
143
|
* and stripping them makes the model unable to cite content the docs do contain.
|
|
144
|
+
*
|
|
145
|
+
* `audience` resolves `<Visibility>` blocks before extraction: `"web"`
|
|
146
|
+
* (default) keeps web-only content and drops agents-only blocks — the site
|
|
147
|
+
* search and hosted syncs must not surface content the page hides — while
|
|
148
|
+
* `"agents"` mirrors llms-full.txt/MCP `get_page` (web removed, agents kept).
|
|
125
149
|
*/
|
|
126
150
|
export const buildSearchDocuments = async (
|
|
127
151
|
project: BlumeProject,
|
|
128
|
-
options?: {
|
|
152
|
+
options?: {
|
|
153
|
+
includeWhenDisabled?: boolean;
|
|
154
|
+
content?: "markdown" | "plain";
|
|
155
|
+
audience?: VisibilityAudience;
|
|
156
|
+
}
|
|
129
157
|
): Promise<SearchDocument[]> => {
|
|
130
158
|
const pageById = new Map(project.graph.pages.map((page) => [page.id, page]));
|
|
131
159
|
|
|
@@ -157,8 +185,12 @@ export const buildSearchDocuments = async (
|
|
|
157
185
|
const page = pageById.get(route.id);
|
|
158
186
|
const raw = page ? await readEntryText(project, page) : "";
|
|
159
187
|
const source = raw ? matter(raw).content : "";
|
|
188
|
+
const visible = applyAudienceVisibility(
|
|
189
|
+
source,
|
|
190
|
+
options?.audience ?? "web"
|
|
191
|
+
);
|
|
160
192
|
const body =
|
|
161
|
-
options?.content === "markdown" ?
|
|
193
|
+
options?.content === "markdown" ? visible.trim() : toPlainText(visible);
|
|
162
194
|
const tags = page?.meta?.search?.tags;
|
|
163
195
|
const crumb = crumbs.get(route.path);
|
|
164
196
|
return {
|
|
@@ -40,10 +40,12 @@ export const syncTypesense = async (
|
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
const collection = client.collections(config.collection);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
.
|
|
46
|
-
|
|
43
|
+
let exists = true;
|
|
44
|
+
try {
|
|
45
|
+
await collection.retrieve();
|
|
46
|
+
} catch {
|
|
47
|
+
exists = false;
|
|
48
|
+
}
|
|
47
49
|
if (exists) {
|
|
48
50
|
await collection.delete();
|
|
49
51
|
}
|
package/src/seo/jsonld.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { Crumb } from "../components/layout/nav-utils.ts";
|
|
2
|
+
import { normalizeBasePath, withBasePath } from "../core/base-path.ts";
|
|
3
|
+
|
|
4
|
+
/** A date-ish value carried through frontmatter (string, YAML Date, or unset). */
|
|
5
|
+
type DateInput = string | Date | null;
|
|
2
6
|
|
|
3
7
|
/** Inputs for a page's JSON-LD, all known at render time in RootLayout. */
|
|
4
8
|
export interface StructuredDataInput {
|
|
@@ -9,12 +13,14 @@ export interface StructuredDataInput {
|
|
|
9
13
|
description?: string;
|
|
10
14
|
/** Page route, e.g. `/blog/post`. */
|
|
11
15
|
route: string;
|
|
16
|
+
/** Deployment base (`import.meta.env.BASE_URL`); prefixed onto absolute URLs. */
|
|
17
|
+
base?: string;
|
|
12
18
|
/** Content type — `blog` and `changelog` map to richer article types. */
|
|
13
19
|
pageType?: string;
|
|
14
20
|
/** Publish date (string or YAML Date); emitted as ISO `datePublished`. */
|
|
15
|
-
published?:
|
|
21
|
+
published?: DateInput;
|
|
16
22
|
/** Last-modified date; emitted as ISO `dateModified`. */
|
|
17
|
-
modified?:
|
|
23
|
+
modified?: DateInput;
|
|
18
24
|
/** BCP-47 language tag for `inLanguage`; defaults to `en`. */
|
|
19
25
|
locale?: string;
|
|
20
26
|
breadcrumbs: Crumb[];
|
|
@@ -31,7 +37,7 @@ const trimSlash = (value: string): string => value.replace(/\/$/u, "");
|
|
|
31
37
|
const absolute = (base: string | null, path: string): string =>
|
|
32
38
|
base ? `${base}${path}` : path;
|
|
33
39
|
|
|
34
|
-
const toIso = (value:
|
|
40
|
+
const toIso = (value: DateInput | undefined): string | undefined => {
|
|
35
41
|
if (!value) {
|
|
36
42
|
return;
|
|
37
43
|
}
|
|
@@ -49,7 +55,11 @@ export const buildStructuredData = (
|
|
|
49
55
|
input: StructuredDataInput
|
|
50
56
|
): Record<string, unknown> | null => {
|
|
51
57
|
const base = input.siteUrl ? trimSlash(input.siteUrl) : null;
|
|
52
|
-
|
|
58
|
+
// Routes carry `basePath`; a `deployment.base` subdirectory is layered on top
|
|
59
|
+
// so JSON-LD URLs match the served location.
|
|
60
|
+
const deployBase = normalizeBasePath(input.base);
|
|
61
|
+
const pageUrl = absolute(base, withBasePath(deployBase, input.route));
|
|
62
|
+
const rootUrl = absolute(base, deployBase);
|
|
53
63
|
const graph: Record<string, unknown>[] = [];
|
|
54
64
|
|
|
55
65
|
if (base) {
|
|
@@ -57,7 +67,7 @@ export const buildStructuredData = (
|
|
|
57
67
|
"@id": `${base}#website`,
|
|
58
68
|
"@type": "WebSite",
|
|
59
69
|
name: input.siteName,
|
|
60
|
-
url:
|
|
70
|
+
url: rootUrl,
|
|
61
71
|
});
|
|
62
72
|
}
|
|
63
73
|
|
|
@@ -88,20 +98,21 @@ export const buildStructuredData = (
|
|
|
88
98
|
}
|
|
89
99
|
graph.push(node);
|
|
90
100
|
|
|
91
|
-
|
|
101
|
+
// Google requires `item` on every ListItem except the last; sidebar groups
|
|
102
|
+
// without an index page produce route-less crumbs, so those are dropped
|
|
103
|
+
// (positions renumbered) rather than emitted as invalid link-less items.
|
|
104
|
+
const linked = input.breadcrumbs.filter(
|
|
105
|
+
(crumb): crumb is Required<Crumb> => typeof crumb.route === "string"
|
|
106
|
+
);
|
|
107
|
+
if (linked.length > 1) {
|
|
92
108
|
graph.push({
|
|
93
109
|
"@type": "BreadcrumbList",
|
|
94
|
-
itemListElement:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (crumb.route) {
|
|
101
|
-
item.item = absolute(base, crumb.route);
|
|
102
|
-
}
|
|
103
|
-
return item;
|
|
104
|
-
}),
|
|
110
|
+
itemListElement: linked.map((crumb, index) => ({
|
|
111
|
+
"@type": "ListItem",
|
|
112
|
+
item: absolute(base, withBasePath(deployBase, crumb.route)),
|
|
113
|
+
name: crumb.label,
|
|
114
|
+
position: index + 1,
|
|
115
|
+
})),
|
|
105
116
|
});
|
|
106
117
|
}
|
|
107
118
|
}
|