@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 +141 -0
- package/dist/codegen.d.ts +56 -0
- package/dist/codegen.d.ts.map +1 -0
- package/dist/codegen.js +235 -0
- package/dist/codegen.js.map +1 -0
- package/dist/config.d.ts +28 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +61 -0
- package/dist/config.js.map +1 -0
- package/dist/core.d.ts +82 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +134 -0
- package/dist/core.js.map +1 -0
- package/dist/frontmatter.d.ts +21 -0
- package/dist/frontmatter.d.ts.map +1 -0
- package/dist/frontmatter.js +58 -0
- package/dist/frontmatter.js.map +1 -0
- package/dist/next.d.ts +58 -0
- package/dist/next.d.ts.map +1 -0
- package/dist/next.js +101 -0
- package/dist/next.js.map +1 -0
- package/package.json +47 -0
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"}
|
package/dist/codegen.js
ADDED
|
@@ -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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"}
|
package/dist/config.d.ts
ADDED
|
@@ -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
|
package/dist/core.js.map
ADDED
|
@@ -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
|
package/dist/next.js.map
ADDED
|
@@ -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
|
+
}
|