blume 1.6.5 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/bin/blume.mjs +3 -2
- package/dist/cli/chunk-0qhq7b8q.js +111 -0
- package/dist/cli/chunk-0qhq7b8q.js.map +11 -0
- package/dist/cli/chunk-18tjv4f7.js +96 -0
- package/dist/cli/chunk-18tjv4f7.js.map +10 -0
- package/dist/cli/chunk-27gtm2ym.js +69 -0
- package/dist/cli/chunk-27gtm2ym.js.map +11 -0
- package/dist/cli/chunk-2aj8ddew.js +72 -0
- package/dist/cli/chunk-2aj8ddew.js.map +10 -0
- package/dist/cli/chunk-3r94j3tc.js +221 -0
- package/dist/cli/chunk-3r94j3tc.js.map +10 -0
- package/dist/cli/chunk-4trphnvy.js +102 -0
- package/dist/cli/chunk-4trphnvy.js.map +11 -0
- package/dist/cli/chunk-4xyggvgf.js +21 -0
- package/dist/cli/chunk-4xyggvgf.js.map +10 -0
- package/dist/cli/chunk-5d4q7121.js +4064 -0
- package/dist/cli/chunk-5d4q7121.js.map +40 -0
- package/dist/cli/chunk-5hs6gb7n.js +32 -0
- package/dist/cli/chunk-5hs6gb7n.js.map +10 -0
- package/dist/cli/chunk-6kzzpsx8.js +26 -0
- package/dist/cli/chunk-6kzzpsx8.js.map +10 -0
- package/dist/cli/chunk-8gnpdsn1.js +952 -0
- package/dist/cli/chunk-8gnpdsn1.js.map +12 -0
- package/dist/cli/chunk-9qs6acpw.js +176 -0
- package/dist/cli/chunk-9qs6acpw.js.map +10 -0
- package/dist/cli/chunk-agy5rzxy.js +2453 -0
- package/dist/cli/chunk-agy5rzxy.js.map +15 -0
- package/dist/cli/chunk-bcy492zc.js +16 -0
- package/dist/cli/chunk-bcy492zc.js.map +10 -0
- package/dist/cli/chunk-btfr9yvw.js +41 -0
- package/dist/cli/chunk-btfr9yvw.js.map +10 -0
- package/dist/cli/chunk-cbjnx4s8.js +73 -0
- package/dist/cli/chunk-cbjnx4s8.js.map +10 -0
- package/dist/cli/chunk-cfw6x4rm.js +1967 -0
- package/dist/cli/chunk-cfw6x4rm.js.map +34 -0
- package/dist/cli/chunk-ckh3a410.js +277 -0
- package/dist/cli/chunk-ckh3a410.js.map +11 -0
- package/dist/cli/chunk-drke6t0h.js +259 -0
- package/dist/cli/chunk-drke6t0h.js.map +11 -0
- package/dist/cli/chunk-ev67ycx0.js +15 -0
- package/dist/cli/chunk-ev67ycx0.js.map +10 -0
- package/dist/cli/chunk-ey89bjj1.js +209 -0
- package/dist/cli/chunk-ey89bjj1.js.map +11 -0
- package/dist/cli/chunk-j6pxe0dt.js +69 -0
- package/dist/cli/chunk-j6pxe0dt.js.map +11 -0
- package/dist/cli/chunk-jk1zwka1.js +387 -0
- package/dist/cli/chunk-jk1zwka1.js.map +12 -0
- package/dist/cli/chunk-jtb45atp.js +467 -0
- package/dist/cli/chunk-jtb45atp.js.map +14 -0
- package/dist/cli/chunk-jxkxjsc1.js +76 -0
- package/dist/cli/chunk-jxkxjsc1.js.map +10 -0
- package/dist/cli/chunk-kwx90v78.js +81 -0
- package/dist/cli/chunk-kwx90v78.js.map +10 -0
- package/dist/cli/chunk-n0nyat6g.js +30 -0
- package/dist/cli/chunk-n0nyat6g.js.map +10 -0
- package/dist/cli/chunk-pxj10x8y.js +35 -0
- package/dist/cli/chunk-pxj10x8y.js.map +10 -0
- package/dist/cli/chunk-qq9nm3qd.js +1141 -0
- package/dist/cli/chunk-qq9nm3qd.js.map +19 -0
- package/dist/cli/chunk-s102bysw.js +5170 -0
- package/dist/cli/chunk-s102bysw.js.map +47 -0
- package/dist/cli/chunk-s5dsk8bj.js +769 -0
- package/dist/cli/chunk-s5dsk8bj.js.map +13 -0
- package/dist/cli/chunk-s5e5jt53.js +227 -0
- package/dist/cli/chunk-s5e5jt53.js.map +11 -0
- package/dist/cli/chunk-sbdqrjbb.js +81 -0
- package/dist/cli/chunk-sbdqrjbb.js.map +10 -0
- package/dist/cli/chunk-tnskyrej.js +117 -0
- package/dist/cli/chunk-tnskyrej.js.map +10 -0
- package/dist/cli/chunk-v2ymm99c.js +1016 -0
- package/dist/cli/chunk-v2ymm99c.js.map +13 -0
- package/dist/cli/chunk-v5mm027v.js +185 -0
- package/dist/cli/chunk-v5mm027v.js.map +11 -0
- package/dist/cli/chunk-vt8fgygt.js +23 -0
- package/dist/cli/chunk-vt8fgygt.js.map +10 -0
- package/dist/cli/chunk-vxv4x1n8.js +17 -0
- package/dist/cli/chunk-vxv4x1n8.js.map +10 -0
- package/dist/cli/chunk-wd27zjcz.js +60 -0
- package/dist/cli/chunk-wd27zjcz.js.map +10 -0
- package/dist/cli/chunk-x66c5yjn.js +23 -0
- package/dist/cli/chunk-x66c5yjn.js.map +10 -0
- package/dist/cli/chunk-xv91q4nm.js +5314 -0
- package/dist/cli/chunk-xv91q4nm.js.map +58 -0
- package/dist/cli/chunk-y3g15rvv.js +679 -0
- package/dist/cli/chunk-y3g15rvv.js.map +15 -0
- package/dist/cli/chunk-ye9zdkgv.js +136 -0
- package/dist/cli/chunk-ye9zdkgv.js.map +10 -0
- package/dist/cli/chunk-ynacq3ev.js +1062 -0
- package/dist/cli/chunk-ynacq3ev.js.map +25 -0
- package/dist/cli/chunk-zr3ygrq3.js +54 -0
- package/dist/cli/chunk-zr3ygrq3.js.map +10 -0
- package/dist/cli/index.js +55 -27597
- package/dist/cli/index.js.map +5 -243
- package/dist/types/ai/ask-context.d.ts +26 -0
- package/dist/types/components/layout/nav-utils.d.ts +33 -1
- package/dist/types/core/code-fences.d.ts +11 -0
- package/dist/types/core/package-root.d.ts +1 -1
- package/dist/types/core/schema.d.ts +70 -0
- package/dist/types/theme/fonts.d.ts +22 -22
- package/docs/02-deployment.mdx +22 -1
- package/docs/configuration/ask-ai.mdx +1 -1
- package/docs/configuration/customization.mdx +2 -9
- package/docs/content/navigation.mdx +2 -0
- package/docs/content/syntax.mdx +1 -1
- package/docs/discoverability/open-graph.mdx +4 -0
- package/docs/reference/cli.mdx +1 -1
- package/package.json +4 -2
- package/src/ai/api/handlers.ts +4 -7
- package/src/ai/api/paths.ts +8 -0
- package/src/ai/api/spec.ts +2 -1
- package/src/ai/ask-context.ts +378 -22
- package/src/astro/generate.ts +161 -28
- package/src/astro/include-hmr.ts +10 -13
- package/src/astro/include-refresh.ts +0 -0
- package/src/astro/index.ts +6 -1
- package/src/astro/integration.ts +280 -53
- package/src/astro/module-types.ts +83 -0
- package/src/astro/templates.ts +256 -108
- package/src/audit/image-size.ts +10 -8
- package/src/cli/command-meta.ts +77 -0
- package/src/cli/commands/add.ts +2 -4
- package/src/cli/commands/audit.ts +2 -4
- package/src/cli/commands/build.ts +70 -346
- package/src/cli/commands/check.ts +2 -4
- package/src/cli/commands/dev.ts +31 -42
- package/src/cli/commands/doctor.ts +2 -4
- package/src/cli/commands/eject.ts +3 -41
- package/src/cli/commands/eval.ts +2 -5
- package/src/cli/commands/init.ts +2 -4
- package/src/cli/commands/mcp-stdio.ts +2 -5
- package/src/cli/commands/preview.ts +3 -5
- package/src/cli/commands/sync.ts +2 -4
- package/src/cli/commands/translate.ts +2 -5
- package/src/cli/commands/validate.ts +2 -4
- package/src/cli/commands/version.ts +2 -4
- package/src/cli/eject-scripts.ts +0 -45
- package/src/cli/host-args.ts +16 -0
- package/src/cli/index.ts +84 -35
- package/src/cli/lazy-command.ts +47 -0
- package/src/components/Icon.astro +24 -0
- package/src/components/content/GithubInfo.astro +4 -1
- package/src/components/icon-sprite-middleware.ts +41 -0
- package/src/components/icon-sprite.ts +93 -0
- package/src/components/layout/IconSprite.astro +11 -0
- package/src/components/layout/NavTree.astro +156 -188
- package/src/components/layout/NavTreeCache.astro +45 -0
- package/src/components/layout/NavTreeScript.astro +256 -0
- package/src/components/layout/PageActions.astro +11 -5
- package/src/components/layout/PageLayout.astro +21 -3
- package/src/components/layout/ReferenceLayout.astro +21 -4
- package/src/components/layout/RootLayout.astro +44 -6
- package/src/components/layout/nav-cache.ts +49 -0
- package/src/components/layout/nav-utils.ts +69 -1
- package/src/components/layout/page-locale.ts +29 -0
- package/src/core/api-name.ts +18 -0
- package/src/core/code-fences.ts +48 -0
- package/src/core/content-assets.ts +3 -7
- package/src/core/includes.ts +3 -7
- package/src/core/package-root.ts +1 -1
- package/src/core/schema.ts +19 -0
- package/src/core/sources/normalize.ts +2 -37
- package/src/core/sources/obsidian.ts +3 -2
- package/src/core/svg-dimensions.ts +97 -0
- package/src/core/version-cut.ts +2 -2
- package/src/deploy/artifacts.ts +370 -0
- package/src/deploy/cloudflare-negotiation.ts +97 -32
- package/src/deploy/function-bundle.ts +66 -20
- package/src/deploy/sitemap.ts +6 -0
- package/src/deploy/vercel-negotiation.ts +8 -30
- package/src/markdown/language-icon.ts +64 -20
- package/src/markdown/mermaid.ts +11 -0
- package/src/og/cache.ts +236 -0
- package/src/og/card.ts +18 -16
- package/src/og/index.ts +8 -1
- package/src/openapi/render-mdx.ts +9 -5
- package/src/registry/eject.ts +23 -10
- package/src/theme/entry.ts +41 -7
- package/src/theme/fonts.ts +30 -23
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { CommandMeta } from "citty";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Every command's `meta`, held apart from the command modules themselves.
|
|
5
|
+
*
|
|
6
|
+
* The CLI entry loads each command lazily (see `lazy-command.ts`), but citty
|
|
7
|
+
* still reads every subcommand's `meta` to render `blume --help` and to match
|
|
8
|
+
* an unknown name against aliases. Keeping that table here lets those paths
|
|
9
|
+
* run without importing a single command module — `dev` alone drags in Astro,
|
|
10
|
+
* `mcp-stdio` the MCP SDK. The command modules read their `meta` from this
|
|
11
|
+
* table too, so the entry and the command can't drift.
|
|
12
|
+
*/
|
|
13
|
+
export const commandMeta = {
|
|
14
|
+
add: {
|
|
15
|
+
description: "Install a source component or template from the registry.",
|
|
16
|
+
name: "add",
|
|
17
|
+
},
|
|
18
|
+
audit: {
|
|
19
|
+
description: "Audit the built site for SEO and site-health issues.",
|
|
20
|
+
name: "audit",
|
|
21
|
+
},
|
|
22
|
+
build: {
|
|
23
|
+
description: "Build the docs site for production.",
|
|
24
|
+
name: "build",
|
|
25
|
+
},
|
|
26
|
+
check: {
|
|
27
|
+
description: "Type-check the docs site with astro check.",
|
|
28
|
+
name: "check",
|
|
29
|
+
},
|
|
30
|
+
dev: {
|
|
31
|
+
description: "Start the Blume development server.",
|
|
32
|
+
name: "dev",
|
|
33
|
+
},
|
|
34
|
+
doctor: {
|
|
35
|
+
description: "Diagnose common configuration and content problems.",
|
|
36
|
+
name: "doctor",
|
|
37
|
+
},
|
|
38
|
+
eject: {
|
|
39
|
+
description: "Promote the generated runtime into an owned Astro project.",
|
|
40
|
+
name: "eject",
|
|
41
|
+
},
|
|
42
|
+
eval: {
|
|
43
|
+
description:
|
|
44
|
+
"Test the docs: an agent answers your questions using only the documentation.",
|
|
45
|
+
name: "eval",
|
|
46
|
+
},
|
|
47
|
+
init: {
|
|
48
|
+
description: "Scaffold a minimal Blume project.",
|
|
49
|
+
name: "init",
|
|
50
|
+
},
|
|
51
|
+
"mcp-stdio": {
|
|
52
|
+
description:
|
|
53
|
+
"Serve an MCP data snapshot over stdio (internal, used by `blume eval`).",
|
|
54
|
+
name: "mcp-stdio",
|
|
55
|
+
},
|
|
56
|
+
preview: {
|
|
57
|
+
description: "Preview the last production build.",
|
|
58
|
+
name: "preview",
|
|
59
|
+
},
|
|
60
|
+
sync: {
|
|
61
|
+
description: "Re-fetch remote content sources and regenerate the runtime.",
|
|
62
|
+
name: "sync",
|
|
63
|
+
},
|
|
64
|
+
translate: {
|
|
65
|
+
description:
|
|
66
|
+
"Translate docs into the configured locales with a local agent CLI.",
|
|
67
|
+
name: "translate",
|
|
68
|
+
},
|
|
69
|
+
validate: {
|
|
70
|
+
description: "Validate internal, anchor, asset, and external links.",
|
|
71
|
+
name: "validate",
|
|
72
|
+
},
|
|
73
|
+
version: {
|
|
74
|
+
description: "Freeze the current docs as an archived version.",
|
|
75
|
+
name: "version",
|
|
76
|
+
},
|
|
77
|
+
} satisfies Record<string, CommandMeta>;
|
package/src/cli/commands/add.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { dirname, join } from "pathe";
|
|
|
6
6
|
|
|
7
7
|
import { findItem, packageSrc, registry } from "../../registry/registry.ts";
|
|
8
8
|
import { rewriteImports } from "../../registry/rewrite-imports.ts";
|
|
9
|
+
import { commandMeta } from "../command-meta.ts";
|
|
9
10
|
import { logger } from "../log.ts";
|
|
10
11
|
|
|
11
12
|
export const addCommand = defineCommand({
|
|
@@ -17,10 +18,7 @@ export const addCommand = defineCommand({
|
|
|
17
18
|
type: "positional",
|
|
18
19
|
},
|
|
19
20
|
},
|
|
20
|
-
meta:
|
|
21
|
-
description: "Install a source component or template from the registry.",
|
|
22
|
-
name: "add",
|
|
23
|
-
},
|
|
21
|
+
meta: commandMeta.add,
|
|
24
22
|
async run({ args }) {
|
|
25
23
|
const root = process.cwd();
|
|
26
24
|
|
|
@@ -13,6 +13,7 @@ import type { AuditResult } from "../../audit/run.ts";
|
|
|
13
13
|
import { BlumeError } from "../../core/diagnostics.ts";
|
|
14
14
|
import { scanProject } from "../../core/project-graph.ts";
|
|
15
15
|
import type { DiagnosticSeverity } from "../../core/types.ts";
|
|
16
|
+
import { commandMeta } from "../command-meta.ts";
|
|
16
17
|
import { reportInternalError } from "../internal-error.ts";
|
|
17
18
|
import { flushStdout, logger } from "../log.ts";
|
|
18
19
|
|
|
@@ -115,10 +116,7 @@ export const auditCommand = defineCommand({
|
|
|
115
116
|
type: "boolean",
|
|
116
117
|
},
|
|
117
118
|
},
|
|
118
|
-
meta:
|
|
119
|
-
description: "Audit the built site for SEO and site-health issues.",
|
|
120
|
-
name: "audit",
|
|
121
|
-
},
|
|
119
|
+
meta: commandMeta.audit,
|
|
122
120
|
async run({ args }) {
|
|
123
121
|
if (args["list-checks"]) {
|
|
124
122
|
process.stdout.write(formatCatalog());
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
|
|
4
4
|
import { build } from "astro";
|
|
5
5
|
import { defineCommand } from "citty";
|
|
6
|
-
import {
|
|
6
|
+
import { join } from "pathe";
|
|
7
7
|
|
|
8
|
-
import { buildAgentReadability } from "../../ai/agent-readability.ts";
|
|
9
8
|
import {
|
|
10
9
|
API_CATALOG_PATH,
|
|
11
10
|
API_CATALOG_TYPE,
|
|
12
|
-
buildApiCatalog,
|
|
13
11
|
hasApiCatalog,
|
|
14
12
|
} from "../../ai/api-catalog.ts";
|
|
13
|
+
import { pageJsonPath } from "../../ai/api/paths.ts";
|
|
15
14
|
import { buildHomeLinkHeader } from "../../ai/link-headers.ts";
|
|
16
|
-
import { buildLlmsFiles } from "../../ai/llms.ts";
|
|
17
15
|
import {
|
|
18
16
|
agentMarkdown,
|
|
19
17
|
buildRawMarkdown,
|
|
@@ -21,16 +19,10 @@ import {
|
|
|
21
19
|
markdownTokenCount,
|
|
22
20
|
} from "../../ai/markdown.ts";
|
|
23
21
|
import {
|
|
24
|
-
AGENT_SKILLS_DIR,
|
|
25
|
-
buildSkillsIndex,
|
|
26
|
-
collectSkills,
|
|
27
|
-
} from "../../ai/skills.ts";
|
|
28
|
-
import type { SkillArtifact } from "../../ai/skills.ts";
|
|
29
|
-
import {
|
|
30
|
-
buildSignaturesDirectory,
|
|
31
22
|
SIGNATURES_DIRECTORY_PATH,
|
|
32
23
|
SIGNATURES_DIRECTORY_TYPE,
|
|
33
24
|
} from "../../ai/web-bot-auth.ts";
|
|
25
|
+
import { publishBuildProject } from "../../astro/integration.ts";
|
|
34
26
|
import { ensureGitignore } from "../../core/gitignore.ts";
|
|
35
27
|
import type { BlumeProject } from "../../core/project-graph.ts";
|
|
36
28
|
import type { ResolvedConfig } from "../../core/schema.ts";
|
|
@@ -39,7 +31,6 @@ import type { ProjectContext } from "../../core/types.ts";
|
|
|
39
31
|
import {
|
|
40
32
|
ADAPTER_IGNORE_DIRS,
|
|
41
33
|
deployStaticDir,
|
|
42
|
-
readsHeaderFiles,
|
|
43
34
|
servesClientSubdir,
|
|
44
35
|
surfaceAdapterOutput,
|
|
45
36
|
} from "../../deploy/adapter-output.ts";
|
|
@@ -52,18 +43,10 @@ import {
|
|
|
52
43
|
blumeDependencyNames,
|
|
53
44
|
functionBundleVerdict,
|
|
54
45
|
} from "../../deploy/function-bundle.ts";
|
|
55
|
-
import {
|
|
56
|
-
import {
|
|
57
|
-
buildNetlifyRedirects,
|
|
58
|
-
buildRedirectManifest,
|
|
59
|
-
buildVercelConfig,
|
|
60
|
-
platformRedirects,
|
|
61
|
-
} from "../../deploy/redirects.ts";
|
|
62
|
-
import { buildRobots } from "../../deploy/robots.ts";
|
|
63
|
-
import { buildSitemapFiles } from "../../deploy/sitemap.ts";
|
|
46
|
+
import { platformRedirects } from "../../deploy/redirects.ts";
|
|
64
47
|
import { injectNegotiationRoutes } from "../../deploy/vercel-negotiation.ts";
|
|
65
|
-
import {
|
|
66
|
-
import {
|
|
48
|
+
import { cardCacheTally, ogCacheDir, pruneCardCache } from "../../og/cache.ts";
|
|
49
|
+
import { commandMeta } from "../command-meta.ts";
|
|
67
50
|
import { refuseIfDevRunning } from "../dev-lock.ts";
|
|
68
51
|
import { logger } from "../log.ts";
|
|
69
52
|
import { prepareProject } from "../prepare.ts";
|
|
@@ -102,208 +85,6 @@ const validateBudgetFlags = (args: BudgetArgs): void => {
|
|
|
102
85
|
}
|
|
103
86
|
};
|
|
104
87
|
|
|
105
|
-
/**
|
|
106
|
-
* Emit platform redirect files for a static build (adapters wire redirects
|
|
107
|
-
* natively). Always writes the manifest; writes `_redirects`/`vercel.json` only
|
|
108
|
-
* when the user hasn't shipped one via public/. Note that Vercel's
|
|
109
|
-
* git-integration builds read `vercel.json` from the repository root only —
|
|
110
|
-
* the copy emitted here takes effect when the dist folder itself is deployed
|
|
111
|
-
* directly via the Vercel CLI.
|
|
112
|
-
*/
|
|
113
|
-
const emitRedirectFiles = async (
|
|
114
|
-
config: ResolvedConfig,
|
|
115
|
-
distDir: string
|
|
116
|
-
): Promise<void> => {
|
|
117
|
-
const redirects = platformRedirects(config);
|
|
118
|
-
if (redirects.length === 0 || config.deployment.output !== "static") {
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
await writeFile(
|
|
122
|
-
join(distDir, "blume-redirects.json"),
|
|
123
|
-
buildRedirectManifest(redirects),
|
|
124
|
-
"utf-8"
|
|
125
|
-
);
|
|
126
|
-
const platformFiles = [
|
|
127
|
-
{ content: buildNetlifyRedirects(redirects), name: "_redirects" },
|
|
128
|
-
{ content: buildVercelConfig(redirects), name: "vercel.json" },
|
|
129
|
-
];
|
|
130
|
-
await Promise.all(
|
|
131
|
-
platformFiles.map((file) =>
|
|
132
|
-
existsSync(join(distDir, file.name))
|
|
133
|
-
? Promise.resolve()
|
|
134
|
-
: writeFile(join(distDir, file.name), file.content, "utf-8")
|
|
135
|
-
)
|
|
136
|
-
);
|
|
137
|
-
logger.success(`Emitted redirect files for ${redirects.length} redirect(s)`);
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Emit a `_headers` file so Netlify / Cloudflare serve the raw AI-ready
|
|
142
|
-
* endpoints (`*.md`, `*.mdx`, `*.txt`) with an explicit `charset=utf-8`. Without
|
|
143
|
-
* it those hosts send `text/markdown` / `text/plain` with no charset and
|
|
144
|
-
* browsers fall back to Windows-1252, garbling any non-ASCII docs (#82).
|
|
145
|
-
*
|
|
146
|
-
* The same file carries the rest of the agent-discovery surface that only a
|
|
147
|
-
* response header can express: the homepage `Link` header (RFC 8288, see
|
|
148
|
-
* `ai/link-headers.ts`), and the registered media types for the extensionless
|
|
149
|
-
* well-known files — `application/linkset+json` for the API catalog, the
|
|
150
|
-
* signatures directory, and the Agent Skills archives. A static host serves
|
|
151
|
-
* those as `octet-stream` or nothing at all without a rule.
|
|
152
|
-
*
|
|
153
|
-
* A `_headers` shipped in `public/` wins, exactly like `_redirects` — the opt-out
|
|
154
|
-
* is checked at its source rather than in `dist`, because on Cloudflare the file
|
|
155
|
-
* in `dist` is not necessarily the user's: `@astrojs/cloudflare` writes its own
|
|
156
|
-
* `_headers` (an immutable `Cache-Control` rule for `/_astro/*`) during the
|
|
157
|
-
* build, before this runs. Testing `dist` therefore read an adapter-generated
|
|
158
|
-
* file as a user opt-out and skipped silently. When both exist, the adapter's
|
|
159
|
-
* rules are preserved and ours are appended.
|
|
160
|
-
*
|
|
161
|
-
* Gated on {@link readsHeaderFiles}, not on `output === "static"`. A **Cloudflare
|
|
162
|
-
* server** build serves `dist/client` through the Worker's ASSETS binding, and
|
|
163
|
-
* Workers static assets honor `_headers` from that directory — so the file
|
|
164
|
-
* applies there too, and skipping it left every Cloudflare server build with no
|
|
165
|
-
* `Link` header and no media type on its own discovery files. The charset half
|
|
166
|
-
* of this file *is* redundant on a server build, because the runtime endpoint
|
|
167
|
-
* sets Content-Type on the Response itself; the `Link` and well-known halves are
|
|
168
|
-
* not, and one conclusion about the first was applied to all three.
|
|
169
|
-
*
|
|
170
|
-
* Exported for the test suite, which exercises it in a subprocess like the
|
|
171
|
-
* other command helpers.
|
|
172
|
-
*/
|
|
173
|
-
export const emitHeaderFiles = async (
|
|
174
|
-
project: BlumeProject,
|
|
175
|
-
distDir: string
|
|
176
|
-
): Promise<void> => {
|
|
177
|
-
const { config } = project;
|
|
178
|
-
if (
|
|
179
|
-
!readsHeaderFiles(config.deployment) ||
|
|
180
|
-
existsSync(join(project.context.root, "public", "_headers"))
|
|
181
|
-
) {
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
const ours = buildNetlifyHeaders(
|
|
185
|
-
config,
|
|
186
|
-
buildHomeLinkHeader(config, markdownRoutePaths(project))
|
|
187
|
-
);
|
|
188
|
-
// An adapter may have written its own rules here already (Cloudflare adds an
|
|
189
|
-
// immutable Cache-Control for /_astro/*). Keep them and append ours: both
|
|
190
|
-
// sets are wanted, and `_headers` has no merge semantics beyond order.
|
|
191
|
-
const target = join(distDir, "_headers");
|
|
192
|
-
const existing = existsSync(target) ? await readFile(target, "utf-8") : "";
|
|
193
|
-
await writeFile(
|
|
194
|
-
target,
|
|
195
|
-
existing ? `${existing.trimEnd()}\n${ours}` : ours,
|
|
196
|
-
"utf-8"
|
|
197
|
-
);
|
|
198
|
-
logger.success(
|
|
199
|
-
"Emitted _headers (UTF-8 Content-Type + homepage Link header)"
|
|
200
|
-
);
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Collect the Agent Skills `ai.skills` publishes, once per build, so both the
|
|
205
|
-
* skills surface and llms.txt (which lists them) read the same set. Empty
|
|
206
|
-
* when the feature is off, the directory is missing, nothing in it is
|
|
207
|
-
* publishable (each with a warning), or a user-shipped
|
|
208
|
-
* `public/.well-known/agent-skills/index.json` already owns the surface.
|
|
209
|
-
*/
|
|
210
|
-
const collectConfiguredSkills = async (
|
|
211
|
-
project: BlumeProject,
|
|
212
|
-
distDir: string
|
|
213
|
-
): Promise<SkillArtifact[]> => {
|
|
214
|
-
const configured = project.config.ai.skills;
|
|
215
|
-
if (!configured) {
|
|
216
|
-
return [];
|
|
217
|
-
}
|
|
218
|
-
const dir = resolve(project.context.root, configured);
|
|
219
|
-
if (!existsSync(dir)) {
|
|
220
|
-
logger.warn(
|
|
221
|
-
`ai.skills points at "${configured}" (${dir}), which does not exist; no skills published.`
|
|
222
|
-
);
|
|
223
|
-
return [];
|
|
224
|
-
}
|
|
225
|
-
if (existsSync(join(distDir, AGENT_SKILLS_DIR.slice(1), "index.json"))) {
|
|
226
|
-
return [];
|
|
227
|
-
}
|
|
228
|
-
const { skills, warnings } = await collectSkills(dir);
|
|
229
|
-
for (const warning of warnings) {
|
|
230
|
-
logger.warn(warning);
|
|
231
|
-
}
|
|
232
|
-
if (skills.length === 0) {
|
|
233
|
-
logger.warn(`ai.skills: no publishable skills found in "${configured}".`);
|
|
234
|
-
}
|
|
235
|
-
return skills;
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Publish the collected Agent Skills: copy each skill artifact under
|
|
240
|
-
* `.well-known/agent-skills/` and emit the discovery index. A user-shipped
|
|
241
|
-
* `public/.well-known/agent-skills/index.json` takes over the whole surface
|
|
242
|
-
* (the collector returns nothing then), matching every other generated
|
|
243
|
-
* artifact.
|
|
244
|
-
*/
|
|
245
|
-
const emitAgentSkills = async (
|
|
246
|
-
project: BlumeProject,
|
|
247
|
-
distDir: string,
|
|
248
|
-
skills: readonly SkillArtifact[]
|
|
249
|
-
): Promise<void> => {
|
|
250
|
-
if (skills.length === 0) {
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
const outDir = join(distDir, AGENT_SKILLS_DIR.slice(1));
|
|
254
|
-
await Promise.all(
|
|
255
|
-
skills.map(async (skill) => {
|
|
256
|
-
const target = join(outDir, skill.path);
|
|
257
|
-
await mkdir(dirname(target), { recursive: true });
|
|
258
|
-
await writeFile(target, skill.content);
|
|
259
|
-
})
|
|
260
|
-
);
|
|
261
|
-
await writeFile(
|
|
262
|
-
join(outDir, "index.json"),
|
|
263
|
-
buildSkillsIndex(skills, project.config),
|
|
264
|
-
"utf-8"
|
|
265
|
-
);
|
|
266
|
-
logger.success(
|
|
267
|
-
`Published ${skills.length} agent skill(s) (.well-known/agent-skills/index.json)`
|
|
268
|
-
);
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Emit the generated `.well-known` discovery files — the RFC 9727 API catalog
|
|
273
|
-
* and the Web Bot Auth signature directory — each skipped when the feature is
|
|
274
|
-
* off or when the user ships their own copy via public/ (already in dist by
|
|
275
|
-
* the time this runs).
|
|
276
|
-
*/
|
|
277
|
-
const emitWellKnownFiles = async (
|
|
278
|
-
config: ResolvedConfig,
|
|
279
|
-
distDir: string
|
|
280
|
-
): Promise<void> => {
|
|
281
|
-
const files = [
|
|
282
|
-
{
|
|
283
|
-
content: buildSignaturesDirectory(config),
|
|
284
|
-
label: "Web Bot Auth",
|
|
285
|
-
path: SIGNATURES_DIRECTORY_PATH,
|
|
286
|
-
},
|
|
287
|
-
{
|
|
288
|
-
content: buildApiCatalog(config),
|
|
289
|
-
label: "RFC 9727",
|
|
290
|
-
path: API_CATALOG_PATH,
|
|
291
|
-
},
|
|
292
|
-
];
|
|
293
|
-
for (const file of files) {
|
|
294
|
-
const target = join(distDir, file.path.slice(1));
|
|
295
|
-
if (!file.content || existsSync(target)) {
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
// Sequential by nature: both files share the .well-known dir creation.
|
|
299
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
300
|
-
await mkdir(join(distDir, ".well-known"), { recursive: true });
|
|
301
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
302
|
-
await writeFile(target, file.content, "utf-8");
|
|
303
|
-
logger.success(`Generated ${file.path.slice(1)} (${file.label})`);
|
|
304
|
-
}
|
|
305
|
-
};
|
|
306
|
-
|
|
307
88
|
/**
|
|
308
89
|
* Splice `Accept: text/markdown` negotiation routes into the Vercel adapter's
|
|
309
90
|
* Build Output config, so a content-page request that prefers Markdown gets the
|
|
@@ -436,6 +217,15 @@ const emitCloudflareNegotiation = async (
|
|
|
436
217
|
contentRoutePaths: project.manifest.routes.map((route) => route.path),
|
|
437
218
|
homeLinkHeader: buildHomeLinkHeader(config, routePaths),
|
|
438
219
|
homeTokens: home ? markdownTokenCount(agentMarkdown(home)) : undefined,
|
|
220
|
+
// Exactly the per-page JSON documents the API emits (see `pageParams`):
|
|
221
|
+
// the non-hidden routes with agent Markdown, when the API is on.
|
|
222
|
+
pageJsonPaths: config.ai.api
|
|
223
|
+
? project.manifest.routes
|
|
224
|
+
.filter(
|
|
225
|
+
(route) => !route.hidden && rawMarkdown[route.path] !== undefined
|
|
226
|
+
)
|
|
227
|
+
.map((route) => pageJsonPath(route.path))
|
|
228
|
+
: [],
|
|
439
229
|
// The wrapper Worker matches full served URLs, so the redirects are
|
|
440
230
|
// based the same way the platform files are — it answers any the
|
|
441
231
|
// worker-first rules claim, where `_redirects` is never consulted and
|
|
@@ -459,6 +249,29 @@ const emitCloudflareNegotiation = async (
|
|
|
459
249
|
);
|
|
460
250
|
};
|
|
461
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Report how many OG cards the build read back from the on-disk cache against
|
|
254
|
+
* how many it rendered (the endpoint tallies both), so a warm rebuild shows
|
|
255
|
+
* where the time went. With `prune`, also drop the cached cards this build
|
|
256
|
+
* never asked for — renamed pages, edited descriptions, cards from an earlier
|
|
257
|
+
* Blume version — so a persisted cache holds exactly the current site's cards.
|
|
258
|
+
*/
|
|
259
|
+
const reportCardCache = async (
|
|
260
|
+
project: BlumeProject,
|
|
261
|
+
prune: boolean
|
|
262
|
+
): Promise<void> => {
|
|
263
|
+
const cards = cardCacheTally();
|
|
264
|
+
if (!cards) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
logger.info(
|
|
268
|
+
`OG cards: ${cards.hits} reused from the cache, ${cards.misses} rendered`
|
|
269
|
+
);
|
|
270
|
+
if (prune) {
|
|
271
|
+
await pruneCardCache(ogCacheDir(project.context));
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
462
275
|
const formatBytes = (bytes: number): string => {
|
|
463
276
|
if (bytes < 1024) {
|
|
464
277
|
return `${bytes} B`;
|
|
@@ -616,118 +429,22 @@ export const isolatedStaticDir = (
|
|
|
616
429
|
};
|
|
617
430
|
|
|
618
431
|
/**
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
*
|
|
622
|
-
*
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
project: BlumeProject,
|
|
626
|
-
distDir: string,
|
|
627
|
-
skills: readonly SkillArtifact[]
|
|
628
|
-
): Promise<void> => {
|
|
629
|
-
const indexPath = join(distDir, "llms.txt");
|
|
630
|
-
const fullPath = join(distDir, "llms-full.txt");
|
|
631
|
-
const writeIndex = !existsSync(indexPath);
|
|
632
|
-
const writeFull = !existsSync(fullPath);
|
|
633
|
-
if (!(writeIndex || writeFull)) {
|
|
634
|
-
return;
|
|
635
|
-
}
|
|
636
|
-
const { index, full } = await buildLlmsFiles(project, { skills });
|
|
637
|
-
const writes: Promise<void>[] = [];
|
|
638
|
-
if (writeIndex) {
|
|
639
|
-
writes.push(writeFile(indexPath, index, "utf-8"));
|
|
640
|
-
}
|
|
641
|
-
if (writeFull) {
|
|
642
|
-
writes.push(writeFile(fullPath, full, "utf-8"));
|
|
643
|
-
}
|
|
644
|
-
await Promise.all(writes);
|
|
645
|
-
logger.success(
|
|
646
|
-
`Generated ${[
|
|
647
|
-
writeIndex ? "llms.txt" : null,
|
|
648
|
-
writeFull ? "llms-full.txt" : null,
|
|
649
|
-
]
|
|
650
|
-
.filter(Boolean)
|
|
651
|
-
.join(" and ")}`
|
|
652
|
-
);
|
|
653
|
-
};
|
|
654
|
-
|
|
655
|
-
/**
|
|
656
|
-
* Run every deploy post-step of a real (non-isolated) build: the search index +
|
|
657
|
-
* hosted-provider sync, llms.txt, sitemap/robots, redirect files, the summary
|
|
658
|
-
* box, and the optional bundle report / budget gate. Exits non-zero if a budget
|
|
659
|
-
* is exceeded. Isolated verify builds skip all of this except the bundle
|
|
660
|
-
* report / budget gate, which they run against their own output.
|
|
432
|
+
* Print the build summary box and run the optional bundle report / budget
|
|
433
|
+
* gate against the served static dir. The deploy artifacts themselves
|
|
434
|
+
* (search index, llms.txt, sitemap, robots, redirect and header files, …)
|
|
435
|
+
* were written by the integration's `astro:build:done` hook during
|
|
436
|
+
* `build()` — see `deploy/artifacts.ts`. Exits non-zero if a budget is
|
|
437
|
+
* exceeded.
|
|
661
438
|
*/
|
|
662
|
-
const
|
|
439
|
+
const reportBuild = async (
|
|
663
440
|
project: BlumeProject,
|
|
664
441
|
distDir: string,
|
|
665
442
|
args: { analyze?: boolean } & BudgetArgs
|
|
666
443
|
): Promise<void> => {
|
|
667
|
-
if (project.config.search.provider === "pagefind") {
|
|
668
|
-
logger.start("Building search index");
|
|
669
|
-
const indexed = await buildSearchIndex(distDir);
|
|
670
|
-
logger.success(`Indexed ${indexed} page(s) for search`);
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
// Upload the index to a hosted provider (Algolia, Orama Cloud, Typesense).
|
|
674
|
-
// Skipped with a warning when its admin key isn't configured.
|
|
675
|
-
await syncSearchProvider(project, {
|
|
676
|
-
start: (message) => logger.start(message),
|
|
677
|
-
success: (message) => logger.success(message),
|
|
678
|
-
warn: (message) => logger.warn(message),
|
|
679
|
-
});
|
|
680
|
-
|
|
681
|
-
// Collected once: llms.txt lists the skills the build publishes below.
|
|
682
|
-
const skills = await collectConfiguredSkills(project, distDir);
|
|
683
|
-
if (project.config.ai.llmsTxt.enabled) {
|
|
684
|
-
await publishLlmsFiles(project, distDir, skills);
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
// A user's own public/ file (copied into dist by Astro) always wins.
|
|
688
|
-
const sitemapFiles = buildSitemapFiles(project);
|
|
689
|
-
if (sitemapFiles && !existsSync(join(distDir, "sitemap.xml"))) {
|
|
690
|
-
await Promise.all(
|
|
691
|
-
sitemapFiles.map((file) =>
|
|
692
|
-
writeFile(join(distDir, file.name), file.xml, "utf-8")
|
|
693
|
-
)
|
|
694
|
-
);
|
|
695
|
-
logger.success(
|
|
696
|
-
sitemapFiles.length === 1
|
|
697
|
-
? "Generated sitemap.xml"
|
|
698
|
-
: `Generated sitemap.xml (index of ${sitemapFiles.length - 1} sitemap files)`
|
|
699
|
-
);
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
const robots = buildRobots(project);
|
|
703
|
-
if (robots && !existsSync(join(distDir, "robots.txt"))) {
|
|
704
|
-
await writeFile(join(distDir, "robots.txt"), robots, "utf-8");
|
|
705
|
-
logger.success("Generated robots.txt");
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
const agentReadability = buildAgentReadability(project);
|
|
709
|
-
if (
|
|
710
|
-
agentReadability &&
|
|
711
|
-
!existsSync(join(distDir, "agent-readability.json"))
|
|
712
|
-
) {
|
|
713
|
-
await writeFile(
|
|
714
|
-
join(distDir, "agent-readability.json"),
|
|
715
|
-
`${JSON.stringify(agentReadability, null, 2)}\n`,
|
|
716
|
-
"utf-8"
|
|
717
|
-
);
|
|
718
|
-
logger.success("Generated agent-readability.json");
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
await emitWellKnownFiles(project.config, distDir);
|
|
722
|
-
await emitAgentSkills(project, distDir, skills);
|
|
723
|
-
|
|
724
|
-
await emitRedirectFiles(project.config, distDir);
|
|
725
|
-
await emitHeaderFiles(project, distDir);
|
|
726
|
-
|
|
727
444
|
const { config } = project;
|
|
728
445
|
const features = serverFeatures(config);
|
|
729
|
-
//
|
|
730
|
-
//
|
|
446
|
+
// The sitemap needs both the flag and a `site` (absolute URLs) — only the
|
|
447
|
+
// latter deserves the remediation hint.
|
|
731
448
|
const sitemapNote = config.seo.sitemap
|
|
732
449
|
? "no (set deployment.site)"
|
|
733
450
|
: "no (seo.sitemap is false)";
|
|
@@ -738,9 +455,9 @@ const publishBuildArtifacts = async (
|
|
|
738
455
|
`Site ${config.deployment.site ?? "not set"}`,
|
|
739
456
|
`Search ${config.search.provider}`,
|
|
740
457
|
`Redirects ${config.redirects.length}`,
|
|
741
|
-
`Sitemap ${
|
|
742
|
-
`Robots ${robots ? "yes" : "no"}`,
|
|
743
|
-
`Agent JSON ${agentReadability ? "yes" : "no"}`,
|
|
458
|
+
`Sitemap ${config.deployment.site && config.seo.sitemap ? "yes" : sitemapNote}`,
|
|
459
|
+
`Robots ${config.seo.robots ? "yes" : "no"}`,
|
|
460
|
+
`Agent JSON ${config.seo.agentReadability ? "yes" : "no"}`,
|
|
744
461
|
`LLM files ${config.ai.llmsTxt.enabled ? "yes" : "no"}`,
|
|
745
462
|
`Server features ${features.length > 0 ? features.join(", ") : "none"}`,
|
|
746
463
|
].join("\n")
|
|
@@ -800,10 +517,7 @@ export const buildCommand = defineCommand({
|
|
|
800
517
|
type: "boolean",
|
|
801
518
|
},
|
|
802
519
|
},
|
|
803
|
-
meta:
|
|
804
|
-
description: "Build the docs site for production.",
|
|
805
|
-
name: "build",
|
|
806
|
-
},
|
|
520
|
+
meta: commandMeta.build,
|
|
807
521
|
async run({ args }) {
|
|
808
522
|
const root = process.cwd();
|
|
809
523
|
|
|
@@ -852,18 +566,28 @@ export const buildCommand = defineCommand({
|
|
|
852
566
|
`Building ${project.graph.pages.length} page(s) (${project.config.deployment.output} output)`
|
|
853
567
|
);
|
|
854
568
|
|
|
569
|
+
// Hand the scanned project to the integration: its `astro:build:done`
|
|
570
|
+
// hook writes the deploy artifacts (search index, llms.txt, sitemap, …)
|
|
571
|
+
// into Astro's client output during the build. An isolated build is a
|
|
572
|
+
// throwaway verify that only needs to confirm the site compiles and
|
|
573
|
+
// renders, so it publishes nothing — no network post-steps (a hosted
|
|
574
|
+
// search sync would push), no deploy artifacts.
|
|
575
|
+
if (!runtimeDir) {
|
|
576
|
+
publishBuildProject(project);
|
|
577
|
+
}
|
|
578
|
+
|
|
855
579
|
await build({
|
|
856
580
|
logLevel: "info",
|
|
857
581
|
root: project.context.outDir,
|
|
858
582
|
});
|
|
859
583
|
|
|
860
|
-
//
|
|
861
|
-
//
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
// and budget gate still run
|
|
865
|
-
// 100` exiting 0 without measuring
|
|
866
|
-
// in CI.
|
|
584
|
+
// A real build also prunes the cache; an isolated verify must not evict
|
|
585
|
+
// cards a live dev server is still serving.
|
|
586
|
+
await reportCardCache(project, !runtimeDir);
|
|
587
|
+
|
|
588
|
+
// The bundle report and budget gate still run for an isolated build —
|
|
589
|
+
// `blume build --isolated --budget-js 100` exiting 0 without measuring
|
|
590
|
+
// anything would be a silent false pass in CI.
|
|
867
591
|
if (runtimeDir) {
|
|
868
592
|
if (
|
|
869
593
|
project.config.deployment.output === "server" &&
|
|
@@ -918,7 +642,7 @@ export const buildCommand = defineCommand({
|
|
|
918
642
|
await emitCloudflareNegotiation(project, markdownRoutePaths(project));
|
|
919
643
|
}
|
|
920
644
|
|
|
921
|
-
await
|
|
645
|
+
await reportBuild(
|
|
922
646
|
project,
|
|
923
647
|
deployStaticDir(project.config, project.context),
|
|
924
648
|
args
|
|
@@ -6,6 +6,7 @@ import { defineCommand } from "citty";
|
|
|
6
6
|
import { join } from "pathe";
|
|
7
7
|
|
|
8
8
|
import { ensureGitignore } from "../../core/gitignore.ts";
|
|
9
|
+
import { commandMeta } from "../command-meta.ts";
|
|
9
10
|
import { refuseIfDevRunning } from "../dev-lock.ts";
|
|
10
11
|
import { logger } from "../log.ts";
|
|
11
12
|
import { prepareProject } from "../prepare.ts";
|
|
@@ -26,10 +27,7 @@ export const checkCommand = defineCommand({
|
|
|
26
27
|
type: "boolean",
|
|
27
28
|
},
|
|
28
29
|
},
|
|
29
|
-
meta:
|
|
30
|
-
description: "Type-check the docs site with astro check.",
|
|
31
|
-
name: "check",
|
|
32
|
-
},
|
|
30
|
+
meta: commandMeta.check,
|
|
33
31
|
async run({ args }) {
|
|
34
32
|
const root = process.cwd();
|
|
35
33
|
|