Skip to content

Commit 6a036c3

Browse files
KyleAMathewsclaude
andcommitted
Serve markdown to LLMs via Accept header
Add content negotiation to doc routes to serve markdown when Accept: text/markdown or Accept: text/plain is requested. This allows AI coding agents and LLMs to fetch cleaner markdown content instead of HTML, reducing token usage by ~10x. - Add prefersMarkdown() helper to check Accept header - Add ServerRoute handler to both doc routes - Include Vary: Accept header for proper caching 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent eecb250 commit 6a036c3

2 files changed

Lines changed: 116 additions & 0 deletions

File tree

src/routes/$libraryId/$version.docs.$.tsx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,60 @@ import { findLibrary, getBranch, getLibrary } from '~/libraries'
55
import { DocContainer } from '~/components/DocContainer'
66
import { notFound } from '@tanstack/react-router'
77

8+
// Helper function to check if the Accept header prefers markdown
9+
function prefersMarkdown(acceptHeader: string | null): boolean {
10+
if (!acceptHeader) return false
11+
12+
const accepts = acceptHeader.split(',').map(type => {
13+
const [mediaType, ...params] = type.trim().split(';')
14+
const quality = params.find(p => p.trim().startsWith('q='))
15+
const q = quality ? parseFloat(quality.split('=')[1]) : 1.0
16+
return { mediaType: mediaType.toLowerCase(), q }
17+
})
18+
19+
const markdownQ = accepts.find(a =>
20+
a.mediaType === 'text/markdown' || a.mediaType === 'text/plain'
21+
)?.q || 0
22+
23+
const htmlQ = accepts.find(a =>
24+
a.mediaType === 'text/html' || a.mediaType === '*/*'
25+
)?.q || 0
26+
27+
return markdownQ > 0 && markdownQ > htmlQ
28+
}
29+
30+
export const ServerRoute = createServerFileRoute().methods({
31+
GET: async ({ request, params }) => {
32+
const acceptHeader = request.headers.get('Accept')
33+
34+
if (prefersMarkdown(acceptHeader)) {
35+
const url = new URL(request.url)
36+
const { libraryId, version, _splat: docsPath } = params
37+
const library = getLibrary(libraryId)
38+
const root = library.docsRoot || 'docs'
39+
40+
const doc = await loadDocs({
41+
repo: library.repo,
42+
branch: getBranch(library, version),
43+
docsPath: `${root}/${docsPath}`,
44+
currentPath: url.pathname,
45+
redirectPath: `/${library.id}/${version}/docs/overview`,
46+
})
47+
48+
const markdownContent = `# ${doc.title}\n${doc.content}`
49+
50+
return new Response(markdownContent, {
51+
headers: {
52+
'Content-Type': 'text/markdown; charset=utf-8',
53+
'Cache-Control': 'public, max-age=0, must-revalidate',
54+
'Cdn-Cache-Control': 'max-age=300, stale-while-revalidate=300, durable',
55+
'Vary': 'Accept',
56+
},
57+
})
58+
}
59+
},
60+
})
61+
862
export const Route = createFileRoute({
963
staleTime: 1000 * 60 * 5,
1064
loader: (ctx) => {
@@ -43,6 +97,7 @@ export const Route = createFileRoute({
4397
return {
4498
'cache-control': 'public, max-age=0, must-revalidate',
4599
'cdn-cache-control': 'max-age=300, stale-while-revalidate=300, durable',
100+
'vary': 'Accept',
46101
}
47102
},
48103
})

src/routes/$libraryId/$version.docs.framework.$framework.$.tsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,60 @@ import { getBranch, getLibrary } from '~/libraries'
55
import { capitalize } from '~/utils/utils'
66
import { DocContainer } from '~/components/DocContainer'
77

8+
// Helper function to check if the Accept header prefers markdown
9+
function prefersMarkdown(acceptHeader: string | null): boolean {
10+
if (!acceptHeader) return false
11+
12+
const accepts = acceptHeader.split(',').map(type => {
13+
const [mediaType, ...params] = type.trim().split(';')
14+
const quality = params.find(p => p.trim().startsWith('q='))
15+
const q = quality ? parseFloat(quality.split('=')[1]) : 1.0
16+
return { mediaType: mediaType.toLowerCase(), q }
17+
})
18+
19+
const markdownQ = accepts.find(a =>
20+
a.mediaType === 'text/markdown' || a.mediaType === 'text/plain'
21+
)?.q || 0
22+
23+
const htmlQ = accepts.find(a =>
24+
a.mediaType === 'text/html' || a.mediaType === '*/*'
25+
)?.q || 0
26+
27+
return markdownQ > 0 && markdownQ > htmlQ
28+
}
29+
30+
export const ServerRoute = createServerFileRoute().methods({
31+
GET: async ({ request, params }) => {
32+
const acceptHeader = request.headers.get('Accept')
33+
34+
if (prefersMarkdown(acceptHeader)) {
35+
const url = new URL(request.url)
36+
const { libraryId, version, framework, _splat: docsPath } = params
37+
const library = getLibrary(libraryId)
38+
const root = library.docsRoot || 'docs'
39+
40+
const doc = await loadDocs({
41+
repo: library.repo,
42+
branch: getBranch(library, version),
43+
docsPath: `${root}/framework/${framework}/${docsPath}`,
44+
currentPath: url.pathname,
45+
redirectPath: `/${library.id}/${version}/docs/overview`,
46+
})
47+
48+
const markdownContent = `# ${doc.title}\n${doc.content}`
49+
50+
return new Response(markdownContent, {
51+
headers: {
52+
'Content-Type': 'text/markdown; charset=utf-8',
53+
'Cache-Control': 'public, max-age=0, must-revalidate',
54+
'Cdn-Cache-Control': 'max-age=300, stale-while-revalidate=300, durable',
55+
'Vary': 'Accept',
56+
},
57+
})
58+
}
59+
},
60+
})
61+
862
export const Route = createFileRoute({
963
staleTime: 1000 * 60 * 5,
1064
loader: (ctx) => {
@@ -36,6 +90,13 @@ export const Route = createFileRoute({
3690
}),
3791
}
3892
},
93+
headers: (ctx) => {
94+
return {
95+
'cache-control': 'public, max-age=0, must-revalidate',
96+
'cdn-cache-control': 'max-age=300, stale-while-revalidate=300, durable',
97+
'vary': 'Accept',
98+
}
99+
},
39100
})
40101

41102
function Docs() {

0 commit comments

Comments
 (0)