@vercel/agent-readability 0.3.0 → 0.4.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.
@@ -0,0 +1,37 @@
1
+ import { Handle } from '@sveltejs/kit';
2
+
3
+ type DetectionMethod = "ua-match" | "signature-agent" | "heuristic";
4
+ /** Info passed to onDetection callbacks across all adapters. */
5
+ type OnDetectionInfo = {
6
+ path: string;
7
+ method: DetectionMethod | "accept-header";
8
+ userAgent: string | null;
9
+ };
10
+ /** Fire-and-forget callback shared by Next.js, SvelteKit, and Nuxt adapters. */
11
+ type OnDetectionCallback = (info: OnDetectionInfo) => void | Promise<void>;
12
+
13
+ interface SvelteKitAgentReadabilityOptions {
14
+ /** URL prefix to intercept. Default: '/docs' */
15
+ docsPrefix?: string;
16
+ /** Maps request path to internal +server.ts route returning markdown */
17
+ rewrite: (pathname: string) => string;
18
+ /** Fire-and-forget callback for analytics/logging. */
19
+ onDetection?: OnDetectionCallback;
20
+ }
21
+ /**
22
+ * SvelteKit handle hook that detects AI agents and rewrites to markdown routes.
23
+ *
24
+ * ```ts
25
+ * // hooks.server.ts
26
+ * import { handleAgentReadability } from '@vercel/agent-readability/sveltekit'
27
+ * import { sequence } from '@sveltejs/kit/hooks'
28
+ *
29
+ * export const handle = sequence(
30
+ * handleAgentReadability({ rewrite: (p) => `/api/docs-md${p}` }),
31
+ * // other handles...
32
+ * )
33
+ * ```
34
+ */
35
+ declare function handleAgentReadability(options: SvelteKitAgentReadabilityOptions): Handle;
36
+
37
+ export { type SvelteKitAgentReadabilityOptions, handleAgentReadability };
@@ -0,0 +1,37 @@
1
+ import { Handle } from '@sveltejs/kit';
2
+
3
+ type DetectionMethod = "ua-match" | "signature-agent" | "heuristic";
4
+ /** Info passed to onDetection callbacks across all adapters. */
5
+ type OnDetectionInfo = {
6
+ path: string;
7
+ method: DetectionMethod | "accept-header";
8
+ userAgent: string | null;
9
+ };
10
+ /** Fire-and-forget callback shared by Next.js, SvelteKit, and Nuxt adapters. */
11
+ type OnDetectionCallback = (info: OnDetectionInfo) => void | Promise<void>;
12
+
13
+ interface SvelteKitAgentReadabilityOptions {
14
+ /** URL prefix to intercept. Default: '/docs' */
15
+ docsPrefix?: string;
16
+ /** Maps request path to internal +server.ts route returning markdown */
17
+ rewrite: (pathname: string) => string;
18
+ /** Fire-and-forget callback for analytics/logging. */
19
+ onDetection?: OnDetectionCallback;
20
+ }
21
+ /**
22
+ * SvelteKit handle hook that detects AI agents and rewrites to markdown routes.
23
+ *
24
+ * ```ts
25
+ * // hooks.server.ts
26
+ * import { handleAgentReadability } from '@vercel/agent-readability/sveltekit'
27
+ * import { sequence } from '@sveltejs/kit/hooks'
28
+ *
29
+ * export const handle = sequence(
30
+ * handleAgentReadability({ rewrite: (p) => `/api/docs-md${p}` }),
31
+ * // other handles...
32
+ * )
33
+ * ```
34
+ */
35
+ declare function handleAgentReadability(options: SvelteKitAgentReadabilityOptions): Handle;
36
+
37
+ export { type SvelteKitAgentReadabilityOptions, handleAgentReadability };
@@ -0,0 +1,151 @@
1
+ // src/patterns.ts
2
+ var AI_AGENT_UA_PATTERNS = [
3
+ // Anthropic — https://support.claude.com/en/articles/8896518
4
+ "claudebot",
5
+ "claude-searchbot",
6
+ "claude-user",
7
+ "anthropic-ai",
8
+ "claude-web",
9
+ // OpenAI — https://platform.openai.com/docs/bots
10
+ "chatgpt",
11
+ "gptbot",
12
+ "oai-searchbot",
13
+ "openai",
14
+ // Google AI
15
+ "gemini",
16
+ "bard",
17
+ "google-cloudvertexbot",
18
+ "google-extended",
19
+ // Meta
20
+ "meta-externalagent",
21
+ "meta-externalfetcher",
22
+ "meta-webindexer",
23
+ // Search/Research AI
24
+ "perplexity",
25
+ "youbot",
26
+ "you.com",
27
+ "deepseekbot",
28
+ // Coding assistants
29
+ "cursor",
30
+ "github-copilot",
31
+ "codeium",
32
+ "tabnine",
33
+ "sourcegraph",
34
+ // Other AI agents / data scrapers
35
+ "cohere-ai",
36
+ "bytespider",
37
+ "amazonbot",
38
+ "ai2bot",
39
+ "diffbot",
40
+ "omgili",
41
+ "omgilibot"
42
+ ];
43
+ var SIGNATURE_AGENT_DOMAINS = ["chatgpt.com"];
44
+ var TRADITIONAL_BOT_PATTERNS = [
45
+ "googlebot",
46
+ "bingbot",
47
+ "yandexbot",
48
+ "baiduspider",
49
+ "duckduckbot",
50
+ "slurp",
51
+ "msnbot",
52
+ "facebot",
53
+ "twitterbot",
54
+ "linkedinbot",
55
+ "whatsapp",
56
+ "telegrambot",
57
+ "pingdom",
58
+ "uptimerobot",
59
+ "newrelic",
60
+ "datadog",
61
+ "statuspage",
62
+ "site24x7",
63
+ "applebot"
64
+ ];
65
+ var BOT_LIKE_REGEX = /bot|agent|fetch|crawl|spider|search/i;
66
+
67
+ // src/detection.ts
68
+ function isAIAgent(request) {
69
+ const userAgent = request.headers.get("user-agent");
70
+ const lowerUA = userAgent?.toLowerCase() ?? "";
71
+ if (lowerUA && AI_AGENT_UA_PATTERNS.some((pattern) => lowerUA.includes(pattern))) {
72
+ return { detected: true, method: "ua-match" };
73
+ }
74
+ const signatureAgent = request.headers.get("signature-agent");
75
+ if (signatureAgent) {
76
+ const lowerSig = signatureAgent.toLowerCase();
77
+ if (SIGNATURE_AGENT_DOMAINS.some((domain) => lowerSig.includes(domain))) {
78
+ return { detected: true, method: "signature-agent" };
79
+ }
80
+ }
81
+ const secFetchMode = request.headers.get("sec-fetch-mode");
82
+ if (!secFetchMode && lowerUA && BOT_LIKE_REGEX.test(lowerUA)) {
83
+ const isTraditionalBot = TRADITIONAL_BOT_PATTERNS.some((pattern) => lowerUA.includes(pattern));
84
+ if (!isTraditionalBot) {
85
+ return { detected: true, method: "heuristic" };
86
+ }
87
+ }
88
+ return { detected: false, method: null };
89
+ }
90
+
91
+ // src/negotiation.ts
92
+ var DEFAULT_MARKDOWN_TYPES = ["text/markdown", "text/x-markdown"];
93
+ function acceptsMarkdown(request, options) {
94
+ const accept = request.headers.get("accept");
95
+ if (!accept) return false;
96
+ const types = options?.mediaTypes ?? DEFAULT_MARKDOWN_TYPES;
97
+ const lowerAccept = accept.toLowerCase();
98
+ return types.some((type) => lowerAccept.includes(type));
99
+ }
100
+ function shouldServeMarkdown(request, options) {
101
+ const detection = isAIAgent(request);
102
+ if (detection.detected) {
103
+ return { serve: true, reason: "agent", detection };
104
+ }
105
+ if (acceptsMarkdown(request, options)) {
106
+ return { serve: true, reason: "accept-header", detection };
107
+ }
108
+ return { serve: false, reason: null, detection };
109
+ }
110
+
111
+ // src/sveltekit/index.ts
112
+ function handleAgentReadability(options) {
113
+ return async ({ event, resolve }) => {
114
+ const { pathname } = event.url;
115
+ const prefix = options.docsPrefix ?? "/docs";
116
+ if (!pathname.startsWith(prefix) || event.isDataRequest || event.isSubRequest) {
117
+ return resolve(event);
118
+ }
119
+ const { serve, detection } = shouldServeMarkdown(event.request);
120
+ if (!serve) return resolve(event);
121
+ if (options.onDetection) {
122
+ try {
123
+ const method = detection.detected ? detection.method : "accept-header";
124
+ const p = options.onDetection({
125
+ path: pathname,
126
+ method,
127
+ userAgent: event.request.headers.get("user-agent")
128
+ });
129
+ if (p instanceof Promise) p.catch(() => {
130
+ });
131
+ } catch {
132
+ }
133
+ }
134
+ const response = await event.fetch(options.rewrite(pathname));
135
+ if (!response.ok) return resolve(event);
136
+ const headers = new Headers(response.headers);
137
+ const varyTokens = (headers.get("vary") ?? "").toLowerCase().split(/\s*,\s*/);
138
+ if (!varyTokens.includes("accept")) {
139
+ headers.append("vary", "Accept");
140
+ }
141
+ return new Response(response.body, {
142
+ status: response.status,
143
+ statusText: response.statusText,
144
+ headers
145
+ });
146
+ };
147
+ }
148
+ export {
149
+ handleAgentReadability
150
+ };
151
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/patterns.ts","../../src/detection.ts","../../src/negotiation.ts","../../src/sveltekit/index.ts"],"sourcesContent":["/**\n * Layer 1: Known AI agent UA substrings (lowercase).\n * Curated from https://bots.fyi/?tags=ai_assistant + official vendor docs.\n * Last reviewed: 2026-03-20\n */\nexport const AI_AGENT_UA_PATTERNS: readonly string[] = [\n\t// Anthropic — https://support.claude.com/en/articles/8896518\n\t\"claudebot\",\n\t\"claude-searchbot\",\n\t\"claude-user\",\n\t\"anthropic-ai\",\n\t\"claude-web\",\n\n\t// OpenAI — https://platform.openai.com/docs/bots\n\t\"chatgpt\",\n\t\"gptbot\",\n\t\"oai-searchbot\",\n\t\"openai\",\n\n\t// Google AI\n\t\"gemini\",\n\t\"bard\",\n\t\"google-cloudvertexbot\",\n\t\"google-extended\",\n\n\t// Meta\n\t\"meta-externalagent\",\n\t\"meta-externalfetcher\",\n\t\"meta-webindexer\",\n\n\t// Search/Research AI\n\t\"perplexity\",\n\t\"youbot\",\n\t\"you.com\",\n\t\"deepseekbot\",\n\n\t// Coding assistants\n\t\"cursor\",\n\t\"github-copilot\",\n\t\"codeium\",\n\t\"tabnine\",\n\t\"sourcegraph\",\n\n\t// Other AI agents / data scrapers\n\t\"cohere-ai\",\n\t\"bytespider\",\n\t\"amazonbot\",\n\t\"ai2bot\",\n\t\"diffbot\",\n\t\"omgili\",\n\t\"omgilibot\",\n];\n\n/**\n * Layer 2: Known AI service URLs in Signature-Agent header (RFC 9421).\n */\nexport const SIGNATURE_AGENT_DOMAINS: readonly string[] = [\"chatgpt.com\"];\n\n/**\n * Layer 3: Traditional bot exclusion list. Bots that should NOT trigger the\n * heuristic layer (search engine crawlers, social previews, monitoring tools).\n */\nexport const TRADITIONAL_BOT_PATTERNS: readonly string[] = [\n\t\"googlebot\",\n\t\"bingbot\",\n\t\"yandexbot\",\n\t\"baiduspider\",\n\t\"duckduckbot\",\n\t\"slurp\",\n\t\"msnbot\",\n\t\"facebot\",\n\t\"twitterbot\",\n\t\"linkedinbot\",\n\t\"whatsapp\",\n\t\"telegrambot\",\n\t\"pingdom\",\n\t\"uptimerobot\",\n\t\"newrelic\",\n\t\"datadog\",\n\t\"statuspage\",\n\t\"site24x7\",\n\t\"applebot\",\n];\n\n/**\n * Broad regex for bot-like UA strings (used only in Layer 3 heuristic).\n * No word boundaries — keywords commonly appear in compound names.\n */\nexport const BOT_LIKE_REGEX: RegExp = /bot|agent|fetch|crawl|spider|search/i;\n","import {\n\tAI_AGENT_UA_PATTERNS,\n\tBOT_LIKE_REGEX,\n\tSIGNATURE_AGENT_DOMAINS,\n\tTRADITIONAL_BOT_PATTERNS,\n} from \"./patterns\";\nimport type { DetectionResult, MinimalRequest } from \"./types\";\n\n/**\n * Detects AI agents from HTTP request headers.\n *\n * Three detection layers (checked in order):\n * 1. Known UA patterns (definitive)\n * 2. Signature-Agent header (definitive, RFC 9421)\n * 3. Missing sec-fetch-mode heuristic (catches unknown bots)\n *\n * Optimizes for recall over precision: serving markdown to a non-AI bot\n * is low-harm; missing an AI agent means a worse experience.\n */\nexport function isAIAgent(request: MinimalRequest): DetectionResult {\n\tconst userAgent = request.headers.get(\"user-agent\");\n\tconst lowerUA = userAgent?.toLowerCase() ?? \"\";\n\n\t// Layer 1: Known UA pattern match\n\tif (lowerUA && AI_AGENT_UA_PATTERNS.some((pattern) => lowerUA.includes(pattern))) {\n\t\treturn { detected: true, method: \"ua-match\" };\n\t}\n\n\t// Layer 2: Signature-Agent header (RFC 9421, used by ChatGPT agent)\n\tconst signatureAgent = request.headers.get(\"signature-agent\");\n\tif (signatureAgent) {\n\t\tconst lowerSig = signatureAgent.toLowerCase();\n\t\tif (SIGNATURE_AGENT_DOMAINS.some((domain) => lowerSig.includes(domain))) {\n\t\t\treturn { detected: true, method: \"signature-agent\" };\n\t\t}\n\t}\n\n\t// Layer 3: Missing browser fingerprint heuristic\n\t// Real browsers (Chrome 76+, Firefox 90+, Safari 16.4+) send sec-fetch-mode\n\t// on navigation requests. Its absence signals a programmatic client.\n\tconst secFetchMode = request.headers.get(\"sec-fetch-mode\");\n\tif (!secFetchMode && lowerUA && BOT_LIKE_REGEX.test(lowerUA)) {\n\t\tconst isTraditionalBot = TRADITIONAL_BOT_PATTERNS.some((pattern) => lowerUA.includes(pattern));\n\t\tif (!isTraditionalBot) {\n\t\t\treturn { detected: true, method: \"heuristic\" };\n\t\t}\n\t}\n\n\treturn { detected: false, method: null };\n}\n","import { isAIAgent } from \"./detection\";\nimport type { DetectionResult, MinimalRequest } from \"./types\";\n\nconst DEFAULT_MARKDOWN_TYPES = [\"text/markdown\", \"text/x-markdown\"];\n\nexport interface AcceptMarkdownOptions {\n\tmediaTypes?: string[];\n}\n\n/**\n * Check if the request prefers markdown via the Accept header.\n */\nexport function acceptsMarkdown(request: MinimalRequest, options?: AcceptMarkdownOptions): boolean {\n\tconst accept = request.headers.get(\"accept\");\n\tif (!accept) return false;\n\n\tconst types = options?.mediaTypes ?? DEFAULT_MARKDOWN_TYPES;\n\tconst lowerAccept = accept.toLowerCase();\n\treturn types.some((type) => lowerAccept.includes(type));\n}\n\nexport interface ShouldServeMarkdownResult {\n\tserve: boolean;\n\treason: \"agent\" | \"accept-header\" | null;\n\tdetection: DetectionResult;\n}\n\n/**\n * Combines agent detection and content negotiation into one call.\n * Returns whether to serve markdown and why.\n */\nexport function shouldServeMarkdown(\n\trequest: MinimalRequest,\n\toptions?: AcceptMarkdownOptions,\n): ShouldServeMarkdownResult {\n\tconst detection = isAIAgent(request);\n\tif (detection.detected) {\n\t\treturn { serve: true, reason: \"agent\", detection };\n\t}\n\n\tif (acceptsMarkdown(request, options)) {\n\t\treturn { serve: true, reason: \"accept-header\", detection };\n\t}\n\n\treturn { serve: false, reason: null, detection };\n}\n","import type { Handle } from \"@sveltejs/kit\";\nimport { shouldServeMarkdown } from \"../negotiation\";\nimport type { OnDetectionCallback } from \"../types\";\n\nexport interface SvelteKitAgentReadabilityOptions {\n\t/** URL prefix to intercept. Default: '/docs' */\n\tdocsPrefix?: string;\n\t/** Maps request path to internal +server.ts route returning markdown */\n\trewrite: (pathname: string) => string;\n\t/** Fire-and-forget callback for analytics/logging. */\n\tonDetection?: OnDetectionCallback;\n}\n\n/**\n * SvelteKit handle hook that detects AI agents and rewrites to markdown routes.\n *\n * ```ts\n * // hooks.server.ts\n * import { handleAgentReadability } from '@vercel/agent-readability/sveltekit'\n * import { sequence } from '@sveltejs/kit/hooks'\n *\n * export const handle = sequence(\n * handleAgentReadability({ rewrite: (p) => `/api/docs-md${p}` }),\n * // other handles...\n * )\n * ```\n */\nexport function handleAgentReadability(options: SvelteKitAgentReadabilityOptions): Handle {\n\treturn async ({ event, resolve }) => {\n\t\tconst { pathname } = event.url;\n\t\tconst prefix = options.docsPrefix ?? \"/docs\";\n\n\t\t// Skip: outside prefix, data requests (__data.json), sub-requests (prevent loop)\n\t\tif (!pathname.startsWith(prefix) || event.isDataRequest || event.isSubRequest) {\n\t\t\treturn resolve(event);\n\t\t}\n\n\t\tconst { serve, detection } = shouldServeMarkdown(event.request);\n\t\tif (!serve) return resolve(event);\n\n\t\t// Fire-and-forget — never block the response\n\t\tif (options.onDetection) {\n\t\t\ttry {\n\t\t\t\tconst method = detection.detected ? detection.method : \"accept-header\";\n\t\t\t\tconst p = options.onDetection({\n\t\t\t\t\tpath: pathname,\n\t\t\t\t\tmethod,\n\t\t\t\t\tuserAgent: event.request.headers.get(\"user-agent\"),\n\t\t\t\t});\n\t\t\t\tif (p instanceof Promise) p.catch(() => {});\n\t\t\t} catch {\n\t\t\t\t/* swallow sync errors */\n\t\t\t}\n\t\t}\n\n\t\t// Internal fetch — zero network hop via SvelteKit's event.fetch\n\t\tconst response = await event.fetch(options.rewrite(pathname));\n\t\tif (!response.ok) return resolve(event);\n\n\t\t// Ensure Vary: Accept for correct CDN caching (token match, not substring)\n\t\tconst headers = new Headers(response.headers);\n\t\tconst varyTokens = (headers.get(\"vary\") ?? \"\").toLowerCase().split(/\\s*,\\s*/);\n\t\tif (!varyTokens.includes(\"accept\")) {\n\t\t\theaders.append(\"vary\", \"Accept\");\n\t\t}\n\n\t\treturn new Response(response.body, {\n\t\t\tstatus: response.status,\n\t\t\tstatusText: response.statusText,\n\t\t\theaders,\n\t\t});\n\t};\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/agent-readability",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Detect AI agents. Serve them markdown. Audit your site against the Agent Readability Spec.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -27,6 +27,26 @@
27
27
  "types": "./dist/next/index.d.cts",
28
28
  "default": "./dist/next/index.cjs"
29
29
  }
