@caprail-dev/agent-pages 0.2.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/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # @caprail-dev/agent-pages
2
+
3
+ Markdown-first pages for AI agents in Next.js: co-locate a `page.md` next to
4
+ each `page.tsx` and serve it to AI crawlers and fetchers automatically —
5
+ plus generated `/llms.txt` and `/llms-full.txt`.
6
+
7
+ Real agent traffic (ClaudeBot, GPTBot, Claude Code, …) mostly does **not**
8
+ send `Accept: text/markdown`, so Accept-header-only negotiation misses it.
9
+ This package decides per request, in order:
10
+
11
+ 1. **Accept q-values** — `text/markdown` listed with `q > 0` and ≥ HTML;
12
+ 2. **User-agent classification** (via [`@caprail-dev/analytics`](https://www.npmjs.com/package/@caprail-dev/analytics)) — AI
13
+ crawlers/fetchers get markdown; search bots and agentic browsers keep
14
+ HTML (serving indexers different content is cloaking);
15
+ 3. **`Signature-Agent` header** (RFC 9421) — signed agents get markdown even
16
+ with a browser-like UA;
17
+ 4. **Heuristic fallback** — no `sec-fetch-mode` (real browsers always send
18
+ it) + a bot-like UA, except search/link-preview bots that need the HTML.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ npm i @caprail-dev/agent-pages # or bun add / pnpm add / yarn add
24
+ ```
25
+
26
+ ## Setup
27
+
28
+ **1. Wrap your `next.config.js`** — runs codegen on every `next dev` /
29
+ `next build` (and watches `page.md` files in dev):
30
+
31
+ ```js
32
+ import { withAgentPages } from "@caprail-dev/agent-pages/config";
33
+
34
+ export default withAgentPages(nextConfig, {
35
+ siteUrl: "https://example.com",
36
+ });
37
+ ```
38
+
39
+ **2. Co-locate `page.md` twins** next to the pages you want agent-readable:
40
+
41
+ ```
42
+ src/app/page.tsx → src/app/page.md (served at /index.md)
43
+ src/app/terms/page.tsx → src/app/terms/page.md (served at /terms.md)
44
+ ```
45
+
46
+ Optional frontmatter feeds the `/llms.txt` link list:
47
+
48
+ ```markdown
49
+ ---
50
+ title: Example
51
+ description: One-liner shown in /llms.txt.
52
+ ---
53
+
54
+ # Example — the markdown twin
55
+ ```
56
+
57
+ **3. Add the middleware:**
58
+
59
+ ```ts
60
+ // middleware.ts
61
+ import { createAgentPagesMiddleware } from "@caprail-dev/agent-pages/next";
62
+ import { MD_REWRITES } from "@/app/_agent-pages/rewrites";
63
+
64
+ export const middleware = createAgentPagesMiddleware({ rewrites: MD_REWRITES });
65
+ export const config = {
66
+ matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
67
+ };
68
+ ```
69
+
70
+ Apps with existing middleware compose via the decision object instead:
71
+
72
+ ```ts
73
+ import { decideAgentPages } from "@caprail-dev/agent-pages/next";
74
+ import { MD_REWRITES } from "@/app/_agent-pages/rewrites";
75
+
76
+ export function middleware(req: NextRequest) {
77
+ const d = decideAgentPages(req, MD_REWRITES);
78
+ // d.kind is "rewrite" or "html"; d.response has Vary set correctly.
79
+ return d.response;
80
+ }
81
+ ```
82
+
83
+ ## What gets generated
84
+
85
+ Inside your app dir (commit these — deterministic output keeps diffs
86
+ meaningful and shows exactly what agents will read):
87
+
88
+ - `_agent-pages/rewrites.ts` — HTML path → markdown twin record. Edge-safe
89
+ strings only; the **only** file the middleware imports.
90
+ - `_agent-pages/manifest.ts` — full doc registry (content inlined). Imported
91
+ only by route handlers; never reaches the edge bundle.
92
+ - `<path>.md/route.ts` per `page.md` — `force-static` markdown routes.
93
+ - `llms.txt/route.ts` + `llms-full.txt/route.ts` (disable with
94
+ `llms: { enabled: false }`).
95
+
96
+ Every generated file is marker-headed; the codegen **refuses to overwrite**
97
+ files without the marker and cleans up generated files whose `page.md` was
98
+ deleted.
99
+
100
+ ## Options
101
+
102
+ ```ts
103
+ withAgentPages(nextConfig, {
104
+ siteUrl: "https://example.com",
105
+ appDir: "src/app", // default: src/app, else app
106
+ llms: {
107
+ enabled: true, // default
108
+ intro: "# Example\n\n> …", // /llms.txt preamble before "## Docs"
109
+ },
110
+ extraDocs: [
111
+ // Docs whose markdown lives in app code (route stays hand-written);
112
+ // included in /llms.txt + /llms-full.txt via an emitted import.
113
+ {
114
+ mdPath: "/install.md",
115
+ title: "Install guide",
116
+ description: "Agent-readable install instructions.",
117
+ source: { module: "@/lib/install-doc", export: "INSTALL_DOC" },
118
+ },
119
+ ],
120
+ });
121
+ ```
122
+
123
+ ## Notes & limitations
124
+
125
+ - Dynamic segments (`[slug]/page.md`) are unsupported in v1 — skipped with a
126
+ warning.
127
+ - Interpolations are not supported in `page.md` — flatten values to literals
128
+ and guard them with a test (e.g. assert the file contains your
129
+ `TERMS_VERSION` constant).
130
+ - The dev watcher uses `fs.watch(…, { recursive: true })` (Node ≥ 20 on
131
+ Linux). Deleting a whole directory may not fire the `page.md` filter —
132
+ restart `next dev` to clean up (a restart with no changes writes nothing).
133
+ - `next.config.js` consumes the compiled `dist/config.js` — in monorepos,
134
+ build this package before `next dev` / `next build`.
135
+ - A blanket `/* eslint-disable */` heads every generated file; with
136
+ `reportUnusedDisableDirectives` enabled you may see warnings on clean
137
+ files — harmless.
138
+
139
+ ## License
140
+
141
+ MIT
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Codegen: scan `appDir/**\/page.md` and emit a single `_agent-pages/manifest`
3
+ * module holding every doc's content + the site/llms metadata. There are no
4
+ * per-page `.md` route handlers — the middleware (`decideAgentPages`) serves
5
+ * markdown inline from this manifest. Runs in plain Node (no bundler hooks —
6
+ * Turbopack-safe); invoked from `withAgentPages` at config evaluation and from
7
+ * the dev watcher.
8
+ *
9
+ * Safety rails: the manifest is marker-headed; an existing file without the
10
+ * marker is never overwritten (throw); stale marker-bearing files we used to
11
+ * generate (old per-doc routes, the rewrites module) are cleaned up;
12
+ * write-if-changed keeps mtimes stable so watchers don't feed back.
13
+ */
14
+ /** A standalone doc whose markdown lives in app code, not a `page.md`. */
15
+ export type ExtraDoc = {
16
+ /** Where the markdown is served — its route handler stays hand-written. */
17
+ mdPath: string;
18
+ title: string;
19
+ description: string;
20
+ /** Emitted as `import { <export> } from "<module>"` in the manifest. */
21
+ source: {
22
+ module: string;
23
+ export: string;
24
+ };
25
+ /** HTML page to rewrite from, or (default) null for markdown-only docs. */
26
+ htmlPath?: string | null;
27
+ };
28
+ export type GenerateOptions = {
29
+ siteUrl: string;
30
+ /** App Router directory to scan (absolute or cwd-relative). */
31
+ appDir: string;
32
+ /** `/llms.txt` preamble (before the `## Docs` list). The middleware serves
33
+ * `/llms.txt` + `/llms-full.txt` from the manifest — codegen only records
34
+ * this intro for it to use. */
35
+ llms?: {
36
+ intro?: string;
37
+ };
38
+ extraDocs?: ExtraDoc[];
39
+ };
40
+ export type GenerateResult = {
41
+ /** Every registered doc (markdown content omitted — it lives in the manifest). */
42
+ docs: Array<{
43
+ htmlPath: string | null;
44
+ mdPath: string;
45
+ title: string;
46
+ description: string;
47
+ }>;
48
+ /** Files (re)written this run, cwd-relative. */
49
+ written: string[];
50
+ /** Stale generated files deleted this run, cwd-relative. */
51
+ removed: string[];
52
+ /** `page.md` files skipped (dynamic segments), cwd-relative. */
53
+ skipped: string[];
54
+ };
55
+ export declare function generateAgentPages(opts: GenerateOptions): GenerateResult;
56
+ //# sourceMappingURL=codegen.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codegen.d.ts","sourceRoot":"","sources":["../src/codegen.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAaH,0EAA0E;AAC1E,MAAM,MAAM,QAAQ,GAAG;IACrB,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,wEAAwE;IACxE,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3C,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,+DAA+D;IAC/D,MAAM,EAAE,MAAM,CAAC;IACf;;mCAE+B;IAC/B,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1B,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,kFAAkF;IAClF,IAAI,EAAE,KAAK,CAAC;QACV,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;QACxB,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,MAAM,CAAC;QACd,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC,CAAC;IACH,gDAAgD;IAChD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,4DAA4D;IAC5D,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,gEAAgE;IAChE,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC;AA6KF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,eAAe,GAAG,cAAc,CAgGxE"}
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Codegen: scan `appDir/**\/page.md` and emit a single `_agent-pages/manifest`
3
+ * module holding every doc's content + the site/llms metadata. There are no
4
+ * per-page `.md` route handlers — the middleware (`decideAgentPages`) serves
5
+ * markdown inline from this manifest. Runs in plain Node (no bundler hooks —
6
+ * Turbopack-safe); invoked from `withAgentPages` at config evaluation and from
7
+ * the dev watcher.
8
+ *
9
+ * Safety rails: the manifest is marker-headed; an existing file without the
10
+ * marker is never overwritten (throw); stale marker-bearing files we used to
11
+ * generate (old per-doc routes, the rewrites module) are cleaned up;
12
+ * write-if-changed keeps mtimes stable so watchers don't feed back.
13
+ */
14
+ import fs from "node:fs";
15
+ import path from "node:path";
16
+ import { parseFrontmatter } from "./frontmatter.js";
17
+ /** First line of every generated file — the overwrite/cleanup sentinel. */
18
+ const MARKER = "// @generated by @caprail-dev/agent-pages";
19
+ const HEADER = `${MARKER} — do not edit; regenerated on every \`next dev\`/\`next build\`.
20
+ /* eslint-disable */
21
+ `;
22
+ const rel = (abs) => path.relative(process.cwd(), abs);
23
+ /**
24
+ * Walk `appDir` collecting `page.md` docs and cleanup candidates (existing
25
+ * `route.ts` files in `*.md` / llms route dirs, and everything inside
26
+ * `_agent-pages/`). Skips `_*`/`@*`/dot dirs; descends `(group)` dirs
27
+ * without adding a URL segment; skips dynamic `[…]` subtrees with a warning.
28
+ */
29
+ function scan(appDir) {
30
+ const docs = [];
31
+ const candidates = [];
32
+ const skipped = [];
33
+ const walk = (dir, segments) => {
34
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
35
+ const abs = path.join(dir, entry.name);
36
+ if (entry.isDirectory()) {
37
+ if (entry.name.includes("[")) {
38
+ const pageMd = path.join(abs, "page.md");
39
+ if (fs.existsSync(pageMd)) {
40
+ console.warn(`[agent-pages] skipping ${rel(pageMd)}: dynamic segments are unsupported in v1`);
41
+ skipped.push(rel(pageMd));
42
+ }
43
+ continue;
44
+ }
45
+ if (entry.name.startsWith("_") ||
46
+ entry.name.startsWith("@") ||
47
+ entry.name.startsWith(".")) {
48
+ continue;
49
+ }
50
+ if (entry.name.startsWith("(") && entry.name.endsWith(")")) {
51
+ walk(abs, segments); // route group — no URL segment
52
+ continue;
53
+ }
54
+ walk(abs, [...segments, entry.name]);
55
+ continue;
56
+ }
57
+ if (entry.name === "route.ts" &&
58
+ (dir.endsWith(".md") ||
59
+ path.basename(dir) === "llms.txt" ||
60
+ path.basename(dir) === "llms-full.txt")) {
61
+ candidates.push(abs);
62
+ }
63
+ if (entry.name !== "page.md")
64
+ continue;
65
+ const htmlPath = "/" + segments.join("/");
66
+ const mdPath = htmlPath === "/" ? "/index.md" : `${htmlPath}.md`;
67
+ const raw = fs.readFileSync(abs, "utf8");
68
+ const fm = parseFrontmatter(raw, rel(abs));
69
+ docs.push({
70
+ htmlPath,
71
+ mdPath,
72
+ title: fm.title || mdPath,
73
+ description: fm.description,
74
+ markdown: fm.body,
75
+ });
76
+ }
77
+ };
78
+ walk(appDir, []);
79
+ // `_agent-pages/` is skipped by the `_*` rule above but is entirely ours —
80
+ // every `.ts` inside it is a cleanup candidate.
81
+ const registryDir = path.join(appDir, "_agent-pages");
82
+ if (fs.existsSync(registryDir)) {
83
+ for (const name of fs.readdirSync(registryDir)) {
84
+ if (name.endsWith(".ts"))
85
+ candidates.push(path.join(registryDir, name));
86
+ }
87
+ }
88
+ return { docs, candidates, skipped };
89
+ }
90
+ /**
91
+ * Emit `line = value;` / `key: value,` with prettier's break-after-operator
92
+ * shape when the one-liner would exceed the 80-column print width, so the
93
+ * generated files are prettier-canonical as written.
94
+ */
95
+ function fit(indent, prefix, value, suffix) {
96
+ const oneLine = `${indent}${prefix} ${value}${suffix}`;
97
+ if (oneLine.length <= 80)
98
+ return oneLine;
99
+ return `${indent}${prefix}\n${indent} ${value}${suffix}`;
100
+ }
101
+ /**
102
+ * String literal with prettier's quote choice: double quotes unless the
103
+ * content holds more `"` than `'` (then single quotes need fewer escapes) —
104
+ * markdown bodies embedding code snippets routinely trip this.
105
+ */
106
+ function quote(value) {
107
+ const json = JSON.stringify(value);
108
+ const doubles = (value.match(/"/g) ?? []).length;
109
+ const singles = (value.match(/'/g) ?? []).length;
110
+ if (doubles <= singles)
111
+ return json;
112
+ return `'${json.slice(1, -1).replace(/\\"/g, '"').replace(/'/g, "\\'")}'`;
113
+ }
114
+ function manifestModule(siteUrl, intro, docs) {
115
+ const imports = docs
116
+ .filter((d) => d.importedAs !== null)
117
+ .map((d) => `import { ${d.source.export} as ${d.importedAs} } from ${JSON.stringify(d.source.module)};`)
118
+ .join("\n");
119
+ const entries = docs
120
+ .map((d) => {
121
+ const lines = [
122
+ " {",
123
+ fit(" ", "htmlPath:", d.htmlPath === null ? "null" : JSON.stringify(d.htmlPath), ","),
124
+ fit(" ", "mdPath:", JSON.stringify(d.mdPath), ","),
125
+ fit(" ", "title:", quote(d.title), ","),
126
+ fit(" ", "description:", quote(d.description), ","),
127
+ fit(" ", "markdown:", d.importedAs ?? quote(d.body ?? ""), ","),
128
+ " },",
129
+ ];
130
+ return lines.join("\n");
131
+ })
132
+ .join("\n");
133
+ return `${HEADER}import type { AgentDoc } from "@caprail-dev/agent-pages";
134
+ ${imports ? "\n" + imports + "\n" : ""}
135
+ export const SITE_URL = ${JSON.stringify(siteUrl)};
136
+
137
+ ${fit("", "export const LLMS_INTRO =", quote(intro), ";")}
138
+
139
+ export const AGENT_DOCS: AgentDoc[] = [
140
+ ${entries}
141
+ ];
142
+ `;
143
+ }
144
+ export function generateAgentPages(opts) {
145
+ const appDir = path.resolve(opts.appDir);
146
+ if (!fs.existsSync(appDir)) {
147
+ throw new Error(`[agent-pages] appDir not found: ${opts.appDir}`);
148
+ }
149
+ const { docs: scanned, candidates, skipped } = scan(appDir);
150
+ let extraIdx = 0;
151
+ const docs = [
152
+ ...scanned.map((d) => ({ ...d, body: d.markdown, importedAs: null })),
153
+ ...(opts.extraDocs ?? []).map((d) => ({
154
+ htmlPath: d.htmlPath ?? null,
155
+ mdPath: d.mdPath,
156
+ title: d.title,
157
+ description: d.description,
158
+ body: null,
159
+ importedAs: `extraDoc${extraIdx++}`,
160
+ source: d.source,
161
+ })),
162
+ ].sort((a, b) => (a.mdPath < b.mdPath ? -1 : 1));
163
+ for (let i = 1; i < docs.length; i++) {
164
+ if (docs[i].mdPath === docs[i - 1].mdPath) {
165
+ throw new Error(`[agent-pages] duplicate mdPath ${docs[i].mdPath} — a page.md and an extraDoc collide`);
166
+ }
167
+ }
168
+ // The manifest is the single emitted artifact — the middleware serves every
169
+ // doc inline from it; there are no per-page route files anymore.
170
+ const intro = opts.llms?.intro ??
171
+ `# ${opts.siteUrl}\n\nEvery doc below is served as markdown.`;
172
+ const emits = new Map([
173
+ [
174
+ path.join(appDir, "_agent-pages", "manifest.ts"),
175
+ manifestModule(opts.siteUrl, intro, docs),
176
+ ],
177
+ ]);
178
+ // Write-if-changed; refuse to overwrite anything we did not generate.
179
+ const written = [];
180
+ for (const [abs, content] of emits) {
181
+ let existing = null;
182
+ try {
183
+ existing = fs.readFileSync(abs, "utf8");
184
+ }
185
+ catch {
186
+ // new file
187
+ }
188
+ if (existing !== null) {
189
+ if (!existing.startsWith(MARKER)) {
190
+ throw new Error(`[agent-pages] refusing to overwrite ${rel(abs)}: file exists and has no @generated marker. Delete or move it first.`);
191
+ }
192
+ if (existing === content)
193
+ continue;
194
+ }
195
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
196
+ fs.writeFileSync(abs, content);
197
+ written.push(rel(abs));
198
+ }
199
+ // Clean up generated files whose source is gone (deleted page.md, llms
200
+ // disabled, …). Unmarked files — e.g. hand-written routes — are left alone.
201
+ const removed = [];
202
+ for (const abs of candidates) {
203
+ if (emits.has(abs))
204
+ continue;
205
+ let existing;
206
+ try {
207
+ existing = fs.readFileSync(abs, "utf8");
208
+ }
209
+ catch {
210
+ continue;
211
+ }
212
+ if (!existing.startsWith(MARKER))
213
+ continue;
214
+ fs.rmSync(abs);
215
+ removed.push(rel(abs));
216
+ try {
217
+ fs.rmdirSync(path.dirname(abs)); // only succeeds when empty
218
+ }
219
+ catch {
220
+ // dir not empty — leave it
221
+ }
222
+ }
223
+ return {
224
+ docs: docs.map(({ htmlPath, mdPath, title, description }) => ({
225
+ htmlPath,
226
+ mdPath,
227
+ title,
228
+ description,
229
+ })),
230
+ written,
231
+ removed,
232
+ skipped,
233
+ };
234
+ }
235
+ //# sourceMappingURL=codegen.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codegen.js","sourceRoot":"","sources":["../src/codegen.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * `next.config.js` wrapper: runs codegen when the config is evaluated (every
3
+ * `next dev` start and `next build` — plain Node, no bundler hooks, so it is
4
+ * Turbopack-safe by construction) and, in dev, watches for `page.md` changes.
5
+ *
6
+ * Note for fresh clones: this module is consumed from the package's compiled
7
+ * `dist/` — build the package before running bare `next dev`/`next build`.
8
+ */
9
+ import { type ExtraDoc } from "./codegen.js";
10
+ export type AgentPagesOptions = {
11
+ /** Canonical origin, e.g. `"https://example.com"` — used in `/llms.txt` links. */
12
+ siteUrl: string;
13
+ /** App Router directory; defaults to `src/app` when it exists, else `app`. */
14
+ appDir?: string;
15
+ /** `/llms.txt` preamble (before the `## Docs` list); the middleware serves
16
+ * `/llms.txt` + `/llms-full.txt` from the manifest. */
17
+ llms?: {
18
+ intro?: string;
19
+ };
20
+ extraDocs?: ExtraDoc[];
21
+ };
22
+ /**
23
+ * Wrap a Next.js config with agent-pages codegen. Codegen errors (bad
24
+ * frontmatter, refusal to overwrite an unmarked file) propagate, aborting
25
+ * `next dev` / `next build` with the message. Returns the config unchanged.
26
+ */
27
+ export declare function withAgentPages<C extends object>(nextConfig: C, opts: AgentPagesOptions): C;
28
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,EAEL,KAAK,QAAQ,EAEd,MAAM,cAAc,CAAC;AAEtB,MAAM,MAAM,iBAAiB,GAAG;IAC9B,kFAAkF;IAClF,OAAO,EAAE,MAAM,CAAC;IAChB,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;2DACuD;IACvD,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1B,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAC;CACxB,CAAC;AAYF;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,MAAM,EAC7C,UAAU,EAAE,CAAC,EACb,IAAI,EAAE,iBAAiB,GACtB,CAAC,CAuCH"}
package/dist/config.js ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * `next.config.js` wrapper: runs codegen when the config is evaluated (every
3
+ * `next dev` start and `next build` — plain Node, no bundler hooks, so it is
4
+ * Turbopack-safe by construction) and, in dev, watches for `page.md` changes.
5
+ *
6
+ * Note for fresh clones: this module is consumed from the package's compiled
7
+ * `dist/` — build the package before running bare `next dev`/`next build`.
8
+ */
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import { clearTimeout, setTimeout } from "node:timers";
12
+ import { generateAgentPages, } from "./codegen.js";
13
+ /** One watcher per process, even though Next evaluates the config repeatedly. */
14
+ const WATCHER_KEY = Symbol.for("@caprail-dev/agent-pages.watcher");
15
+ function report(result) {
16
+ for (const file of result.written)
17
+ console.log(`[agent-pages] wrote ${file}`);
18
+ for (const file of result.removed) {
19
+ console.log(`[agent-pages] removed ${file}`);
20
+ }
21
+ }
22
+ /**
23
+ * Wrap a Next.js config with agent-pages codegen. Codegen errors (bad
24
+ * frontmatter, refusal to overwrite an unmarked file) propagate, aborting
25
+ * `next dev` / `next build` with the message. Returns the config unchanged.
26
+ */
27
+ export function withAgentPages(nextConfig, opts) {
28
+ const appDir = opts.appDir ?? (fs.existsSync("src/app") ? "src/app" : "app");
29
+ const run = () => generateAgentPages({
30
+ siteUrl: opts.siteUrl,
31
+ appDir,
32
+ llms: opts.llms,
33
+ extraDocs: opts.extraDocs,
34
+ });
35
+ report(run());
36
+ if (process.env.NODE_ENV === "development") {
37
+ const slots = globalThis;
38
+ if (!slots[WATCHER_KEY]) {
39
+ slots[WATCHER_KEY] = true;
40
+ let timer;
41
+ const watcher = fs.watch(appDir, { recursive: true }, (_event, filename) => {
42
+ if (!filename || path.basename(filename) !== "page.md")
43
+ return;
44
+ clearTimeout(timer);
45
+ timer = setTimeout(() => {
46
+ try {
47
+ report(run());
48
+ }
49
+ catch (err) {
50
+ // A bad edit must not crash the dev server — log and keep watching.
51
+ console.error("[agent-pages] codegen failed:", err);
52
+ }
53
+ }, 150);
54
+ timer.unref();
55
+ });
56
+ watcher.unref();
57
+ }
58
+ }
59
+ return nextConfig;
60
+ }
61
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEvD,OAAO,EACL,kBAAkB,GAGnB,MAAM,cAAc,CAAC;AAatB,iFAAiF;AACjF,MAAM,WAAW,GAAG,MAAM,CAAC,GAAG,CAAC,kCAAkC,CAAC,CAAC;AAEnE,SAAS,MAAM,CAAC,MAAsB;IACpC,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,OAAO;QAAE,OAAO,CAAC,GAAG,CAAC,uBAAuB,IAAI,EAAE,CAAC,CAAC;IAC9E,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QAClC,OAAO,CAAC,GAAG,CAAC,yBAAyB,IAAI,EAAE,CAAC,CAAC;IAC/C,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC5B,UAAa,EACb,IAAuB;IAEvB,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IAC7E,MAAM,GAAG,GAAG,GAAG,EAAE,CACf,kBAAkB,CAAC;QACjB,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,MAAM;QACN,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,SAAS,EAAE,IAAI,CAAC,SAAS;KAC1B,CAAC,CAAC;IAEL,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC;IAEd,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,aAAa,EAAE,CAAC;QAC3C,MAAM,KAAK,GAAG,UAAqC,CAAC;QACpD,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,CAAC;YACxB,KAAK,CAAC,WAAW,CAAC,GAAG,IAAI,CAAC;YAC1B,IAAI,KAAiC,CAAC;YACtC,MAAM,OAAO,GAAG,EAAE,CAAC,KAAK,CACtB,MAAM,EACN,EAAE,SAAS,EAAE,IAAI,EAAE,EACnB,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE;gBACnB,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAK,SAAS;oBAAE,OAAO;gBAC/D,YAAY,CAAC,KAAK,CAAC,CAAC;gBACpB,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;oBACtB,IAAI,CAAC;wBACH,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC;oBAChB,CAAC;oBAAC,OAAO,GAAG,EAAE,CAAC;wBACb,oEAAoE;wBACpE,OAAO,CAAC,KAAK,CAAC,+BAA+B,EAAE,GAAG,CAAC,CAAC;oBACtD,CAAC;gBACH,CAAC,EAAE,GAAG,CAAC,CAAC;gBACR,KAAK,CAAC,KAAK,EAAE,CAAC;YAChB,CAAC,CACF,CAAC;YACF,OAAO,CAAC,KAAK,EAAE,CAAC;QAClB,CAAC;IACH,CAAC;IAED,OAAO,UAAU,CAAC;AACpB,CAAC"}
package/dist/core.d.ts ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Markdown-first serving core: should a request for an HTML page be served
3
+ * its markdown twin instead, plus the doc registry shapes that drive the
4
+ * generated `/llms.txt`, `/llms-full.txt`, and per-doc markdown routes.
5
+ *
6
+ * Pure and edge-safe — no request objects, no Node APIs, just primitives —
7
+ * so it is unit-testable and runs unchanged in the Edge runtime.
8
+ */
9
+ /** HTML page path → markdown twin path (e.g. `"/" → "/index.md"`). */
10
+ export type MarkdownRewrites = Record<string, string>;
11
+ /**
12
+ * Whether the `Accept` header asks for markdown at least as strongly as HTML.
13
+ * Minimal q-value parse: `text/markdown` must be listed explicitly (a bare
14
+ * `*\/*` is not a markdown preference — browsers send it) with q > 0 and
15
+ * q(markdown) >= q(html).
16
+ */
17
+ export declare function prefersMarkdown(accept: string | null): boolean;
18
+ /** The request facts the markdown decision keys off — runtime-agnostic. */
19
+ export type MarkdownRequest = {
20
+ method: string;
21
+ accept: string | null;
22
+ userAgent: string | null;
23
+ signatureAgent?: string | null;
24
+ secFetchMode?: string | null;
25
+ };
26
+ /**
27
+ * Whether this client should be served markdown instead of HTML (path-agnostic
28
+ * — the caller decides the page has a markdown twin). Markdown wins when, in
29
+ * order:
30
+ *
31
+ * 1. the client asks for it (`Accept: text/markdown`);
32
+ * 2. the UA classifies as an AI crawler/fetcher — the consumers that want
33
+ * markdown. `search` engines (Googlebot, Bingbot, OAI-SearchBot, …) are
34
+ * excluded on purpose: serving indexers different content than humans is
35
+ * cloaking. `browser` agents (ChatGPT Agent / Atlas) render real pages,
36
+ * so they get HTML too — both verdicts are final and skip the heuristics;
37
+ * 3. the request carries a `Signature-Agent` header (RFC 9421) — AI agents
38
+ * that sign their requests (e.g. ChatGPT's) identify themselves this way
39
+ * even when their UA looks like a plain browser;
40
+ * 4. heuristic fallback for agents our classifier doesn't know yet: no
41
+ * `sec-fetch-mode` header (real browsers always send it) and a bot-like
42
+ * UA — except search/preview bots that need the HTML (see NEEDS_HTML).
43
+ */
44
+ export declare function clientWantsMarkdown(input: MarkdownRequest): boolean;
45
+ /**
46
+ * Resolve the markdown rewrite target for a request, or null to serve the page
47
+ * as-is — the rewrite-style helper for consumers that map an HTML path to a
48
+ * separate `.md` route. (Caprail's own proxy now serves markdown inline from
49
+ * the manifest via `decideAgentPages`; this stays for external rewrite setups.)
50
+ */
51
+ export declare function resolveMarkdownRewrite(input: MarkdownRequest & {
52
+ pathname: string;
53
+ }, rewrites: MarkdownRewrites): string | null;
54
+ /**
55
+ * The negotiation headers every markdown-negotiated response varies on —
56
+ * set on both the rewritten and the HTML branch of a negotiated path so
57
+ * non-Vercel intermediaries cache them separately.
58
+ */
59
+ export declare const AGENT_PAGES_VARY = "Accept, User-Agent, Signature-Agent, Sec-Fetch-Mode";
60
+ /** One agent-readable doc — drives the rewrite map, llms.txt, and routes. */
61
+ export type AgentDoc = {
62
+ /** The HTML page this doc twins, or null when the doc is markdown-only. */
63
+ htmlPath: string | null;
64
+ /** Where the markdown is served. */
65
+ mdPath: string;
66
+ title: string;
67
+ /** One-liner for the `/llms.txt` link list ("" when none was provided). */
68
+ description: string;
69
+ markdown: string;
70
+ };
71
+ /** `/llms.txt` index per the llmstxt.org convention. */
72
+ export declare function llmsTxt(opts: {
73
+ siteUrl: string;
74
+ /** Everything before the `## Docs` section — heading, blockquote, prose. */
75
+ intro: string;
76
+ docs: readonly AgentDoc[];
77
+ }): string;
78
+ /** `/llms-full.txt` — every doc's full content in one response. */
79
+ export declare function llmsFullTxt(docs: readonly AgentDoc[]): string;
80
+ /** Shared response shape for all markdown/llms routes. */
81
+ export declare function markdownResponse(body: string): Response;
82
+ //# sourceMappingURL=core.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,sEAAsE;AACtE,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAEtD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CA4B9D;AAmBD,2EAA2E;AAC3E,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAkBnE;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,eAAe,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,EAC7C,QAAQ,EAAE,gBAAgB,GACzB,MAAM,GAAG,IAAI,CAQf;AAED;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,wDAC0B,CAAC;AAExD,6EAA6E;AAC7E,MAAM,MAAM,QAAQ,GAAG;IACrB,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,oCAAoC;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,wDAAwD;AACxD,wBAAgB,OAAO,CAAC,IAAI,EAAE;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,SAAS,QAAQ,EAAE,CAAC;CAC3B,GAAG,MAAM,CAQT;AAED,mEAAmE;AACnE,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,QAAQ,EAAE,GAAG,MAAM,CAE7D;AAED,0DAA0D;AAC1D,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAOvD"}
package/dist/core.js ADDED
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Markdown-first serving core: should a request for an HTML page be served
3
+ * its markdown twin instead, plus the doc registry shapes that drive the
4
+ * generated `/llms.txt`, `/llms-full.txt`, and per-doc markdown routes.
5
+ *
6
+ * Pure and edge-safe — no request objects, no Node APIs, just primitives —
7
+ * so it is unit-testable and runs unchanged in the Edge runtime.
8
+ */
9
+ import { classify } from "@caprail-dev/analytics";
10
+ /**
11
+ * Whether the `Accept` header asks for markdown at least as strongly as HTML.
12
+ * Minimal q-value parse: `text/markdown` must be listed explicitly (a bare
13
+ * `*\/*` is not a markdown preference — browsers send it) with q > 0 and
14
+ * q(markdown) >= q(html).
15
+ */
16
+ export function prefersMarkdown(accept) {
17
+ if (!accept)
18
+ return false;
19
+ let qMd = null;
20
+ let qHtml = 0;
21
+ for (const part of accept.toLowerCase().split(",")) {
22
+ const [type, ...params] = part.split(";");
23
+ const mediaType = type?.trim();
24
+ if (!mediaType)
25
+ continue;
26
+ let q = 1;
27
+ for (const param of params) {
28
+ const [key, value] = param.split("=");
29
+ if (key?.trim() === "q") {
30
+ const parsed = Number(value?.trim());
31
+ if (Number.isFinite(parsed))
32
+ q = parsed;
33
+ }
34
+ }
35
+ if (mediaType === "text/markdown") {
36
+ qMd = Math.max(qMd ?? 0, q);
37
+ }
38
+ else if (mediaType === "text/html" ||
39
+ mediaType === "application/xhtml+xml") {
40
+ qHtml = Math.max(qHtml, q);
41
+ }
42
+ }
43
+ return qMd !== null && qMd > 0 && qMd >= qHtml;
44
+ }
45
+ /**
46
+ * Bot-like UA pattern for the heuristic fallback: generic bot vocabulary plus
47
+ * the HTTP client libraries agent frameworks ride on. Deliberately loose —
48
+ * it only fires when `sec-fetch-mode` is also absent, and serving markdown
49
+ * to a misjudged non-AI bot is low-harm.
50
+ */
51
+ const BOT_LIKE = /bot|crawler|spider|scraper|python-requests|python-httpx|aiohttp|go-http-client|node-fetch|axios|libwww/i;
52
+ /**
53
+ * Bot-like UAs that must keep getting HTML: search indexers our classifier
54
+ * doesn't know (serving them markdown is cloaking) and link-preview bots
55
+ * that read OpenGraph tags from the HTML head.
56
+ */
57
+ const NEEDS_HTML = /yandex|baidu|duckduckbot|applebot|seznambot|naver|sogou|petalbot|msnbot|slackbot|twitterbot|facebookexternalhit|facebot|discordbot|linkedinbot|whatsapp|telegrambot|pinterest|skypeuripreview|embedly|redditbot/i;
58
+ /**
59
+ * Whether this client should be served markdown instead of HTML (path-agnostic
60
+ * — the caller decides the page has a markdown twin). Markdown wins when, in
61
+ * order:
62
+ *
63
+ * 1. the client asks for it (`Accept: text/markdown`);
64
+ * 2. the UA classifies as an AI crawler/fetcher — the consumers that want
65
+ * markdown. `search` engines (Googlebot, Bingbot, OAI-SearchBot, …) are
66
+ * excluded on purpose: serving indexers different content than humans is
67
+ * cloaking. `browser` agents (ChatGPT Agent / Atlas) render real pages,
68
+ * so they get HTML too — both verdicts are final and skip the heuristics;
69
+ * 3. the request carries a `Signature-Agent` header (RFC 9421) — AI agents
70
+ * that sign their requests (e.g. ChatGPT's) identify themselves this way
71
+ * even when their UA looks like a plain browser;
72
+ * 4. heuristic fallback for agents our classifier doesn't know yet: no
73
+ * `sec-fetch-mode` header (real browsers always send it) and a bot-like
74
+ * UA — except search/preview bots that need the HTML (see NEEDS_HTML).
75
+ */
76
+ export function clientWantsMarkdown(input) {
77
+ if (input.method !== "GET" && input.method !== "HEAD")
78
+ return false;
79
+ if (prefersMarkdown(input.accept))
80
+ return true;
81
+ const agent = classify(input.userAgent);
82
+ if (agent.isAgent) {
83
+ return agent.type === "fetcher" || agent.type === "crawler";
84
+ }
85
+ if (input.signatureAgent)
86
+ return true;
87
+ return (!input.secFetchMode &&
88
+ !!input.userAgent &&
89
+ BOT_LIKE.test(input.userAgent) &&
90
+ !NEEDS_HTML.test(input.userAgent));
91
+ }
92
+ /**
93
+ * Resolve the markdown rewrite target for a request, or null to serve the page
94
+ * as-is — the rewrite-style helper for consumers that map an HTML path to a
95
+ * separate `.md` route. (Caprail's own proxy now serves markdown inline from
96
+ * the manifest via `decideAgentPages`; this stays for external rewrite setups.)
97
+ */
98
+ export function resolveMarkdownRewrite(input, rewrites) {
99
+ // Keys are extensionless page paths only, so a rewritten request (`/index.md`)
100
+ // can never match again — no rewrite loop. `hasOwn` guards prototype keys.
101
+ const target = Object.hasOwn(rewrites, input.pathname)
102
+ ? rewrites[input.pathname]
103
+ : undefined;
104
+ if (!target)
105
+ return null;
106
+ return clientWantsMarkdown(input) ? target : null;
107
+ }
108
+ /**
109
+ * The negotiation headers every markdown-negotiated response varies on —
110
+ * set on both the rewritten and the HTML branch of a negotiated path so
111
+ * non-Vercel intermediaries cache them separately.
112
+ */
113
+ export const AGENT_PAGES_VARY = "Accept, User-Agent, Signature-Agent, Sec-Fetch-Mode";
114
+ /** `/llms.txt` index per the llmstxt.org convention. */
115
+ export function llmsTxt(opts) {
116
+ const links = opts.docs
117
+ .map((d) => `- [${d.title}](${opts.siteUrl}${d.mdPath})${d.description ? `: ${d.description}` : ""}`)
118
+ .join("\n");
119
+ return `${opts.intro.trimEnd()}\n\n## Docs\n\n${links}\n`;
120
+ }
121
+ /** `/llms-full.txt` — every doc's full content in one response. */
122
+ export function llmsFullTxt(docs) {
123
+ return docs.map((d) => d.markdown.trimEnd()).join("\n\n---\n\n") + "\n";
124
+ }
125
+ /** Shared response shape for all markdown/llms routes. */
126
+ export function markdownResponse(body) {
127
+ return new Response(body, {
128
+ headers: {
129
+ "content-type": "text/markdown; charset=utf-8",
130
+ "cache-control": "public, max-age=3600, s-maxage=86400",
131
+ },
132
+ });
133
+ }
134
+ //# sourceMappingURL=core.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.js","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Minimal frontmatter for `page.md` files: only `title` and `description`,
3
+ * `key: value` lines between `---` delimiters. Deliberately not YAML — no
4
+ * lists, nesting, or multiline values — so there is no dependency and no
5
+ * surprise parses.
6
+ */
7
+ export type Frontmatter = {
8
+ title: string;
9
+ description: string;
10
+ /** The markdown with the frontmatter block (and leading blank lines) stripped. */
11
+ body: string;
12
+ };
13
+ /**
14
+ * Parse a `page.md` file. Frontmatter is only recognized when the first line
15
+ * is exactly `---`; the block ends at the next such line. Missing `title`
16
+ * falls back to the first `# ` heading in the body (warns when even that is
17
+ * absent); missing `description` warns and yields `""` — its `/llms.txt`
18
+ * entry will have no summary.
19
+ */
20
+ export declare function parseFrontmatter(raw: string, sourcePath: string): Frontmatter;
21
+ //# sourceMappingURL=frontmatter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frontmatter.d.ts","sourceRoot":"","sources":["../src/frontmatter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAcF;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,WAAW,CAwC7E"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Minimal frontmatter for `page.md` files: only `title` and `description`,
3
+ * `key: value` lines between `---` delimiters. Deliberately not YAML — no
4
+ * lists, nesting, or multiline values — so there is no dependency and no
5
+ * surprise parses.
6
+ */
7
+ /** Strip one layer of matching surrounding quotes from a frontmatter value. */
8
+ function unquote(value) {
9
+ if (value.length >= 2 &&
10
+ (value.startsWith('"') || value.startsWith("'")) &&
11
+ value.endsWith(value[0])) {
12
+ return value.slice(1, -1);
13
+ }
14
+ return value;
15
+ }
16
+ /**
17
+ * Parse a `page.md` file. Frontmatter is only recognized when the first line
18
+ * is exactly `---`; the block ends at the next such line. Missing `title`
19
+ * falls back to the first `# ` heading in the body (warns when even that is
20
+ * absent); missing `description` warns and yields `""` — its `/llms.txt`
21
+ * entry will have no summary.
22
+ */
23
+ export function parseFrontmatter(raw, sourcePath) {
24
+ const lines = raw.split(/\r?\n/);
25
+ const fields = {};
26
+ let body = raw;
27
+ if (lines[0]?.trim() === "---") {
28
+ const end = lines.findIndex((line, i) => i > 0 && line.trim() === "---");
29
+ if (end > 0) {
30
+ for (const line of lines.slice(1, end)) {
31
+ const colon = line.indexOf(":");
32
+ if (colon === -1)
33
+ continue;
34
+ const key = line.slice(0, colon).trim();
35
+ const value = unquote(line.slice(colon + 1).trim());
36
+ if (key)
37
+ fields[key] = value;
38
+ }
39
+ body = lines
40
+ .slice(end + 1)
41
+ .join("\n")
42
+ .replace(/^\n+/, "");
43
+ }
44
+ }
45
+ let title = fields.title ?? "";
46
+ if (!title) {
47
+ title = /^# (.+)$/m.exec(body)?.[1]?.trim() ?? "";
48
+ if (!title) {
49
+ console.warn(`[agent-pages] ${sourcePath}: no "title" frontmatter and no "# " heading — using the path as title`);
50
+ }
51
+ }
52
+ const description = fields.description ?? "";
53
+ if (!description) {
54
+ console.warn(`[agent-pages] ${sourcePath}: no "description" frontmatter — its /llms.txt entry will have no summary`);
55
+ }
56
+ return { title, description, body };
57
+ }
58
+ //# sourceMappingURL=frontmatter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frontmatter.js","sourceRoot":"","sources":["../src/frontmatter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AASH,+EAA+E;AAC/E,SAAS,OAAO,CAAC,KAAa;IAC5B,IACE,KAAK,CAAC,MAAM,IAAI,CAAC;QACjB,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QAChD,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,EACzB,CAAC;QACD,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC5B,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAW,EAAE,UAAkB;IAC9D,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACjC,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,IAAI,IAAI,GAAG,GAAG,CAAC;IAEf,IAAI,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,KAAK,EAAE,CAAC;QAC/B,MAAM,GAAG,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,KAAK,CAAC,CAAC;QACzE,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;YACZ,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;gBACvC,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;gBAChC,IAAI,KAAK,KAAK,CAAC,CAAC;oBAAE,SAAS;gBAC3B,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC;gBACxC,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;gBACpD,IAAI,GAAG;oBAAE,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;YAC/B,CAAC;YACD,IAAI,GAAG,KAAK;iBACT,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC;iBACd,IAAI,CAAC,IAAI,CAAC;iBACV,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QACzB,CAAC;IACH,CAAC;IAED,IAAI,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC;IAC/B,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;QAClD,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,CAAC,IAAI,CACV,iBAAiB,UAAU,wEAAwE,CACpG,CAAC;QACJ,CAAC;IACH,CAAC;IAED,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,IAAI,EAAE,CAAC;IAC7C,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,OAAO,CAAC,IAAI,CACV,iBAAiB,UAAU,2EAA2E,CACvG,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;AACtC,CAAC"}
package/dist/next.d.ts ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Next.js middleware adapter. `decideAgentPages` returns a decision object
3
+ * instead of a sealed middleware so apps can compose it with their own
4
+ * middleware work (e.g. beaconing the served path to analytics);
5
+ * `createAgentPagesMiddleware` is the batteries-included form.
6
+ *
7
+ * The decision serves markdown *inline* from the generated manifest — there
8
+ * are no per-page `.md` route handlers. A request for an HTML page's markdown
9
+ * twin (`/terms` from an agent, or a direct `/terms.md`) resolves straight to
10
+ * the `page.md` content; `/llms.txt` + `/llms-full.txt` are assembled on the
11
+ * fly. The middleware therefore imports the manifest content; keep doc sets
12
+ * reasonable (this is the trade for "author only `page.md`, no route files").
13
+ */
14
+ import { NextResponse, type NextRequest } from "next/server";
15
+ import { type AgentDoc } from "./core.js";
16
+ export type AgentPagesDecision = {
17
+ kind: "markdown";
18
+ /** The markdown path being served (`/index.md`, `/terms.md`, …). */
19
+ mdPath: string;
20
+ /** The markdown body — hand to `markdownResponse` / `serveAgentPages`. */
21
+ body: string;
22
+ /** True when this came from negotiating an HTML page (Vary applies). */
23
+ negotiated: boolean;
24
+ } | {
25
+ kind: "html";
26
+ /** True when the path has a markdown twin (set Vary on the response). */
27
+ negotiated: boolean;
28
+ };
29
+ /** `/llms.txt` + `/llms-full.txt` config; `false` disables those routes. */
30
+ export type LlmsOption = {
31
+ siteUrl: string;
32
+ intro: string;
33
+ } | false;
34
+ /**
35
+ * Decide what to serve for a request, sourcing content from the manifest docs:
36
+ *
37
+ * - `/llms.txt` / `/llms-full.txt` → assembled markdown (unless `llms: false`);
38
+ * - a direct markdown path (`/terms.md`) → that doc's markdown;
39
+ * - an HTML page whose twin the client wants as markdown → the twin's markdown;
40
+ * - anything else → HTML (with `negotiated` set when a twin exists, so the
41
+ * caller can add `Vary`).
42
+ */
43
+ export declare function decideAgentPages(req: NextRequest, docs: readonly AgentDoc[], opts?: {
44
+ llms?: LlmsOption;
45
+ }): AgentPagesDecision;
46
+ /**
47
+ * Build the markdown `Response` for a `kind: "markdown"` decision, with `Vary`
48
+ * set when it came from content negotiation.
49
+ */
50
+ export declare function serveAgentPages(decision: Extract<AgentPagesDecision, {
51
+ kind: "markdown";
52
+ }>): Response;
53
+ /** Batteries-included middleware for apps with no other middleware work. */
54
+ export declare function createAgentPagesMiddleware(opts: {
55
+ docs: readonly AgentDoc[];
56
+ llms?: LlmsOption;
57
+ }): (req: NextRequest) => Response | NextResponse;
58
+ //# sourceMappingURL=next.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"next.d.ts","sourceRoot":"","sources":["../src/next.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAE7D,OAAO,EAML,KAAK,QAAQ,EACd,MAAM,WAAW,CAAC;AAEnB,MAAM,MAAM,kBAAkB,GAC1B;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,wEAAwE;IACxE,UAAU,EAAE,OAAO,CAAC;CACrB,GACD;IACE,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,UAAU,EAAE,OAAO,CAAC;CACrB,CAAC;AAEN,4EAA4E;AAC5E,MAAM,MAAM,UAAU,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,KAAK,CAAC;AAEpE;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,SAAS,QAAQ,EAAE,EACzB,IAAI,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,UAAU,CAAA;CAAE,GAC3B,kBAAkB,CAyDpB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,OAAO,CAAC,kBAAkB,EAAE;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,CAAC,GAC1D,QAAQ,CAIV;AAED,4EAA4E;AAC5E,wBAAgB,0BAA0B,CAAC,IAAI,EAAE;IAC/C,IAAI,EAAE,SAAS,QAAQ,EAAE,CAAC;IAC1B,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB,IACS,KAAK,WAAW,KAAG,QAAQ,GAAG,YAAY,CAOnD"}
package/dist/next.js ADDED
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Next.js middleware adapter. `decideAgentPages` returns a decision object
3
+ * instead of a sealed middleware so apps can compose it with their own
4
+ * middleware work (e.g. beaconing the served path to analytics);
5
+ * `createAgentPagesMiddleware` is the batteries-included form.
6
+ *
7
+ * The decision serves markdown *inline* from the generated manifest — there
8
+ * are no per-page `.md` route handlers. A request for an HTML page's markdown
9
+ * twin (`/terms` from an agent, or a direct `/terms.md`) resolves straight to
10
+ * the `page.md` content; `/llms.txt` + `/llms-full.txt` are assembled on the
11
+ * fly. The middleware therefore imports the manifest content; keep doc sets
12
+ * reasonable (this is the trade for "author only `page.md`, no route files").
13
+ */
14
+ import { NextResponse } from "next/server";
15
+ import { AGENT_PAGES_VARY, clientWantsMarkdown, llmsFullTxt, llmsTxt, markdownResponse, } from "./core.js";
16
+ /**
17
+ * Decide what to serve for a request, sourcing content from the manifest docs:
18
+ *
19
+ * - `/llms.txt` / `/llms-full.txt` → assembled markdown (unless `llms: false`);
20
+ * - a direct markdown path (`/terms.md`) → that doc's markdown;
21
+ * - an HTML page whose twin the client wants as markdown → the twin's markdown;
22
+ * - anything else → HTML (with `negotiated` set when a twin exists, so the
23
+ * caller can add `Vary`).
24
+ */
25
+ export function decideAgentPages(req, docs, opts) {
26
+ const path = req.nextUrl.pathname;
27
+ const llms = opts?.llms;
28
+ if (llms && (req.method === "GET" || req.method === "HEAD")) {
29
+ if (path === "/llms.txt") {
30
+ return {
31
+ kind: "markdown",
32
+ mdPath: path,
33
+ negotiated: false,
34
+ body: llmsTxt({ siteUrl: llms.siteUrl, intro: llms.intro, docs }),
35
+ };
36
+ }
37
+ if (path === "/llms-full.txt") {
38
+ return {
39
+ kind: "markdown",
40
+ mdPath: path,
41
+ negotiated: false,
42
+ body: llmsFullTxt(docs),
43
+ };
44
+ }
45
+ }
46
+ // Direct `.md` URL — served regardless of negotiation (agents fetch these
47
+ // straight from `/llms.txt` links).
48
+ const direct = docs.find((d) => d.mdPath === path);
49
+ if (direct) {
50
+ return {
51
+ kind: "markdown",
52
+ mdPath: path,
53
+ negotiated: false,
54
+ body: direct.markdown,
55
+ };
56
+ }
57
+ // HTML page with a markdown twin — negotiate.
58
+ const twin = docs.find((d) => d.htmlPath === path);
59
+ if (twin) {
60
+ const wantsMarkdown = clientWantsMarkdown({
61
+ method: req.method,
62
+ accept: req.headers.get("accept"),
63
+ userAgent: req.headers.get("user-agent"),
64
+ signatureAgent: req.headers.get("signature-agent"),
65
+ secFetchMode: req.headers.get("sec-fetch-mode"),
66
+ });
67
+ if (wantsMarkdown) {
68
+ return {
69
+ kind: "markdown",
70
+ mdPath: twin.mdPath,
71
+ negotiated: true,
72
+ body: twin.markdown,
73
+ };
74
+ }
75
+ return { kind: "html", negotiated: true };
76
+ }
77
+ return { kind: "html", negotiated: false };
78
+ }
79
+ /**
80
+ * Build the markdown `Response` for a `kind: "markdown"` decision, with `Vary`
81
+ * set when it came from content negotiation.
82
+ */
83
+ export function serveAgentPages(decision) {
84
+ const res = markdownResponse(decision.body);
85
+ if (decision.negotiated)
86
+ res.headers.set("vary", AGENT_PAGES_VARY);
87
+ return res;
88
+ }
89
+ /** Batteries-included middleware for apps with no other middleware work. */
90
+ export function createAgentPagesMiddleware(opts) {
91
+ return (req) => {
92
+ const decision = decideAgentPages(req, opts.docs, { llms: opts.llms });
93
+ if (decision.kind === "markdown")
94
+ return serveAgentPages(decision);
95
+ const res = NextResponse.next();
96
+ if (decision.negotiated)
97
+ res.headers.set("vary", AGENT_PAGES_VARY);
98
+ return res;
99
+ };
100
+ }
101
+ //# sourceMappingURL=next.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"next.js","sourceRoot":"","sources":["../src/next.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@caprail-dev/agent-pages",
3
+ "version": "0.2.0",
4
+ "description": "Markdown-first pages for AI agents — co-located page.md twins served inline via UA-based content negotiation for Next.js.",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "license": "MIT",
8
+ "files": [
9
+ "dist",
10
+ "README.md"
11
+ ],
12
+ "main": "./dist/core.js",
13
+ "types": "./dist/core.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/core.d.ts",
17
+ "default": "./dist/core.js"
18
+ },
19
+ "./next": {
20
+ "types": "./dist/next.d.ts",
21
+ "default": "./dist/next.js"
22
+ },
23
+ "./config": {
24
+ "types": "./dist/config.d.ts",
25
+ "default": "./dist/config.js"
26
+ }
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "scripts": {
32
+ "build": "tsc -p tsconfig.json",
33
+ "prepublishOnly": "bun run build",
34
+ "test": "bun test"
35
+ },
36
+ "peerDependencies": {
37
+ "next": ">=14"
38
+ },
39
+ "peerDependenciesMeta": {
40
+ "next": {
41
+ "optional": true
42
+ }
43
+ },
44
+ "dependencies": {
45
+ "@caprail-dev/analytics": "^0.5.0"
46
+ }
47
+ }