blume 1.6.0 → 1.6.2
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 +21 -0
- package/dist/cli/index.js +1318 -270
- package/dist/cli/index.js.map +68 -61
- package/dist/types/core/config-input.d.ts +9 -0
- package/dist/types/core/data.d.ts +12 -1
- package/dist/types/core/i18n-ui.d.ts +4 -0
- package/dist/types/core/schema.d.ts +7 -0
- package/dist/types/core/types.d.ts +6 -0
- package/dist/types/openapi/references.d.ts +5 -0
- package/docs/07-faq.mdx +9 -9
- package/docs/advanced/api-reference.mdx +10 -1
- package/docs/advanced/custom-pages.mdx +3 -1
- package/docs/advanced/graphql.mdx +1 -1
- package/docs/configuration/ai.mdx +76 -7
- package/docs/configuration/seo.mdx +3 -3
- package/docs/configuration/theming.mdx +6 -0
- package/docs/content/components.mdx +8 -1
- package/docs/index.mdx +2 -2
- package/package.json +53 -53
- package/skills/blume/SKILL.md +2 -2
- package/src/ai/agent-readability.ts +60 -17
- package/src/ai/api/handlers.ts +273 -0
- package/src/ai/api/paths.ts +14 -0
- package/src/ai/api/problem.ts +63 -0
- package/src/ai/api/spec.ts +681 -0
- package/src/ai/api-catalog.ts +11 -1
- package/src/ai/link-headers.ts +12 -3
- package/src/ai/llms.ts +9 -2
- package/src/ai/mcp/query.ts +390 -0
- package/src/ai/mcp/server.ts +32 -352
- package/src/astro/examples.ts +29 -2
- package/src/astro/generate.ts +256 -64
- package/src/astro/index.ts +7 -0
- package/src/astro/markdown-negotiation.ts +1 -1
- package/src/astro/runtime-modules.ts +196 -0
- package/src/astro/templates.ts +398 -38
- package/src/cli/commands/build.ts +9 -1
- package/src/cli/commands/dev.ts +6 -3
- package/src/cli/host-args.ts +18 -0
- package/src/cli/index.ts +2 -1
- package/src/components/copy-feedback.ts +93 -9
- package/src/components/islands/ask-ai.tsx +4 -1
- package/src/components/islands/hooks.ts +3 -1
- package/src/components/layout/PageActions.astro +25 -14
- package/src/core/config-input.ts +9 -0
- package/src/core/data.ts +17 -2
- package/src/core/define-components.ts +2 -0
- package/src/core/i18n-ui.ts +3 -0
- package/src/core/includes.ts +2 -1
- package/src/core/manifest.ts +10 -0
- package/src/core/schema.ts +20 -5
- package/src/core/types.ts +6 -0
- 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 +1 -0
- 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/core/version-cut.ts +5 -3
- package/src/deploy/vercel-negotiation.ts +97 -6
- package/src/og/card.ts +1 -1
- package/src/openapi/references.ts +8 -0
- package/src/openapi/render-mdx.ts +18 -4
- package/src/openapi/scalar.ts +0 -4
- package/src/registry/eject.ts +36 -17
- package/src/theme/entry.ts +2 -2
- package/src/theme/sources.ts +49 -0
package/src/core/ui-packs/id.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const id: UIStringsOverride = {
|
|
|
11
11
|
copyClaudeCode: "Salin perintah Claude Code",
|
|
12
12
|
copyCode: "Salin kode",
|
|
13
13
|
copyCodex: "Salin perintah Codex",
|
|
14
|
+
copyFailed: "Gagal menyalin",
|
|
14
15
|
copyMarkdown: "Salin sebagai Markdown",
|
|
15
16
|
copyServerUrl: "Salin URL server",
|
|
16
17
|
edit: "Edit di GitHub",
|
package/src/core/ui-packs/it.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const it: UIStringsOverride = {
|
|
|
11
11
|
copyClaudeCode: "Copia comando di Claude Code",
|
|
12
12
|
copyCode: "Copia codice",
|
|
13
13
|
copyCodex: "Copia comando di Codex",
|
|
14
|
+
copyFailed: "Copia non riuscita",
|
|
14
15
|
copyMarkdown: "Copia come Markdown",
|
|
15
16
|
copyServerUrl: "Copia URL del server",
|
|
16
17
|
edit: "Modifica su GitHub",
|
package/src/core/ui-packs/ja.ts
CHANGED
package/src/core/ui-packs/ko.ts
CHANGED
package/src/core/ui-packs/nl.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const nl: UIStringsOverride = {
|
|
|
11
11
|
copyClaudeCode: "Claude Code-opdracht kopiëren",
|
|
12
12
|
copyCode: "Code kopiëren",
|
|
13
13
|
copyCodex: "Codex-opdracht kopiëren",
|
|
14
|
+
copyFailed: "Kopiëren mislukt",
|
|
14
15
|
copyMarkdown: "Kopiëren als Markdown",
|
|
15
16
|
copyServerUrl: "Server-URL kopiëren",
|
|
16
17
|
edit: "Bewerken op GitHub",
|
package/src/core/ui-packs/no.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const no: UIStringsOverride = {
|
|
|
11
11
|
copyClaudeCode: "Kopier Claude Code-kommando",
|
|
12
12
|
copyCode: "Kopier kode",
|
|
13
13
|
copyCodex: "Kopier Codex-kommando",
|
|
14
|
+
copyFailed: "Kopiering mislyktes",
|
|
14
15
|
copyMarkdown: "Kopier som Markdown",
|
|
15
16
|
copyServerUrl: "Kopier server-URL",
|
|
16
17
|
edit: "Rediger på GitHub",
|
package/src/core/ui-packs/pl.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const pl: UIStringsOverride = {
|
|
|
11
11
|
copyClaudeCode: "Kopiuj polecenie Claude Code",
|
|
12
12
|
copyCode: "Kopiuj kod",
|
|
13
13
|
copyCodex: "Kopiuj polecenie Codex",
|
|
14
|
+
copyFailed: "Kopiowanie nie powiodło się",
|
|
14
15
|
copyMarkdown: "Kopiuj jako Markdown",
|
|
15
16
|
copyServerUrl: "Kopiuj adres URL serwera",
|
|
16
17
|
edit: "Edytuj na GitHubie",
|
|
@@ -11,6 +11,7 @@ export const ptBR: UIStringsOverride = {
|
|
|
11
11
|
copyClaudeCode: "Copiar comando do Claude Code",
|
|
12
12
|
copyCode: "Copiar código",
|
|
13
13
|
copyCodex: "Copiar comando do Codex",
|
|
14
|
+
copyFailed: "Falha ao copiar",
|
|
14
15
|
copyMarkdown: "Copiar como Markdown",
|
|
15
16
|
copyServerUrl: "Copiar URL do servidor",
|
|
16
17
|
edit: "Editar no GitHub",
|
package/src/core/ui-packs/pt.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const pt: UIStringsOverride = {
|
|
|
11
11
|
copyClaudeCode: "Copiar comando do Claude Code",
|
|
12
12
|
copyCode: "Copiar código",
|
|
13
13
|
copyCodex: "Copiar comando do Codex",
|
|
14
|
+
copyFailed: "Falha ao copiar",
|
|
14
15
|
copyMarkdown: "Copiar como Markdown",
|
|
15
16
|
copyServerUrl: "Copiar URL do servidor",
|
|
16
17
|
edit: "Editar no GitHub",
|
package/src/core/ui-packs/ro.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const ro: UIStringsOverride = {
|
|
|
11
11
|
copyClaudeCode: "Copiază comanda Claude Code",
|
|
12
12
|
copyCode: "Copiază codul",
|
|
13
13
|
copyCodex: "Copiază comanda Codex",
|
|
14
|
+
copyFailed: "Copierea a eșuat",
|
|
14
15
|
copyMarkdown: "Copiază ca Markdown",
|
|
15
16
|
copyServerUrl: "Copiază URL-ul serverului",
|
|
16
17
|
edit: "Editează pe GitHub",
|
package/src/core/ui-packs/ru.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const ru: UIStringsOverride = {
|
|
|
11
11
|
copyClaudeCode: "Скопировать команду Claude Code",
|
|
12
12
|
copyCode: "Скопировать код",
|
|
13
13
|
copyCodex: "Скопировать команду Codex",
|
|
14
|
+
copyFailed: "Не удалось скопировать",
|
|
14
15
|
copyMarkdown: "Скопировать как Markdown",
|
|
15
16
|
copyServerUrl: "Скопировать URL сервера",
|
|
16
17
|
edit: "Редактировать на GitHub",
|
package/src/core/ui-packs/sk.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const sk: UIStringsOverride = {
|
|
|
11
11
|
copyClaudeCode: "Kopírovať príkaz Claude Code",
|
|
12
12
|
copyCode: "Kopírovať kód",
|
|
13
13
|
copyCodex: "Kopírovať príkaz Codex",
|
|
14
|
+
copyFailed: "Kopírovanie zlyhalo",
|
|
14
15
|
copyMarkdown: "Kopírovať ako Markdown",
|
|
15
16
|
copyServerUrl: "Kopírovať URL servera",
|
|
16
17
|
edit: "Upraviť na GitHube",
|
package/src/core/ui-packs/sr.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const sr: UIStringsOverride = {
|
|
|
11
11
|
copyClaudeCode: "Копирај Claude Code команду",
|
|
12
12
|
copyCode: "Копирај код",
|
|
13
13
|
copyCodex: "Копирај Codex команду",
|
|
14
|
+
copyFailed: "Копирање није успело",
|
|
14
15
|
copyMarkdown: "Копирај као Markdown",
|
|
15
16
|
copyServerUrl: "Копирај URL сервера",
|
|
16
17
|
edit: "Уреди на GitHub-у",
|
package/src/core/ui-packs/sv.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const sv: UIStringsOverride = {
|
|
|
11
11
|
copyClaudeCode: "Kopiera Claude Code-kommando",
|
|
12
12
|
copyCode: "Kopiera kod",
|
|
13
13
|
copyCodex: "Kopiera Codex-kommando",
|
|
14
|
+
copyFailed: "Kopiering misslyckades",
|
|
14
15
|
copyMarkdown: "Kopiera som Markdown",
|
|
15
16
|
copyServerUrl: "Kopiera server-URL",
|
|
16
17
|
edit: "Redigera på GitHub",
|
package/src/core/ui-packs/th.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const th: UIStringsOverride = {
|
|
|
11
11
|
copyClaudeCode: "คัดลอกคำสั่ง Claude Code",
|
|
12
12
|
copyCode: "คัดลอกโค้ด",
|
|
13
13
|
copyCodex: "คัดลอกคำสั่ง Codex",
|
|
14
|
+
copyFailed: "คัดลอกไม่สำเร็จ",
|
|
14
15
|
copyMarkdown: "คัดลอกเป็น Markdown",
|
|
15
16
|
copyServerUrl: "คัดลอก URL ของเซิร์ฟเวอร์",
|
|
16
17
|
edit: "แก้ไขบน GitHub",
|
package/src/core/ui-packs/tr.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const tr: UIStringsOverride = {
|
|
|
11
11
|
copyClaudeCode: "Claude Code komutunu kopyala",
|
|
12
12
|
copyCode: "Kodu kopyala",
|
|
13
13
|
copyCodex: "Codex komutunu kopyala",
|
|
14
|
+
copyFailed: "Kopyalama başarısız oldu",
|
|
14
15
|
copyMarkdown: "Markdown olarak kopyala",
|
|
15
16
|
copyServerUrl: "Sunucu URL'sini kopyala",
|
|
16
17
|
edit: "GitHub'da düzenle",
|
package/src/core/ui-packs/uk.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const uk: UIStringsOverride = {
|
|
|
11
11
|
copyClaudeCode: "Скопіювати команду Claude Code",
|
|
12
12
|
copyCode: "Скопіювати код",
|
|
13
13
|
copyCodex: "Скопіювати команду Codex",
|
|
14
|
+
copyFailed: "Не вдалося скопіювати",
|
|
14
15
|
copyMarkdown: "Скопіювати як Markdown",
|
|
15
16
|
copyServerUrl: "Скопіювати URL сервера",
|
|
16
17
|
edit: "Редагувати на GitHub",
|
package/src/core/ui-packs/vi.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const vi: UIStringsOverride = {
|
|
|
11
11
|
copyClaudeCode: "Sao chép lệnh Claude Code",
|
|
12
12
|
copyCode: "Sao chép mã",
|
|
13
13
|
copyCodex: "Sao chép lệnh Codex",
|
|
14
|
+
copyFailed: "Sao chép thất bại",
|
|
14
15
|
copyMarkdown: "Sao chép dưới dạng Markdown",
|
|
15
16
|
copyServerUrl: "Sao chép URL máy chủ",
|
|
16
17
|
edit: "Chỉnh sửa trên GitHub",
|
package/src/core/ui-packs/zh.ts
CHANGED
package/src/core/version-cut.ts
CHANGED
|
@@ -177,12 +177,14 @@ export const insertArchivedVersion = async (
|
|
|
177
177
|
const indent = text.slice(lineStart).match(/^\s*/u)?.[0] ?? "";
|
|
178
178
|
const rest = text.slice(insertAt);
|
|
179
179
|
// Match the array's authored shape: empty stays bare, an inline array gets
|
|
180
|
-
// an inline entry, a multiline array gets its own indented line
|
|
180
|
+
// an inline entry, a multiline array gets its own indented line — on the
|
|
181
|
+
// file's own line ending, so a CRLF config doesn't gain a lone LF.
|
|
182
|
+
const eol = /^\r?\n/u.exec(rest)?.[0];
|
|
181
183
|
let entry: string;
|
|
182
184
|
if (rest.trimStart().startsWith("]")) {
|
|
183
185
|
entry = `{ id: "${id}" }`;
|
|
184
|
-
} else if (
|
|
185
|
-
entry =
|
|
186
|
+
} else if (eol) {
|
|
187
|
+
entry = `${eol}${indent} { id: "${id}" },`;
|
|
186
188
|
} else {
|
|
187
189
|
entry = `{ id: "${id}" }, `;
|
|
188
190
|
}
|
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
* into it so a content-page request that prefers `text/markdown` is rewritten
|
|
10
10
|
* (not redirected) to the page's prerendered `.md` mirror — the deployed
|
|
11
11
|
* counterpart of the dev-server rewrite in `astro/markdown-negotiation.ts`.
|
|
12
|
+
* The same routing config also answers a *missing* page: a request that
|
|
13
|
+
* prefers Markdown (or asks for a `.md` URL no page backs) gets the
|
|
14
|
+
* prerendered Markdown 404 body with the 404 status, instead of the HTML
|
|
15
|
+
* shell, and one that prefers JSON (or asks for a `.json` URL) gets the
|
|
16
|
+
* prerendered problem-details 404.
|
|
12
17
|
*/
|
|
13
18
|
|
|
14
19
|
/**
|
|
@@ -24,6 +29,15 @@
|
|
|
24
29
|
export const ACCEPT_MARKDOWN_HEADER_VALUE =
|
|
25
30
|
"(.*,)?\\s*text/(x-)?markdown(\\s*[;,].*)?$";
|
|
26
31
|
|
|
32
|
+
/**
|
|
33
|
+
* The JSON counterpart, for the problem-details 404: `application/json` or
|
|
34
|
+
* `application/problem+json`. Browsers never send either on a navigation
|
|
35
|
+
* (the catch-all wildcard does not match), so ordinary page requests are
|
|
36
|
+
* unaffected.
|
|
37
|
+
*/
|
|
38
|
+
export const ACCEPT_JSON_HEADER_VALUE =
|
|
39
|
+
"(.*,)?\\s*application/(problem\\+)?json(\\s*[;,].*)?$";
|
|
40
|
+
|
|
27
41
|
/**
|
|
28
42
|
* A Build Output API route — the subset these helpers read and write. Parsed
|
|
29
43
|
* routes keep whatever other fields they carry at runtime; only these are
|
|
@@ -47,8 +61,65 @@ const ACCEPT_MARKDOWN_CONDITION: VercelRoute["has"] = [
|
|
|
47
61
|
{ key: "accept", type: "header", value: ACCEPT_MARKDOWN_HEADER_VALUE },
|
|
48
62
|
];
|
|
49
63
|
|
|
64
|
+
const ACCEPT_JSON_CONDITION: VercelRoute["has"] = [
|
|
65
|
+
{ key: "accept", type: "header", value: ACCEPT_JSON_HEADER_VALUE },
|
|
66
|
+
];
|
|
67
|
+
|
|
50
68
|
const VARY_ACCEPT = { vary: "Accept" };
|
|
51
69
|
|
|
70
|
+
/** Where the prerendered Markdown 404 (`pages/404.md.ts`) lands. */
|
|
71
|
+
const NOT_FOUND_MARKDOWN_DEST = "/404.md";
|
|
72
|
+
|
|
73
|
+
/** Where the prerendered JSON 404 (`pages/404.json.ts`) lands. */
|
|
74
|
+
const NOT_FOUND_JSON_DEST = "/404.json";
|
|
75
|
+
|
|
76
|
+
/** The adapter's own not-found fallback — the anchor the Markdown 404 precedes. */
|
|
77
|
+
const NOT_FOUND_HTML_DEST = "/404.html";
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Miss-phase routes that answer a missing page with the Markdown 404 body: any
|
|
81
|
+
* path when the client prefers Markdown, and any `.md`/`.mdx` URL (a request
|
|
82
|
+
* for a raw-Markdown mirror that has no page wants Markdown back, not the HTML
|
|
83
|
+
* shell). Both keep the 404 status. Spliced immediately before the adapter's
|
|
84
|
+
* `/404.html` fallback, so they run after every server route (the MCP
|
|
85
|
+
* endpoint, server islands, images) has had its turn and never hijack a
|
|
86
|
+
* request one of those would have answered.
|
|
87
|
+
*/
|
|
88
|
+
const NOT_FOUND_MARKDOWN_ROUTES: readonly VercelRoute[] = [
|
|
89
|
+
{
|
|
90
|
+
dest: NOT_FOUND_MARKDOWN_DEST,
|
|
91
|
+
has: ACCEPT_MARKDOWN_CONDITION,
|
|
92
|
+
headers: VARY_ACCEPT,
|
|
93
|
+
src: "^/.*$",
|
|
94
|
+
status: 404,
|
|
95
|
+
},
|
|
96
|
+
{ dest: NOT_FOUND_MARKDOWN_DEST, src: "^/.*\\.mdx?$", status: 404 },
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The JSON 404's miss-phase routes, the problem-details twin of the Markdown
|
|
101
|
+
* ones: any path when the client prefers JSON, and any `.json` URL no file
|
|
102
|
+
* backs. Spliced at the same anchor, after every server route — so the
|
|
103
|
+
* `/api/` catch-all (which answers its own namespace with a problem document)
|
|
104
|
+
* has already had its turn.
|
|
105
|
+
*/
|
|
106
|
+
const NOT_FOUND_JSON_ROUTES: readonly VercelRoute[] = [
|
|
107
|
+
{
|
|
108
|
+
dest: NOT_FOUND_JSON_DEST,
|
|
109
|
+
has: ACCEPT_JSON_CONDITION,
|
|
110
|
+
headers: VARY_ACCEPT,
|
|
111
|
+
src: "^/.*$",
|
|
112
|
+
status: 404,
|
|
113
|
+
},
|
|
114
|
+
{ dest: NOT_FOUND_JSON_DEST, src: "^/.*\\.json$", status: 404 },
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
/** Which prerendered 404 twins the build emitted, so their routes get wired. */
|
|
118
|
+
export interface NotFoundVariants {
|
|
119
|
+
json?: boolean;
|
|
120
|
+
markdown?: boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
52
123
|
/**
|
|
53
124
|
* Vercel rejects route `src` patterns longer than 4096 characters, so route
|
|
54
125
|
* alternations are split across as many route entries as needed. The budget
|
|
@@ -186,12 +257,15 @@ export const TRAILING_SLASH_REDIRECT: VercelRoute = {
|
|
|
186
257
|
* user-authored route of that identical shape would be semantically equal to
|
|
187
258
|
* the one re-added); the homepage `Link` route by its three-field
|
|
188
259
|
* continue-with-link shape (the Build Output config is adapter-generated, so
|
|
189
|
-
* no user-authored route competes in this file)
|
|
260
|
+
* no user-authored route competes in this file); the Markdown 404 routes by
|
|
261
|
+
* their `/404.md` destination.
|
|
190
262
|
*/
|
|
191
263
|
const isNegotiationRoute = (route: VercelRoute): boolean =>
|
|
192
264
|
route.has?.some(
|
|
193
265
|
(condition) => condition.value === ACCEPT_MARKDOWN_HEADER_VALUE
|
|
194
266
|
) === true ||
|
|
267
|
+
(route.dest === NOT_FOUND_MARKDOWN_DEST && route.status === 404) ||
|
|
268
|
+
(route.dest === NOT_FOUND_JSON_DEST && route.status === 404) ||
|
|
195
269
|
(route.continue === true &&
|
|
196
270
|
route.headers?.vary === "Accept" &&
|
|
197
271
|
isString(route.src) &&
|
|
@@ -213,17 +287,22 @@ const isNegotiationRoute = (route: VercelRoute): boolean =>
|
|
|
213
287
|
* platform's mechanism for extensionless static files (e.g. the Web Bot Auth
|
|
214
288
|
* signature directory). The trailing-slash 308 redirect is always spliced in
|
|
215
289
|
* alongside, so slashed duplicates of every page collapse onto the canonical
|
|
216
|
-
* slashless URL.
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
290
|
+
* slashless URL. For each 404 twin the build emitted (`notFound.markdown` for
|
|
291
|
+
* `404.md`, `notFound.json` for `404.json`), its routes go into the miss
|
|
292
|
+
* phase right before the adapter's `/404.html` fallback — and nowhere when
|
|
293
|
+
* that fallback is absent, since a `dest` with no file behind it would serve
|
|
294
|
+
* nothing. Returns the updated JSON
|
|
295
|
+
* text (tab-indented, like the adapter's own output), or `null` when there is
|
|
296
|
+
* nowhere safe to splice: an unparsable config, no `routes` array, or no
|
|
297
|
+
* `handle: "filesystem"` marker to anchor the splice.
|
|
220
298
|
*/
|
|
221
299
|
export const injectNegotiationRoutes = (
|
|
222
300
|
configText: string,
|
|
223
301
|
routePaths: readonly string[],
|
|
224
302
|
homeLinkHeader?: string | null,
|
|
225
303
|
contentTypeOverrides?: Record<string, string>,
|
|
226
|
-
homeTokens?: number
|
|
304
|
+
homeTokens?: number,
|
|
305
|
+
notFound: NotFoundVariants = {}
|
|
227
306
|
): string | null => {
|
|
228
307
|
const overrideEntries = Object.entries(contentTypeOverrides ?? {});
|
|
229
308
|
let config: {
|
|
@@ -272,6 +351,18 @@ export const injectNegotiationRoutes = (
|
|
|
272
351
|
...rewriteRoutes,
|
|
273
352
|
TRAILING_SLASH_REDIRECT
|
|
274
353
|
);
|
|
354
|
+
const notFoundRoutes = [
|
|
355
|
+
...(notFound.markdown ? NOT_FOUND_MARKDOWN_ROUTES : []),
|
|
356
|
+
...(notFound.json ? NOT_FOUND_JSON_ROUTES : []),
|
|
357
|
+
];
|
|
358
|
+
if (notFoundRoutes.length > 0) {
|
|
359
|
+
const fallbackIndex = routes.findIndex(
|
|
360
|
+
(route) => route.status === 404 && route.dest === NOT_FOUND_HTML_DEST
|
|
361
|
+
);
|
|
362
|
+
if (fallbackIndex !== -1) {
|
|
363
|
+
routes.splice(fallbackIndex, 0, ...notFoundRoutes);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
275
366
|
config.routes = routes;
|
|
276
367
|
return `${JSON.stringify(config, null, "\t")}\n`;
|
|
277
368
|
};
|
package/src/og/card.ts
CHANGED
|
@@ -81,7 +81,7 @@ export interface OgCardOptions {
|
|
|
81
81
|
accent?: string;
|
|
82
82
|
/** Brand/site name shown in the top-left lockup. */
|
|
83
83
|
brand?: string;
|
|
84
|
-
/** Muted subtitle under the headline (
|
|
84
|
+
/** Muted subtitle under the headline (the page description, else the site's). */
|
|
85
85
|
description?: string;
|
|
86
86
|
/**
|
|
87
87
|
* Inlined SVG markup of the configured logo, painted into the brand
|
|
@@ -54,6 +54,11 @@ export interface ReferenceSource {
|
|
|
54
54
|
includeInSearch: boolean;
|
|
55
55
|
/** Whether generated pages emit noindex metadata and stay out of the sitemap. */
|
|
56
56
|
noindex: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Whether operation meta descriptions end with the generated English
|
|
59
|
+
* "Reference for …" sentence, or carry the spec's own prose alone.
|
|
60
|
+
*/
|
|
61
|
+
seoDescriptionSuffix: boolean;
|
|
57
62
|
/** Local path or `http(s)` URL, verbatim from config. */
|
|
58
63
|
spec: string;
|
|
59
64
|
/**
|
|
@@ -132,6 +137,7 @@ interface Block {
|
|
|
132
137
|
label?: string;
|
|
133
138
|
noindex: boolean;
|
|
134
139
|
route?: string;
|
|
140
|
+
seoDescriptionSuffix: boolean;
|
|
135
141
|
spec: string;
|
|
136
142
|
}[];
|
|
137
143
|
spec?: string;
|
|
@@ -146,6 +152,7 @@ const sourcesOf = (block: Block): Block["sources"] => {
|
|
|
146
152
|
includeInLlms: true,
|
|
147
153
|
includeInSearch: true,
|
|
148
154
|
noindex: false,
|
|
155
|
+
seoDescriptionSuffix: true,
|
|
149
156
|
spec: block.spec,
|
|
150
157
|
});
|
|
151
158
|
}
|
|
@@ -192,6 +199,7 @@ const referencesFor = (
|
|
|
192
199
|
renderer,
|
|
193
200
|
route,
|
|
194
201
|
scalar: block.scalar,
|
|
202
|
+
seoDescriptionSuffix: source.seoDescriptionSuffix,
|
|
195
203
|
slug: routeSlug(route),
|
|
196
204
|
spec: source.spec,
|
|
197
205
|
theme: block.theme,
|
|
@@ -174,12 +174,23 @@ const GRAPHQL_MEMBER_PHRASES = {
|
|
|
174
174
|
/**
|
|
175
175
|
* The spec's own prose for the operation, followed by the endpoint it documents
|
|
176
176
|
* — so every operation page carries a distinct, self-describing meta
|
|
177
|
-
* description even when the spec's summaries are terse.
|
|
177
|
+
* description even when the spec's summaries are terse. With the suffix
|
|
178
|
+
* switched off (`seoDescriptionSuffix: false`, for sites whose prose isn't
|
|
179
|
+
* English) the description is the prose alone, or the page `title` — a
|
|
180
|
+
* language-neutral `GET /pets`, channel, or field name — when the operation
|
|
181
|
+
* has no prose at all, so no page ships an empty description.
|
|
178
182
|
*/
|
|
179
183
|
const operationDescription = (
|
|
180
184
|
spec: ApiSpecData,
|
|
181
|
-
operation: ApiOperationRef
|
|
185
|
+
operation: ApiOperationRef,
|
|
186
|
+
options: { suffix: boolean; title: string }
|
|
182
187
|
): string => {
|
|
188
|
+
if (!options.suffix) {
|
|
189
|
+
return clip(
|
|
190
|
+
plainProse(operation.description || operation.summary) || options.title,
|
|
191
|
+
META_DESCRIPTION_MAX
|
|
192
|
+
);
|
|
193
|
+
}
|
|
183
194
|
// AsyncAPI operations act on a channel, not an HTTP endpoint; GraphQL pages
|
|
184
195
|
// document a root field or a named type.
|
|
185
196
|
let suffix: string;
|
|
@@ -210,7 +221,7 @@ export const operationMdx = (
|
|
|
210
221
|
operation: ApiOperationRef,
|
|
211
222
|
reference?: Pick<
|
|
212
223
|
ReferenceSource,
|
|
213
|
-
"includeInLlms" | "includeInSearch" | "noindex"
|
|
224
|
+
"includeInLlms" | "includeInSearch" | "noindex" | "seoDescriptionSuffix"
|
|
214
225
|
>
|
|
215
226
|
): RenderedPage => {
|
|
216
227
|
const method = operation.method.toUpperCase();
|
|
@@ -242,7 +253,10 @@ export const operationMdx = (
|
|
|
242
253
|
searchFlags.exclude = true;
|
|
243
254
|
}
|
|
244
255
|
const seo: RenderedPageData["seo"] = {
|
|
245
|
-
description: operationDescription(spec, operation
|
|
256
|
+
description: operationDescription(spec, operation, {
|
|
257
|
+
suffix: reference?.seoDescriptionSuffix !== false,
|
|
258
|
+
title,
|
|
259
|
+
}),
|
|
246
260
|
};
|
|
247
261
|
if (reference?.noindex) {
|
|
248
262
|
seo.noindex = true;
|
package/src/openapi/scalar.ts
CHANGED
|
@@ -144,9 +144,6 @@ export const buildReferenceFiles = async (options: {
|
|
|
144
144
|
warnings.push(spec.warning);
|
|
145
145
|
}
|
|
146
146
|
const pagePath = referencePagePath(ref.route);
|
|
147
|
-
// Relative path from the page back to src/generated/data.json: a page one
|
|
148
|
-
// directory deep (api/events.astro) needs an extra "../".
|
|
149
|
-
const depth = pagePath.split("/").length - 1;
|
|
150
147
|
files.push({
|
|
151
148
|
content: scalarReferenceTemplate({
|
|
152
149
|
configuration: {
|
|
@@ -157,7 +154,6 @@ export const buildReferenceFiles = async (options: {
|
|
|
157
154
|
// hideTestRequestButton, orderSchemaPropertiesBy, and the rest).
|
|
158
155
|
...ref.scalar,
|
|
159
156
|
},
|
|
160
|
-
dataImport: `${"../".repeat(depth + 1)}generated/data.json`,
|
|
161
157
|
noindex: ref.noindex,
|
|
162
158
|
route: ref.route,
|
|
163
159
|
title: ref.label,
|
package/src/registry/eject.ts
CHANGED
|
@@ -9,7 +9,12 @@ import { buildRawMarkdown } from "../ai/markdown.ts";
|
|
|
9
9
|
import { buildMcpData } from "../ai/mcp/data.ts";
|
|
10
10
|
import { buildMcpDiscovery, buildMcpServerCard } from "../ai/mcp/discovery.ts";
|
|
11
11
|
import { planComponentSlots } from "../astro/component-slots.ts";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
EXAMPLE_SCAN_GLOB,
|
|
14
|
+
discoverExamples,
|
|
15
|
+
exampleMarkdownLookup,
|
|
16
|
+
exampleScanRoots,
|
|
17
|
+
} from "../astro/examples.ts";
|
|
13
18
|
import {
|
|
14
19
|
buildRuntimeData,
|
|
15
20
|
collectStaged,
|
|
@@ -61,6 +66,7 @@ import {
|
|
|
61
66
|
tailwindEntryTemplate,
|
|
62
67
|
} from "../theme/entry.ts";
|
|
63
68
|
import { buildThemeCss } from "../theme/palette.ts";
|
|
69
|
+
import { rebaseSourceDirectives } from "../theme/sources.ts";
|
|
64
70
|
import { twoslashCss } from "../theme/twoslash.ts";
|
|
65
71
|
|
|
66
72
|
const toPosix = (path: string): string => path.split("\\").join("/");
|
|
@@ -203,7 +209,7 @@ const mcpFiles = async (
|
|
|
203
209
|
path: join(genDir, "mcp-data.json"),
|
|
204
210
|
},
|
|
205
211
|
{
|
|
206
|
-
content: mcpEndpointTemplate(
|
|
212
|
+
content: mcpEndpointTemplate(),
|
|
207
213
|
path: join(srcDir, "pages", mcpPageFile(route)),
|
|
208
214
|
},
|
|
209
215
|
{
|
|
@@ -251,13 +257,21 @@ const changelogFiles = (
|
|
|
251
257
|
};
|
|
252
258
|
|
|
253
259
|
/** Contents of the configured `examples.css`, or `""` when unset/absent. */
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
260
|
+
/**
|
|
261
|
+
* Read a user stylesheet that eject inlines into a generated entry under
|
|
262
|
+
* `genDir`, re-rooting its relative `@source` paths from the user's file.
|
|
263
|
+
* Resolves to an empty string when the file is unset or absent.
|
|
264
|
+
*/
|
|
265
|
+
const readUserCss = async (
|
|
266
|
+
file: string | null,
|
|
267
|
+
genDir: string
|
|
268
|
+
): Promise<string> => {
|
|
269
|
+
if (!(file && existsSync(file))) {
|
|
270
|
+
return "";
|
|
271
|
+
}
|
|
272
|
+
const css = await readFile(file, "utf-8");
|
|
273
|
+
return rebaseSourceDirectives(css, { from: file, to: genDir });
|
|
274
|
+
};
|
|
261
275
|
|
|
262
276
|
/**
|
|
263
277
|
* The per-example preview route `<Component />` iframes embed, nested under
|
|
@@ -331,10 +345,11 @@ export const eject = async (
|
|
|
331
345
|
context.pagesRoot ? discoverPages(context.pagesRoot) : Promise.resolve([]),
|
|
332
346
|
detectNeedsReact(root),
|
|
333
347
|
detectUsesMath(root),
|
|
334
|
-
context.themeFile
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
348
|
+
readUserCss(context.themeFile, genDir),
|
|
349
|
+
readUserCss(
|
|
350
|
+
config.examples.css ? join(root, config.examples.css) : null,
|
|
351
|
+
genDir
|
|
352
|
+
),
|
|
338
353
|
buildRawMarkdown(project),
|
|
339
354
|
discoverIslands(root),
|
|
340
355
|
]);
|
|
@@ -387,9 +402,11 @@ export const eject = async (
|
|
|
387
402
|
contentRoot: relContext.contentRoot,
|
|
388
403
|
contentRoutes: project.manifest.routes.map((route) => route.path),
|
|
389
404
|
context: relContext,
|
|
390
|
-
dataPath: "./src/generated/data.json",
|
|
391
405
|
examplesPath: "./src/generated/examples.ts",
|
|
392
406
|
examplesThemePath: "./src/generated/examples.css",
|
|
407
|
+
// No CLI publishes the runtime data modules in memory after eject, so
|
|
408
|
+
// the config aliases each to the JSON snapshot written below.
|
|
409
|
+
generatedModulesDir: "./src/generated",
|
|
393
410
|
integrationBridge: ejectIntegrationBridge(
|
|
394
411
|
config,
|
|
395
412
|
root,
|
|
@@ -398,7 +415,6 @@ export const eject = async (
|
|
|
398
415
|
needsReact,
|
|
399
416
|
needsSvelte,
|
|
400
417
|
needsVue,
|
|
401
|
-
openapiPath: "./src/generated/openapi.json",
|
|
402
418
|
pages: relPages,
|
|
403
419
|
searchClientPath: "./src/generated/search-client.ts",
|
|
404
420
|
themePath: "./src/generated/app.css",
|
|
@@ -452,7 +468,9 @@ export const eject = async (
|
|
|
452
468
|
// Relative sources keep the ejected app portable.
|
|
453
469
|
content: examplesEntryTemplate({
|
|
454
470
|
configTokens: buildThemeCss(config.theme),
|
|
455
|
-
sources:
|
|
471
|
+
sources: exampleScanRoots(root, examples.dir).map(
|
|
472
|
+
(dir) => `${relative(genDir, dir)}/${EXAMPLE_SCAN_GLOB}`
|
|
473
|
+
),
|
|
456
474
|
userCss: userExamplesCss,
|
|
457
475
|
}),
|
|
458
476
|
path: join(genDir, "examples.css"),
|
|
@@ -513,7 +531,8 @@ export const eject = async (
|
|
|
513
531
|
if (config.seo.og.enabled) {
|
|
514
532
|
files.push({
|
|
515
533
|
content: ogEndpointTemplate(
|
|
516
|
-
customOgRoutes(pages, config.title, config.seo.og.titles)
|
|
534
|
+
customOgRoutes(pages, config.title, config.seo.og.titles),
|
|
535
|
+
{ pageDescriptions: config.seo.og.description !== false }
|
|
517
536
|
),
|
|
518
537
|
path: join(srcDir, "pages", "og", "[...slug].png.ts"),
|
|
519
538
|
});
|
package/src/theme/entry.ts
CHANGED
|
@@ -835,7 +835,7 @@ ${options.userTheme}
|
|
|
835
835
|
interface ExamplesEntryOptions {
|
|
836
836
|
/** Config-derived token overrides (`:root { --blume-accent: ... }`). */
|
|
837
837
|
configTokens: string;
|
|
838
|
-
/** Globs to scan for utility classes (
|
|
838
|
+
/** Globs to scan for utility classes (the project and examples directory). */
|
|
839
839
|
sources: string[];
|
|
840
840
|
/** Raw contents of the configured `examples.css`, if any. */
|
|
841
841
|
userCss: string;
|
|
@@ -855,7 +855,7 @@ export const examplesEntryTemplate = (options: ExamplesEntryOptions): string =>
|
|
|
855
855
|
`/* Generated by Blume. Do not edit. */
|
|
856
856
|
@import "tailwindcss";
|
|
857
857
|
|
|
858
|
-
/* Scan the
|
|
858
|
+
/* Scan the project (and an out-of-root examples directory) for utility classes. */
|
|
859
859
|
${options.sources.map((source) => `@source "${source}";`).join("\n")}
|
|
860
860
|
|
|
861
861
|
${DARK_VARIANT}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { dirname, isAbsolute, relative, resolve } from "pathe";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A Tailwind `@source "…"` / `@source not "…"` directive with a quoted path.
|
|
5
|
+
* `@source inline("…")` never matches: the quote must directly follow the
|
|
6
|
+
* keyword (or `not`), and `inline(` sits in between.
|
|
7
|
+
*/
|
|
8
|
+
const SOURCE_DIRECTIVE =
|
|
9
|
+
/@source(?<not>\s+not)?\s+(?<quote>["'])(?<path>[^"']+)\k<quote>/gu;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Rewrite the relative `@source` paths in a user stylesheet so they still
|
|
13
|
+
* point where the author meant once the sheet is inlined into a generated
|
|
14
|
+
* Tailwind entry. Tailwind resolves `@source` relative to the stylesheet that
|
|
15
|
+
* declares it, and Blume splices `theme.css` / `examples.css` verbatim into
|
|
16
|
+
* `.blume/src/generated/*.css` (or `src/generated/*.css` after eject), which
|
|
17
|
+
* would silently re-root them there. Resolving each path against the user's
|
|
18
|
+
* file and re-expressing it relative to the generated directory keeps the
|
|
19
|
+
* standard contract — write `@source "../../packages/ui"` next to the file
|
|
20
|
+
* that says it — and lets a monorepo scan sibling workspace packages for
|
|
21
|
+
* utility classes without any knowledge of Blume's internal layout. Absolute
|
|
22
|
+
* paths pass through untouched.
|
|
23
|
+
*/
|
|
24
|
+
export const rebaseSourceDirectives = (
|
|
25
|
+
css: string,
|
|
26
|
+
options: {
|
|
27
|
+
/** The user's stylesheet the CSS was read from. */
|
|
28
|
+
from: string;
|
|
29
|
+
/** The directory of the generated entry the CSS is inlined into. */
|
|
30
|
+
to: string;
|
|
31
|
+
}
|
|
32
|
+
): string => {
|
|
33
|
+
const base = dirname(options.from);
|
|
34
|
+
return css.replace(
|
|
35
|
+
SOURCE_DIRECTIVE,
|
|
36
|
+
(
|
|
37
|
+
directive: string,
|
|
38
|
+
not: string | undefined,
|
|
39
|
+
quote: string,
|
|
40
|
+
path: string
|
|
41
|
+
): string => {
|
|
42
|
+
if (isAbsolute(path)) {
|
|
43
|
+
return directive;
|
|
44
|
+
}
|
|
45
|
+
const rebased = relative(options.to, resolve(base, path));
|
|
46
|
+
return `@source${not ?? ""} ${quote}${rebased}${quote}`;
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
};
|