diff --git a/.fallowrc.json b/.fallowrc.json
index a2960dab98..3494e711f0 100644
--- a/.fallowrc.json
+++ b/.fallowrc.json
@@ -42,9 +42,7 @@
"examples/sdk/batch-orchestration.ts",
"examples/sdk/ai-sdk-tools.ts",
"examples/sdk/host-runtime.ts",
- "test/integration/**/*.test.ts",
- "website/docs/404.mdx",
- "website/rspress.config.ts"
+ "test/integration/**/*.test.ts"
],
"framework": [
{
@@ -59,6 +57,8 @@
],
"ignorePatterns": [
"examples/test-app/**",
+ // The social card template: scripts/og.mjs serves it with generated /og/* files.
+ "website/og/og.html",
"scripts/perf/**",
"scripts/layering/**",
"scripts/di-seams/**",
@@ -71,7 +71,17 @@
// by an import edge. Same shape as the help-conformance declaration files above.
"scripts/size-report-package.d.mts"
],
- "dynamicallyLoaded": ["packages/daemon/src/execution/interaction/index.ts"],
+ "dynamicallyLoaded": [
+ "packages/daemon/src/execution/interaction/index.ts",
+ // The site's modules that Astro loads without an import Fallow can read: from an .astro
+ // ${home}` });
+ assert.equal(builtBase(page('home')), BASE);
+ assert.throws(() => builtBase(page('home')), /cannot read the site base/);
+});
diff --git a/scripts/capability-matrix/generate.ts b/scripts/capability-matrix/generate.ts
index 17cdacf152..4b91efbc9b 100644
--- a/scripts/capability-matrix/generate.ts
+++ b/scripts/capability-matrix/generate.ts
@@ -270,8 +270,6 @@ export function renderPage(data: MatrixData): string {
'description: Which commands each target supports, read from the runtime code that decides it.',
'---',
'',
- '# Capabilities',
- '',
'The target tables below are generated from the runtime command-admission facts that back the `capabilities` command: a command that leaves the code leaves this page. Labels, limits, and integration rows are maintained beside the generator, and command availability does not establish support for every operation, subcommand, or device model.',
`It covers agent-device ${data.version}, generated ${data.generatedAt}.`,
'',
diff --git a/scripts/check-affected/model.test.ts b/scripts/check-affected/model.test.ts
index 5a01805a95..bd278d9f9f 100644
--- a/scripts/check-affected/model.test.ts
+++ b/scripts/check-affected/model.test.ts
@@ -200,8 +200,8 @@ test('colocated Android binary fixtures select the unit suite instead of failing
);
});
-test('MCP metadata change selects the mcp-metadata check', () => {
- assert.deepEqual(ids(['server.json']), ['mcp-metadata']);
+test('MCP metadata change selects the mcp-metadata check and the site build that serves it', () => {
+ assert.deepEqual(ids(['server.json']), ['mcp-metadata', 'docs-links']);
});
test('public package surface change selects the build and the published-package gate via exports', () => {
@@ -231,7 +231,11 @@ test('user docs pages and README select the docs-links and docs-prose checks', (
assert.deepEqual(result.checks, ['docs-links', 'docs-prose'], file);
assert.deepEqual(result.docsOnlyPaths, []);
}
- for (const file of ['website/package.json', 'website/rspress.config.ts']) {
+ for (const file of [
+ 'website/package.json',
+ 'website/astro.config.mjs',
+ 'website/src/content.config.ts',
+ ]) {
assert.deepEqual(plan([file]).checks, ['docs-links'], file);
}
assert.deepEqual(plan(['website/docs/docs/commands.md']).checks, [
diff --git a/scripts/check-affected/model.ts b/scripts/check-affected/model.ts
index 0cb44ec071..941eea309e 100644
--- a/scripts/check-affected/model.ts
+++ b/scripts/check-affected/model.ts
@@ -490,6 +490,12 @@ const BUILD_OWNERSHIP: ReadonlyArray<{
detail: 'MCP registry metadata must stay in sync',
owns: (file) => file === 'server.json' || file === 'smithery.yaml',
},
+ {
+ check: 'docs-links',
+ rule: 'own:docs-links',
+ detail: 'the site builds /.well-known/mcp.json from server.json',
+ owns: (file) => file === 'server.json',
+ },
// image-size ships no fixed version for its parser DoS advisories; the in-tree
// pnpm patch is the mitigation, and only its defining files own the proof.
{
@@ -566,9 +572,11 @@ function isAgentGuidance(file: string): boolean {
}
// The user docs: README and the docs site pages. `check:doc-prose` reads them; `check:doc-links`
-// resolves their links and also reruns when the website manifest or config that decide what the
-// built site renders change.
-const SITE_BUILD_INPUTS = new Set(['website/package.json', 'website/rspress.config.ts']);
+// resolves their links and also reruns when the site's own code changes: its manifest, config,
+// pages and components (everything in website/ outside the docs tree and loose Markdown).
+function isSiteBuildInput(file: string): boolean {
+ return file.startsWith('website/') && !file.startsWith('website/docs/') && !/\.mdx?$/.test(file);
+}
// The generated capabilities page. Its cells are a read of the runtime facts, so the
// page and the generator that renders it share one drift gate.
@@ -610,7 +618,7 @@ const docsOwnership: OwnershipRule = ({ file }) => {
),
);
}
- if (isUserDocPage(file) || SITE_BUILD_INPUTS.has(file)) {
+ if (isUserDocPage(file) || isSiteBuildInput(file)) {
selections.push(
reason(
'docs-links',
diff --git a/scripts/check-doc-links.ts b/scripts/check-doc-links.ts
index 759c04d341..82ae9bbf2c 100644
--- a/scripts/check-doc-links.ts
+++ b/scripts/check-doc-links.ts
@@ -1,9 +1,10 @@
-// `pnpm check:doc-links` — every internal link on the built docs site lands on a page and an
-// element ID that rspress actually rendered, and README.md's links into the published docs do too.
-// It reads website/doc_build after `pnpm --dir website build`, so rspress's own slugger, link
+// `pnpm check:doc-links` — every internal link on the built site lands on a page and an
+// element ID that Astro actually rendered, and README.md's links into the published docs do too.
+// It reads website/dist after `pnpm --dir website build`, so Starlight's own slugger, link
// rewriting, and clean URLs are what get checked. External URLs are skipped (no network).
+// Every docs page also needs its Markdown twin (`docs/.md`), which LLM clients fetch.
//
-// The HTML is parsed with regexes on purpose: it is rspress's generated output, which always
+// The HTML is parsed with regexes on purpose: it is Astro's generated output, which always
// writes attributes double-quoted with `&`, `"`, `<`, `>` entity-escaped, so `id="…"` and
// `href="…"` cannot contain an unescaped quote.
@@ -12,12 +13,11 @@ import path from 'node:path';
import { pathToFileURL } from 'node:url';
const REPO_ROOT = path.resolve(import.meta.dirname, '..');
-const BUILD_DIR = path.join(REPO_ROOT, 'website/doc_build');
+const BUILD_DIR = path.join(REPO_ROOT, 'website/dist');
const DOCS_SOURCE_DIR = 'website/docs';
-// README links to the published site, whose root is this URL whatever base a local build used.
-const PUBLISHED_SITE = 'https://oss.callstack.com/agent-device/';
-const README_DOCS_URL =
- /https:\/\/oss\.callstack\.com\/agent-device\/docs\/[\w./-]*(?:#[\w%.-]+)?/g;
+// README links to the published site, whose root is this URL.
+const PUBLISHED_SITE = 'https://agent-device.dev/';
+const README_DOCS_URL = /https:\/\/agent-device\.dev\/docs\/[\w./-]*(?:#[\w%.-]+)?/g;
export type DocLinkViolation = {
/** Built page route (`docs/sessions`), or `README.md:`. */
@@ -55,6 +55,15 @@ function listHtml(buildDir: string): string[] {
.sort();
}
+/** Every built docs page (`docs/x.html`) has its Markdown twin (`docs/x.md`). */
+function missingMarkdownTwins(buildDir: string, htmlFiles: readonly string[]): DocLinkViolation[] {
+ return htmlFiles
+ .filter((file) => file.startsWith('docs/'))
+ .map((file) => file.replace(/\.html$/, ''))
+ .filter((route) => !fs.existsSync(path.join(buildDir, `${route}.md`)))
+ .map((route) => ({ where: route, message: `no Markdown twin "${route}.md"` }));
+}
+
function duplicateHeadingIds(route: string, html: string): DocLinkViolation[] {
const seen = new Set();
const violations: DocLinkViolation[] = [];
@@ -133,7 +142,7 @@ function readmeViolations(site: Site, readmeText: string): DocLinkViolation[] {
}
/**
- * Check a built rspress site. `base` is the base it was built with, with leading and trailing
+ * Check a built site. `base` is the base it was built with, with leading and trailing
* slashes (`/agent-device/`); internal hrefs start with it. README's URLs name the published site.
*/
export function checkDocLinks(
@@ -142,8 +151,9 @@ export function checkDocLinks(
base: string,
): DocLinkViolation[] {
const pages = new Map();
- const violations: DocLinkViolation[] = [];
- for (const file of listHtml(buildDir)) {
+ const htmlFiles = listHtml(buildDir);
+ const violations = missingMarkdownTwins(buildDir, htmlFiles);
+ for (const file of htmlFiles) {
const html = fs.readFileSync(path.join(buildDir, file), 'utf8');
const ids = new Set(
[...html.matchAll(ATTRIBUTE('id'))].map(([, id]) => decodeEntities(id ?? '')),
@@ -160,16 +170,15 @@ export function checkDocLinks(
}
/**
- * The base the site was built with, read from the build rather than restated: rspress emits its
- * entry script under `assetPrefix`, which defaults to `base` (rspress `initRsbuild.js`).
+ * The base the site was built with, read from the build rather than restated: Astro emits its
+ * assets under `_astro/`.
*/
-function builtBase(buildDir: string): string {
+export function builtBase(buildDir: string): string {
const home = fs.readFileSync(path.join(buildDir, 'index.html'), 'utf8');
- const match = /
+