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,370 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
import { dirname, join, resolve } from "pathe";
|
|
5
|
+
|
|
6
|
+
import { buildAgentReadability } from "../ai/agent-readability.ts";
|
|
7
|
+
import { API_CATALOG_PATH, buildApiCatalog } from "../ai/api-catalog.ts";
|
|
8
|
+
import { buildHomeLinkHeader } from "../ai/link-headers.ts";
|
|
9
|
+
import { buildLlmsFiles } from "../ai/llms.ts";
|
|
10
|
+
import { markdownRoutePaths } from "../ai/markdown.ts";
|
|
11
|
+
import {
|
|
12
|
+
AGENT_SKILLS_DIR,
|
|
13
|
+
buildSkillsIndex,
|
|
14
|
+
collectSkills,
|
|
15
|
+
} from "../ai/skills.ts";
|
|
16
|
+
import type { SkillArtifact } from "../ai/skills.ts";
|
|
17
|
+
import {
|
|
18
|
+
buildSignaturesDirectory,
|
|
19
|
+
SIGNATURES_DIRECTORY_PATH,
|
|
20
|
+
} from "../ai/web-bot-auth.ts";
|
|
21
|
+
import type { BlumeProject } from "../core/project-graph.ts";
|
|
22
|
+
import type { ResolvedConfig } from "../core/schema.ts";
|
|
23
|
+
import { buildSearchIndex } from "../search/build.ts";
|
|
24
|
+
import { syncSearchProvider } from "../search/sync/index.ts";
|
|
25
|
+
import { readsHeaderFiles } from "./adapter-output.ts";
|
|
26
|
+
import { buildNetlifyHeaders } from "./headers.ts";
|
|
27
|
+
import {
|
|
28
|
+
buildNetlifyRedirects,
|
|
29
|
+
buildRedirectManifest,
|
|
30
|
+
buildVercelConfig,
|
|
31
|
+
platformRedirects,
|
|
32
|
+
} from "./redirects.ts";
|
|
33
|
+
import { buildRobots } from "./robots.ts";
|
|
34
|
+
import { buildSitemapFiles, describeSitemapFiles } from "./sitemap.ts";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The deploy artifacts Blume layers onto Astro's build output: the search
|
|
38
|
+
* index (and hosted-provider sync), llms.txt, sitemap, robots, the
|
|
39
|
+
* agent-readability manifest, the `.well-known` discovery files, Agent
|
|
40
|
+
* Skills, and the platform `_redirects`/`_headers` files.
|
|
41
|
+
*
|
|
42
|
+
* Written from the integration's `astro:build:done` hook — Astro's channel
|
|
43
|
+
* for exactly this kind of post-build work — into the directory Astro
|
|
44
|
+
* reports as the client output (`dist/`, or `dist/client` for a server
|
|
45
|
+
* build). The Vercel adapter copies that directory into its Build Output
|
|
46
|
+
* static tree in a later hook, so the artifacts ride along; every other
|
|
47
|
+
* adapter serves it directly. Running inside the hook rather than after
|
|
48
|
+
* `astro build` returns means an ejected project keeps producing them.
|
|
49
|
+
*
|
|
50
|
+
* Every file yields to one the user ships in `public/` (Astro copied it into
|
|
51
|
+
* the output before this runs).
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/** The log surface the writers report through (Astro's integration logger). */
|
|
55
|
+
export interface ArtifactLogger {
|
|
56
|
+
info: (message: string) => void;
|
|
57
|
+
warn: (message: string) => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Emit platform redirect files for a static build (adapters wire redirects
|
|
62
|
+
* natively). Always writes the manifest; writes `_redirects`/`vercel.json` only
|
|
63
|
+
* when the user hasn't shipped one via public/. Note that Vercel's
|
|
64
|
+
* git-integration builds read `vercel.json` from the repository root only —
|
|
65
|
+
* the copy emitted here takes effect when the dist folder itself is deployed
|
|
66
|
+
* directly via the Vercel CLI.
|
|
67
|
+
*/
|
|
68
|
+
const emitRedirectFiles = async (
|
|
69
|
+
config: ResolvedConfig,
|
|
70
|
+
distDir: string,
|
|
71
|
+
logger: ArtifactLogger
|
|
72
|
+
): Promise<void> => {
|
|
73
|
+
const redirects = platformRedirects(config);
|
|
74
|
+
if (redirects.length === 0 || config.deployment.output !== "static") {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
await writeFile(
|
|
78
|
+
join(distDir, "blume-redirects.json"),
|
|
79
|
+
buildRedirectManifest(redirects),
|
|
80
|
+
"utf-8"
|
|
81
|
+
);
|
|
82
|
+
const platformFiles = [
|
|
83
|
+
{ content: buildNetlifyRedirects(redirects), name: "_redirects" },
|
|
84
|
+
{ content: buildVercelConfig(redirects), name: "vercel.json" },
|
|
85
|
+
];
|
|
86
|
+
await Promise.all(
|
|
87
|
+
platformFiles.map((file) =>
|
|
88
|
+
existsSync(join(distDir, file.name))
|
|
89
|
+
? Promise.resolve()
|
|
90
|
+
: writeFile(join(distDir, file.name), file.content, "utf-8")
|
|
91
|
+
)
|
|
92
|
+
);
|
|
93
|
+
logger.info(`Emitted redirect files for ${redirects.length} redirect(s)`);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Emit a `_headers` file so Netlify / Cloudflare serve the raw AI-ready
|
|
98
|
+
* endpoints (`*.md`, `*.mdx`, `*.txt`) with an explicit `charset=utf-8`. Without
|
|
99
|
+
* it those hosts send `text/markdown` / `text/plain` with no charset and
|
|
100
|
+
* browsers fall back to Windows-1252, garbling any non-ASCII docs (#82).
|
|
101
|
+
*
|
|
102
|
+
* The same file carries the rest of the agent-discovery surface that only a
|
|
103
|
+
* response header can express: the homepage `Link` header (RFC 8288, see
|
|
104
|
+
* `ai/link-headers.ts`), and the registered media types for the extensionless
|
|
105
|
+
* well-known files — `application/linkset+json` for the API catalog, the
|
|
106
|
+
* signatures directory, and the Agent Skills archives. A static host serves
|
|
107
|
+
* those as `octet-stream` or nothing at all without a rule.
|
|
108
|
+
*
|
|
109
|
+
* A `_headers` shipped in `public/` wins, exactly like `_redirects` — the opt-out
|
|
110
|
+
* is checked at its source rather than in `dist`, because on Cloudflare the file
|
|
111
|
+
* in `dist` is not necessarily the user's: `@astrojs/cloudflare` writes its own
|
|
112
|
+
* `_headers` (an immutable `Cache-Control` rule for `/_astro/*`) during the
|
|
113
|
+
* build, before this runs. Testing `dist` therefore read an adapter-generated
|
|
114
|
+
* file as a user opt-out and skipped silently. When both exist, the adapter's
|
|
115
|
+
* rules are preserved and ours are appended.
|
|
116
|
+
*
|
|
117
|
+
* Gated on {@link readsHeaderFiles}, not on `output === "static"`. A **Cloudflare
|
|
118
|
+
* server** build serves `dist/client` through the Worker's ASSETS binding, and
|
|
119
|
+
* Workers static assets honor `_headers` from that directory — so the file
|
|
120
|
+
* applies there too, and skipping it left every Cloudflare server build with no
|
|
121
|
+
* `Link` header and no media type on its own discovery files. The charset half
|
|
122
|
+
* of this file *is* redundant on a server build, because the runtime endpoint
|
|
123
|
+
* sets Content-Type on the Response itself; the `Link` and well-known halves are
|
|
124
|
+
* not, and one conclusion about the first was applied to all three.
|
|
125
|
+
*
|
|
126
|
+
* Exported for the test suite, which exercises it in a subprocess like the
|
|
127
|
+
* command helpers.
|
|
128
|
+
*/
|
|
129
|
+
export const emitHeaderFiles = async (
|
|
130
|
+
project: BlumeProject,
|
|
131
|
+
distDir: string,
|
|
132
|
+
logger: ArtifactLogger
|
|
133
|
+
): Promise<void> => {
|
|
134
|
+
const { config } = project;
|
|
135
|
+
if (
|
|
136
|
+
!readsHeaderFiles(config.deployment) ||
|
|
137
|
+
existsSync(join(project.context.root, "public", "_headers"))
|
|
138
|
+
) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const ours = buildNetlifyHeaders(
|
|
142
|
+
config,
|
|
143
|
+
buildHomeLinkHeader(config, markdownRoutePaths(project))
|
|
144
|
+
);
|
|
145
|
+
// An adapter may have written its own rules here already (Cloudflare adds an
|
|
146
|
+
// immutable Cache-Control for /_astro/*). Keep them and append ours: both
|
|
147
|
+
// sets are wanted, and `_headers` has no merge semantics beyond order.
|
|
148
|
+
const target = join(distDir, "_headers");
|
|
149
|
+
const existing = existsSync(target) ? await readFile(target, "utf-8") : "";
|
|
150
|
+
await writeFile(
|
|
151
|
+
target,
|
|
152
|
+
existing ? `${existing.trimEnd()}\n${ours}` : ours,
|
|
153
|
+
"utf-8"
|
|
154
|
+
);
|
|
155
|
+
logger.info("Emitted _headers (UTF-8 Content-Type + homepage Link header)");
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Collect the Agent Skills `ai.skills` publishes, once per build, so both the
|
|
160
|
+
* skills surface and llms.txt (which lists them) read the same set. Empty
|
|
161
|
+
* when the feature is off, the directory is missing, nothing in it is
|
|
162
|
+
* publishable (each with a warning), or a user-shipped
|
|
163
|
+
* `public/.well-known/agent-skills/index.json` already owns the surface.
|
|
164
|
+
*/
|
|
165
|
+
const collectConfiguredSkills = async (
|
|
166
|
+
project: BlumeProject,
|
|
167
|
+
distDir: string,
|
|
168
|
+
logger: ArtifactLogger
|
|
169
|
+
): Promise<SkillArtifact[]> => {
|
|
170
|
+
const configured = project.config.ai.skills;
|
|
171
|
+
if (!configured) {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
const dir = resolve(project.context.root, configured);
|
|
175
|
+
if (!existsSync(dir)) {
|
|
176
|
+
logger.warn(
|
|
177
|
+
`ai.skills points at "${configured}" (${dir}), which does not exist; no skills published.`
|
|
178
|
+
);
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
if (existsSync(join(distDir, AGENT_SKILLS_DIR.slice(1), "index.json"))) {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
const { skills, warnings } = await collectSkills(dir);
|
|
185
|
+
for (const warning of warnings) {
|
|
186
|
+
logger.warn(warning);
|
|
187
|
+
}
|
|
188
|
+
if (skills.length === 0) {
|
|
189
|
+
logger.warn(`ai.skills: no publishable skills found in "${configured}".`);
|
|
190
|
+
}
|
|
191
|
+
return skills;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Publish the collected Agent Skills: copy each skill artifact under
|
|
196
|
+
* `.well-known/agent-skills/` and emit the discovery index. A user-shipped
|
|
197
|
+
* `public/.well-known/agent-skills/index.json` takes over the whole surface
|
|
198
|
+
* (the collector returns nothing then), matching every other generated
|
|
199
|
+
* artifact.
|
|
200
|
+
*/
|
|
201
|
+
const emitAgentSkills = async (
|
|
202
|
+
project: BlumeProject,
|
|
203
|
+
distDir: string,
|
|
204
|
+
skills: readonly SkillArtifact[],
|
|
205
|
+
logger: ArtifactLogger
|
|
206
|
+
): Promise<void> => {
|
|
207
|
+
if (skills.length === 0) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const outDir = join(distDir, AGENT_SKILLS_DIR.slice(1));
|
|
211
|
+
await Promise.all(
|
|
212
|
+
skills.map(async (skill) => {
|
|
213
|
+
const target = join(outDir, skill.path);
|
|
214
|
+
await mkdir(dirname(target), { recursive: true });
|
|
215
|
+
await writeFile(target, skill.content);
|
|
216
|
+
})
|
|
217
|
+
);
|
|
218
|
+
await writeFile(
|
|
219
|
+
join(outDir, "index.json"),
|
|
220
|
+
buildSkillsIndex(skills, project.config),
|
|
221
|
+
"utf-8"
|
|
222
|
+
);
|
|
223
|
+
logger.info(
|
|
224
|
+
`Published ${skills.length} agent skill(s) (.well-known/agent-skills/index.json)`
|
|
225
|
+
);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Emit the generated `.well-known` discovery files — the RFC 9727 API catalog
|
|
230
|
+
* and the Web Bot Auth signature directory — each skipped when the feature is
|
|
231
|
+
* off or when the user ships their own copy via public/ (already in dist by
|
|
232
|
+
* the time this runs).
|
|
233
|
+
*/
|
|
234
|
+
const emitWellKnownFiles = async (
|
|
235
|
+
config: ResolvedConfig,
|
|
236
|
+
distDir: string,
|
|
237
|
+
logger: ArtifactLogger
|
|
238
|
+
): Promise<void> => {
|
|
239
|
+
const files = [
|
|
240
|
+
{
|
|
241
|
+
content: buildSignaturesDirectory(config),
|
|
242
|
+
label: "Web Bot Auth",
|
|
243
|
+
path: SIGNATURES_DIRECTORY_PATH,
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
content: buildApiCatalog(config),
|
|
247
|
+
label: "RFC 9727",
|
|
248
|
+
path: API_CATALOG_PATH,
|
|
249
|
+
},
|
|
250
|
+
];
|
|
251
|
+
for (const file of files) {
|
|
252
|
+
const target = join(distDir, file.path.slice(1));
|
|
253
|
+
if (!file.content || existsSync(target)) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
// Sequential by nature: both files share the .well-known dir creation.
|
|
257
|
+
// oxlint-disable-next-line no-await-in-loop
|
|
258
|
+
await mkdir(join(distDir, ".well-known"), { recursive: true });
|
|
259
|
+
// oxlint-disable-next-line no-await-in-loop
|
|
260
|
+
await writeFile(target, file.content, "utf-8");
|
|
261
|
+
logger.info(`Generated ${file.path.slice(1)} (${file.label})`);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Generate `llms.txt`/`llms-full.txt` into the dist dir. A user's own file in
|
|
267
|
+
* `public/` (copied into dist by Astro before this runs, like the sitemap and
|
|
268
|
+
* robots.txt) wins over the generated one — each file is checked and replaced
|
|
269
|
+
* independently, so a custom `llms.txt` still gets a generated `llms-full.txt`.
|
|
270
|
+
*/
|
|
271
|
+
const publishLlmsFiles = async (
|
|
272
|
+
project: BlumeProject,
|
|
273
|
+
distDir: string,
|
|
274
|
+
skills: readonly SkillArtifact[],
|
|
275
|
+
logger: ArtifactLogger
|
|
276
|
+
): Promise<void> => {
|
|
277
|
+
const indexPath = join(distDir, "llms.txt");
|
|
278
|
+
const fullPath = join(distDir, "llms-full.txt");
|
|
279
|
+
const writeIndex = !existsSync(indexPath);
|
|
280
|
+
const writeFull = !existsSync(fullPath);
|
|
281
|
+
if (!(writeIndex || writeFull)) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const { index, full } = await buildLlmsFiles(project, { skills });
|
|
285
|
+
const writes: Promise<void>[] = [];
|
|
286
|
+
if (writeIndex) {
|
|
287
|
+
writes.push(writeFile(indexPath, index, "utf-8"));
|
|
288
|
+
}
|
|
289
|
+
if (writeFull) {
|
|
290
|
+
writes.push(writeFile(fullPath, full, "utf-8"));
|
|
291
|
+
}
|
|
292
|
+
await Promise.all(writes);
|
|
293
|
+
logger.info(
|
|
294
|
+
`Generated ${[
|
|
295
|
+
writeIndex ? "llms.txt" : null,
|
|
296
|
+
writeFull ? "llms-full.txt" : null,
|
|
297
|
+
]
|
|
298
|
+
.filter(Boolean)
|
|
299
|
+
.join(" and ")}`
|
|
300
|
+
);
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Write every deploy artifact into `distDir`, the directory the platform
|
|
305
|
+
* serves as static files (see the module comment). A user's own `public/`
|
|
306
|
+
* file always wins. `indexSearch` is the Pagefind indexer, replaceable by
|
|
307
|
+
* tests: Pagefind's in-process service cannot be reopened once a build has
|
|
308
|
+
* closed it, so only one suite may run the real one.
|
|
309
|
+
*/
|
|
310
|
+
export const publishBuildArtifacts = async (
|
|
311
|
+
project: BlumeProject,
|
|
312
|
+
distDir: string,
|
|
313
|
+
logger: ArtifactLogger,
|
|
314
|
+
indexSearch: (outDir: string) => Promise<number> = buildSearchIndex
|
|
315
|
+
): Promise<void> => {
|
|
316
|
+
if (project.config.search.provider === "pagefind") {
|
|
317
|
+
logger.info("Building search index");
|
|
318
|
+
const indexed = await indexSearch(distDir);
|
|
319
|
+
logger.info(`Indexed ${indexed} page(s) for search`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Upload the index to a hosted provider (Algolia, Orama Cloud, Typesense).
|
|
323
|
+
// Skipped with a warning when its admin key isn't configured.
|
|
324
|
+
await syncSearchProvider(project, {
|
|
325
|
+
start: logger.info.bind(logger),
|
|
326
|
+
success: logger.info.bind(logger),
|
|
327
|
+
warn: logger.warn.bind(logger),
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Collected once: llms.txt lists the skills the build publishes below.
|
|
331
|
+
const skills = await collectConfiguredSkills(project, distDir, logger);
|
|
332
|
+
if (project.config.ai.llmsTxt.enabled) {
|
|
333
|
+
await publishLlmsFiles(project, distDir, skills, logger);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const sitemapFiles = buildSitemapFiles(project);
|
|
337
|
+
if (sitemapFiles && !existsSync(join(distDir, "sitemap.xml"))) {
|
|
338
|
+
await Promise.all(
|
|
339
|
+
sitemapFiles.map((file) =>
|
|
340
|
+
writeFile(join(distDir, file.name), file.xml, "utf-8")
|
|
341
|
+
)
|
|
342
|
+
);
|
|
343
|
+
logger.info(describeSitemapFiles(sitemapFiles));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const robots = buildRobots(project);
|
|
347
|
+
if (robots && !existsSync(join(distDir, "robots.txt"))) {
|
|
348
|
+
await writeFile(join(distDir, "robots.txt"), robots, "utf-8");
|
|
349
|
+
logger.info("Generated robots.txt");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const agentReadability = buildAgentReadability(project);
|
|
353
|
+
if (
|
|
354
|
+
agentReadability &&
|
|
355
|
+
!existsSync(join(distDir, "agent-readability.json"))
|
|
356
|
+
) {
|
|
357
|
+
await writeFile(
|
|
358
|
+
join(distDir, "agent-readability.json"),
|
|
359
|
+
`${JSON.stringify(agentReadability, null, 2)}\n`,
|
|
360
|
+
"utf-8"
|
|
361
|
+
);
|
|
362
|
+
logger.info("Generated agent-readability.json");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
await emitWellKnownFiles(project.config, distDir, logger);
|
|
366
|
+
await emitAgentSkills(project, distDir, skills, logger);
|
|
367
|
+
|
|
368
|
+
await emitRedirectFiles(project.config, distDir, logger);
|
|
369
|
+
await emitHeaderFiles(project, distDir, logger);
|
|
370
|
+
};
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
* Blume prerenders every content page — even under `deployment.output:
|
|
5
5
|
* "server"` — and on Cloudflare the ASSETS binding serves those files before
|
|
6
6
|
* the Worker script runs, so no server-side code (Astro middleware included)
|
|
7
|
-
* ever sees a content-page request. Worse, even a request that
|
|
8
|
-
* Worker is answered by `@astrojs/cloudflare`'s handler
|
|
9
|
-
* ASSETS binding, ahead of `app.render` — the only place
|
|
7
|
+
* ever sees a content-page request. Worse, even a content-page request that
|
|
8
|
+
* does reach the Worker is answered by `@astrojs/cloudflare`'s handler
|
|
9
|
+
* straight from the ASSETS binding, ahead of `app.render` — the only place
|
|
10
|
+
* middleware runs — because `app.match` resolves a prerendered route to
|
|
11
|
+
* nothing and the handler then falls back to the binding.
|
|
10
12
|
*
|
|
11
13
|
* Negotiation therefore needs two coordinated pieces, both applied to the
|
|
12
14
|
* adapter's emitted deploy bundle after `astro build`:
|
|
@@ -20,6 +22,22 @@
|
|
|
20
22
|
* from the ASSETS binding, and it delegates everything else to the Astro
|
|
21
23
|
* Worker untouched.
|
|
22
24
|
*
|
|
25
|
+
* The prerendered per-page JSON documents (`/api/docs/pages/{route}.json`)
|
|
26
|
+
* are the one prerendered surface the adapter's fallback does *not* cover.
|
|
27
|
+
* They come from a prerendered *dynamic* route, and for those Astro's
|
|
28
|
+
* `matchRequest` returns the first non-prerendered route matching the same
|
|
29
|
+
* path — on a server build that is the `/api/[...path]` catch-all, which
|
|
30
|
+
* answers with a 404 problem document; `fallbackToAssets` never runs. The
|
|
31
|
+
* static-pathname endpoints (`pages.json`, `navigation.json`) are unaffected:
|
|
32
|
+
* they sit in the manifest's asset set, which the handler serves from the
|
|
33
|
+
* binding first. So whenever a worker-first rule claims a page JSON URL, the
|
|
34
|
+
* wrapper answers it from the binding itself, keyed by the exact set of
|
|
35
|
+
* documents the build emitted. The generated rule sets that would claim every
|
|
36
|
+
* `.json` file — a subpath base and the coarse fallback — exempt `*.json`
|
|
37
|
+
* outright, keeping those files on the zero-Worker path; the wrapper branch
|
|
38
|
+
* covers the remaining claims (a content section under `/api`, or
|
|
39
|
+
* user-configured rules).
|
|
40
|
+
*
|
|
23
41
|
* Cloudflare does not apply `_headers` to worker-first routes, so the wrapper
|
|
24
42
|
* also re-stamps what the static layer would otherwise add on the routes it
|
|
25
43
|
* takes over: the homepage agent-discovery `Link` header and the Markdown
|
|
@@ -64,8 +82,19 @@ const MAX_RULE_LENGTH = 100;
|
|
|
64
82
|
* route everything through the Worker except the fingerprinted build assets
|
|
65
83
|
* and the raw AI-ready endpoints, whose `charset=utf-8` comes from `_headers`
|
|
66
84
|
* (not applied on worker-first routes) and whose responses never negotiate.
|
|
85
|
+
* The prerendered `.json` documents are exempted for the same reason, and
|
|
86
|
+
* because the Astro Worker would hand the per-page ones to the `/api/`
|
|
87
|
+
* catch-all (see the module comment); a miss on an exempted path still
|
|
88
|
+
* invokes the Worker, so unknown `.json` URLs keep their problem document.
|
|
67
89
|
*/
|
|
68
|
-
const FALLBACK_RULES = [
|
|
90
|
+
const FALLBACK_RULES = [
|
|
91
|
+
"/*",
|
|
92
|
+
"!/_astro/*",
|
|
93
|
+
"!/*.md",
|
|
94
|
+
"!/*.mdx",
|
|
95
|
+
"!/*.txt",
|
|
96
|
+
"!/*.json",
|
|
97
|
+
];
|
|
69
98
|
|
|
70
99
|
/**
|
|
71
100
|
* The deployment base as a rule/URL prefix: trailing slash stripped, empty
|
|
@@ -107,7 +136,8 @@ const ruleBody = (rule: string): string =>
|
|
|
107
136
|
* `.md`/`.mdx` mirrors; a bare route gets its exact path in both request
|
|
108
137
|
* spellings (with and without the trailing slash) so no unrelated URL pays
|
|
109
138
|
* the Worker hop. On a subpath deploy the whole base is routed as one group —
|
|
110
|
-
* every route lives under it anyway
|
|
139
|
+
* every route lives under it anyway — with the raw endpoints and the
|
|
140
|
+
* prerendered `.json` documents exempted like the coarse fallback does.
|
|
111
141
|
*
|
|
112
142
|
* Configured redirects need no exemption from these rules: the wrapper Worker
|
|
113
143
|
* answers any it claims from its baked-in redirect table with the configured
|
|
@@ -125,6 +155,7 @@ export const buildRunWorkerFirstRules = (
|
|
|
125
155
|
`!${prefix}/*.md`,
|
|
126
156
|
`!${prefix}/*.mdx`,
|
|
127
157
|
`!${prefix}/*.txt`,
|
|
158
|
+
`!${prefix}/*.json`,
|
|
128
159
|
`!${prefix}/_astro/*`,
|
|
129
160
|
];
|
|
130
161
|
}
|
|
@@ -230,6 +261,13 @@ export interface NegotiationWorkerOptions {
|
|
|
230
261
|
homeTokens?: number;
|
|
231
262
|
/** Configured redirects the wrapper answers with their exact status. */
|
|
232
263
|
redirects?: readonly WorkerRedirect[];
|
|
264
|
+
/**
|
|
265
|
+
* Base-less served paths of the prerendered per-page JSON documents the
|
|
266
|
+
* build emitted (see `pageJsonPath`), decoded. The wrapper answers exactly
|
|
267
|
+
* these from the assets binding, since the Astro Worker would route them to
|
|
268
|
+
* the `/api/` catch-all (see the module comment).
|
|
269
|
+
*/
|
|
270
|
+
pageJsonPaths?: readonly string[];
|
|
233
271
|
}
|
|
234
272
|
|
|
235
273
|
/**
|
|
@@ -242,6 +280,7 @@ export const buildNegotiationWorker = (
|
|
|
242
280
|
options: NegotiationWorkerOptions
|
|
243
281
|
): string => {
|
|
244
282
|
const routes = JSON.stringify(options.routePaths);
|
|
283
|
+
const pageJson = JSON.stringify(options.pageJsonPaths ?? []);
|
|
245
284
|
const binding = JSON.stringify(options.assetsBinding);
|
|
246
285
|
const prefix = JSON.stringify(encodeURI(basePrefix(options.base)));
|
|
247
286
|
const homeLinkHeader = JSON.stringify(options.homeLinkHeader ?? null);
|
|
@@ -266,13 +305,17 @@ export const buildNegotiationWorker = (
|
|
|
266
305
|
// build. \`assets.run_worker_first\` routes content-page requests here instead
|
|
267
306
|
// of the platform's static layer; a client that prefers Markdown gets the
|
|
268
307
|
// page's prerendered \`.md\` mirror from the assets binding, a configured
|
|
269
|
-
// redirect is answered with its exact configured status, and
|
|
308
|
+
// redirect is answered with its exact configured status, and a prerendered
|
|
309
|
+
// per-page JSON document is served from the binding (the Astro Worker would
|
|
310
|
+
// route it to the \`/api/\` catch-all, because Astro resolves a prerendered
|
|
311
|
+
// dynamic route to the first live route on the same path). Every other
|
|
270
312
|
// request is delegated to the Astro Worker untouched. \`_headers\` does not
|
|
271
313
|
// apply to worker-first routes, so the homepage Link header and the Markdown
|
|
272
314
|
// charset are re-stamped here.
|
|
273
315
|
import server from ${JSON.stringify(options.mainSpecifier)};
|
|
274
316
|
|
|
275
317
|
const ROUTES = new Set(${routes});
|
|
318
|
+
const PAGE_JSON = new Set(${pageJson});
|
|
276
319
|
const BASE_PREFIX = ${prefix};
|
|
277
320
|
const ASSETS_BINDING = ${binding};
|
|
278
321
|
const HOME_LINK_HEADER = ${homeLinkHeader};
|
|
@@ -286,13 +329,32 @@ const REDIRECTS = ${redirects};
|
|
|
286
329
|
const redirectFor = (pathname) => {
|
|
287
330
|
const trimmed =
|
|
288
331
|
pathname !== "/" && pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
|
|
289
|
-
|
|
332
|
+
const path = safeDecode(trimmed);
|
|
333
|
+
return Object.hasOwn(REDIRECTS, path) ? REDIRECTS[path] : null;
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
// The request path with the deployment base removed: the whole path on a
|
|
337
|
+
// root deploy, \`""\` for the bare base, \`null\` for a path outside the base.
|
|
338
|
+
const stripBase = (pathname) => {
|
|
339
|
+
if (!BASE_PREFIX) {
|
|
340
|
+
return pathname;
|
|
341
|
+
}
|
|
342
|
+
if (pathname === BASE_PREFIX) {
|
|
343
|
+
return "";
|
|
344
|
+
}
|
|
345
|
+
return pathname.startsWith(BASE_PREFIX + "/")
|
|
346
|
+
? pathname.slice(BASE_PREFIX.length)
|
|
347
|
+
: null;
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
// Decoded for the lookups, which are keyed by decoded paths; a malformed
|
|
351
|
+
// escape keeps the raw path, which simply won't match anything.
|
|
352
|
+
const safeDecode = (path) => {
|
|
290
353
|
try {
|
|
291
|
-
|
|
354
|
+
return decodeURIComponent(path);
|
|
292
355
|
} catch {
|
|
293
|
-
|
|
356
|
+
return path;
|
|
294
357
|
}
|
|
295
|
-
return Object.hasOwn(REDIRECTS, path) ? REDIRECTS[path] : null;
|
|
296
358
|
};
|
|
297
359
|
|
|
298
360
|
// \`_redirects\` semantics, which the static layer applies to these same
|
|
@@ -341,21 +403,13 @@ const markdownVariantUrl = (rawUrl) => {
|
|
|
341
403
|
const queryIndex = rawUrl.indexOf("?");
|
|
342
404
|
const query = queryIndex === -1 ? "" : rawUrl.slice(queryIndex);
|
|
343
405
|
const rawPath = queryIndex === -1 ? rawUrl : rawUrl.slice(0, queryIndex);
|
|
344
|
-
|
|
345
|
-
if (
|
|
346
|
-
|
|
347
|
-
path = path.slice(BASE_PREFIX.length) || "/";
|
|
348
|
-
} else {
|
|
349
|
-
return null;
|
|
350
|
-
}
|
|
406
|
+
const rest = stripBase(rawPath);
|
|
407
|
+
if (rest === null) {
|
|
408
|
+
return null;
|
|
351
409
|
}
|
|
410
|
+
const path = rest || "/";
|
|
352
411
|
const trimmed = path !== "/" && path.endsWith("/") ? path.slice(0, -1) : path;
|
|
353
|
-
|
|
354
|
-
try {
|
|
355
|
-
pathname = decodeURIComponent(trimmed);
|
|
356
|
-
} catch {
|
|
357
|
-
// Keep the raw path; it simply won't match a content route.
|
|
358
|
-
}
|
|
412
|
+
const pathname = safeDecode(trimmed);
|
|
359
413
|
if (!ROUTES.has(pathname)) {
|
|
360
414
|
return null;
|
|
361
415
|
}
|
|
@@ -363,15 +417,17 @@ const markdownVariantUrl = (rawUrl) => {
|
|
|
363
417
|
return BASE_PREFIX + encodeURI(target) + ".md" + query;
|
|
364
418
|
};
|
|
365
419
|
|
|
420
|
+
// Exactly the per-page JSON documents the build emitted, so a URL the site
|
|
421
|
+
// never generated (a hidden page, a path outside the API) takes the normal
|
|
422
|
+
// route to the Astro Worker and its problem document.
|
|
423
|
+
const isPageJson = (pathname) => {
|
|
424
|
+
const rest = stripBase(pathname);
|
|
425
|
+
return rest !== null && PAGE_JSON.has(safeDecode(rest));
|
|
426
|
+
};
|
|
427
|
+
|
|
366
428
|
const isHomePath = (pathname) => {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
if (path !== BASE_PREFIX && !path.startsWith(BASE_PREFIX + "/")) {
|
|
370
|
-
return false;
|
|
371
|
-
}
|
|
372
|
-
path = path.slice(BASE_PREFIX.length);
|
|
373
|
-
}
|
|
374
|
-
return path === "" || path === "/";
|
|
429
|
+
const rest = stripBase(pathname);
|
|
430
|
+
return rest === "" || rest === "/";
|
|
375
431
|
};
|
|
376
432
|
|
|
377
433
|
const withHeaders = (response, apply) => {
|
|
@@ -395,9 +451,18 @@ export default {
|
|
|
395
451
|
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
396
452
|
return server.fetch(request, env, context);
|
|
397
453
|
}
|
|
454
|
+
const assets = env[ASSETS_BINDING];
|
|
455
|
+
// The request goes through untouched, so a conditional revalidation
|
|
456
|
+
// reaches the binding and comes back as a 304 — anything but a miss is
|
|
457
|
+
// the document's own answer. A miss falls through to the Astro Worker.
|
|
458
|
+
if (assets !== undefined && isPageJson(url.pathname)) {
|
|
459
|
+
const asset = await assets.fetch(request);
|
|
460
|
+
if (asset.status !== 404) {
|
|
461
|
+
return asset;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
398
464
|
const variant = markdownVariantUrl(url.pathname + url.search);
|
|
399
465
|
const home = isHomePath(url.pathname);
|
|
400
|
-
const assets = env[ASSETS_BINDING];
|
|
401
466
|
if (
|
|
402
467
|
variant !== null &&
|
|
403
468
|
assets !== undefined &&
|