30
+ },
31
+ "./sveltekit": {
32
+ "import": {
33
+ "types": "./dist/sveltekit/index.d.ts",
34
+ "default": "./dist/sveltekit/index.js"
35
+ },
36
+ "require": {
37
+ "types": "./dist/sveltekit/index.d.cts",
38
+ "default": "./dist/sveltekit/index.cjs"
39
+ }
40
+ },
41
+ "./nuxt": {
42
+ "import": {
43
+ "types": "./dist/nuxt/index.d.ts",
44
+ "default": "./dist/nuxt/index.js"
45
+ },
46
+ "require": {
47
+ "types": "./dist/nuxt/index.d.cts",
48
+ "default": "./dist/nuxt/index.cjs"
49
+ }
30
50
  }
31
51
  },
32
52
  "bin": {
@@ -40,9 +60,17 @@
40
60
  "node": ">=20.0.0"
41
61
  },
42
62
  "peerDependencies": {
63
+ "@sveltejs/kit": ">=2",
64
+ "h3": ">=1.8 <2",
43
65
  "next": ">=14"
44
66
  },
45
67
  "peerDependenciesMeta": {
68
+ "@sveltejs/kit": {
69
+ "optional": true
70
+ },
71
+ "h3": {
72
+ "optional": true
73
+ },
46
74
  "next": {
47
75
  "optional": true
48
76
  }
@@ -50,13 +78,16 @@
50
78
  "devDependencies": {
51
79
  "@biomejs/biome": "^1.9",
52
80
  "@changesets/cli": "^2",
81
+ "@sveltejs/kit": "^2",
53
82
  "@types/node": "^22",
54
83
  "@vitest/coverage-v8": "^3",
55
84
  "cheerio": "^1",
56
85
  "citty": "^0.1",
86
+ "h3": ">=1.8 <2",
57
87
  "next": "^15",
58
88
  "p-limit": "^6",
59
89
  "picocolors": "^1",
90
+ "svelte": "^5",
60
91
  "tsup": "^8",
61
92
  "typescript": "^5.8",
62
93
  "vitest": "^3"
@@ -76,6 +107,8 @@
76
107
  "markdown",
77
108
  "detection",
78
109
  "next.js",
110
+ "sveltekit",
111
+ "nuxt",
79
112
  "middleware"
80
113
  ],
81
114
  "scripts": {
package/skill/SKILL.md CHANGED
@@ -23,9 +23,11 @@ Make a website readable by AI agents by adding discovery files, content negotiat
23
23
  pnpm add @vercel/agent-readability
24
24
  ```
25
25
 
26
- ### 2. Add middleware (Next.js)
26
+ ### 2. Add middleware
27
27
 
28
- Create or update `middleware.ts`:
28
+ Detect framework: `next.config.*` → Next.js, `svelte.config.js` → SvelteKit, `nuxt.config.ts` → Nuxt.
29
+
30
+ #### Next.js — `middleware.ts`
29
31
 
30
32
  ```ts
31
33
  import { withAgentReadability } from '@vercel/agent-readability/next'
@@ -52,13 +54,46 @@ export default withAgentReadability(
52
54
  )
53
55
  ```
54
56
 
55
- For non-Next.js frameworks, use the core API directly:
57
+ #### SvelteKit `hooks.server.ts`
58
+
59
+ ```ts
60
+ import { handleAgentReadability } from '@vercel/agent-readability/sveltekit'
61
+ import { sequence } from '@sveltejs/kit/hooks'
62
+
63
+ export const handle = sequence(
64
+ handleAgentReadability({
65
+ docsPrefix: '/docs',
66
+ rewrite: (pathname) => `/api/docs-md${pathname}`,
67
+ }),
68
+ // other handles...
69
+ )
70
+ ```
71
+
72
+ The `rewrite` function maps to a `+server.ts` route that returns markdown. SvelteKit's `event.fetch()` resolves it internally with zero network hop.
73
+
74
+ #### Nuxt — `server/middleware/agent.ts`
75
+
76
+ ```ts
77
+ import { defineAgentMiddleware } from '@vercel/agent-readability/nuxt'
78
+
79
+ export default defineAgentMiddleware({
80
+ docsPrefix: '/docs',
81
+ getMarkdown: async (pathname, event) => {
82
+ const doc = await queryContent(pathname).findOne()
83
+ return doc.body
84
+ },
85
+ })
86
+ ```
87
+
88
+ `getMarkdown` receives the pathname and h3 event. Return a string (auto-wrapped in `text/markdown` Response) or a full `Response`.
89
+
90
+ #### Other frameworks — core API
56
91
 
57
92
  ```ts
58
- import { isAIAgent, acceptsMarkdown } from '@vercel/agent-readability'
93
+ import { shouldServeMarkdown } from '@vercel/agent-readability'
59
94
 
60
- // In your request handler:
61
- if (isAIAgent(request).detected || acceptsMarkdown(request)) {
95
+ const { serve } = shouldServeMarkdown(request)
96
+ if (serve) {
62
97
  return new Response(markdownContent, {
63
98
  headers: { 'Content-Type': 'text/markdown', 'Vary': 'Accept' },
64
99
  })
@@ -129,7 +164,7 @@ Add JSON-LD to pages with Schema.org types (Article, TechArticle, WebPage):
129
164
  Add to `<head>` on all pages that have markdown versions:
130
165
 
131
166
  ```html
132
- <link rel="alternate" type="text/markdown" href="/docs/page.md">
167
+ <link rel="alternate" type="text/markdown" href="/docs/page">
133
168
  ```
134
169
 
135
170
  ### 7. Add frontmatter to markdown responses
@@ -167,5 +202,8 @@ The audit scores your site on 25 checks across reachability, discovery, content
167
202
  ## Notes
168
203
 
169
204
  - Set `Vary: Accept` on all markdown responses for correct CDN caching
170
- - For missing pages, return 200 with markdown body (agents discard 404 bodies)
171
- - The `onDetection` callback in the Next.js adapter runs via `event.waitUntil()`
205
+ - For missing pages, return 200 with markdown body on the canonical URL (agents discard 404 bodies)
206
+ - Prefer canonical page URLs and negotiate markdown with `Accept: text/markdown` instead of exposing `.md` page URLs
207
+ - Next.js: `onDetection` runs via `event.waitUntil()`
208
+ - SvelteKit: `onDetection` is fire-and-forget (errors swallowed)
209
+ - Nuxt: `onDetection` is fire-and-forget (errors swallowed)