Skip to content

Commit 115f34b

Browse files
committed
feat: add Accept header content negotiation for markdown requests
- Implement hybrid strategy: Accept header + user-agent detection - Support standards-compliant text/markdown and text/x-markdown Accept headers - Maintain backward compatibility with existing user-agent detection - Enhanced logging shows detection method (Accept header, User-agent, Manual) - Inspired by OpenCode's content negotiation approach Examples: - Accept: text/markdown → markdown (standards-compliant) - User-Agent: Claude/1.0 → markdown (fallback for existing tools) - ?format=md → markdown (manual override) This makes docs accessible to more tools while following HTTP standards.
1 parent 471a1e5 commit 115f34b

File tree

1 file changed

+36
-7
lines changed

1 file changed

+36
-7
lines changed

src/middleware.ts

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,18 +54,47 @@ function isAIOrDevTool(userAgent: string): boolean {
5454
return patterns.some(pattern => pattern.test(userAgent));
5555
}
5656

57+
/**
58+
* Detects if client wants markdown via Accept header (standards-compliant)
59+
*/
60+
function wantsMarkdownViaAccept(acceptHeader: string): boolean {
61+
return acceptHeader.includes('text/markdown') ||
62+
acceptHeader.includes('text/x-markdown');
63+
}
64+
65+
/**
66+
* Detects if client wants markdown via Accept header or user-agent
67+
*/
68+
function wantsMarkdown(request: NextRequest): boolean {
69+
const userAgent = request.headers.get('user-agent') || '';
70+
const acceptHeader = request.headers.get('accept') || '';
71+
72+
// Strategy 1: Accept header content negotiation (standards-compliant)
73+
if (wantsMarkdownViaAccept(acceptHeader)) {
74+
return true;
75+
}
76+
77+
// Strategy 2: User-agent detection (fallback for tools that don't set Accept)
78+
return isAIOrDevTool(userAgent);
79+
}
80+
5781
/**
5882
* Handles redirection to markdown versions for AI/LLM clients
5983
*/
6084
const handleAIClientRedirect = (request: NextRequest) => {
6185
const userAgent = request.headers.get('user-agent') || '';
86+
const acceptHeader = request.headers.get('accept') || '';
6287
const url = request.nextUrl;
6388

6489
// Determine if this will be served as markdown
6590
const forceMarkdown = url.searchParams.get('format') === 'md';
66-
const isAIClient = isAIOrDevTool(userAgent);
91+
const clientWantsMarkdown = wantsMarkdown(request);
6792
const willServeMarkdown =
68-
(isAIClient || forceMarkdown) && !url.pathname.endsWith('.md');
93+
(clientWantsMarkdown || forceMarkdown) && !url.pathname.endsWith('.md');
94+
95+
// Determine detection method for logging
96+
const detectionMethod = wantsMarkdownViaAccept(acceptHeader) ? 'Accept header' :
97+
isAIOrDevTool(userAgent) ? 'User-agent' : 'Manual';
6998

7099
// Log user agent for debugging (only for non-static assets)
71100
if (
@@ -74,8 +103,9 @@ const handleAIClientRedirect = (request: NextRequest) => {
74103
!url.pathname.startsWith('/api/')
75104
) {
76105
const contentType = willServeMarkdown ? '📄 MARKDOWN' : '🌐 HTML';
106+
const methodInfo = willServeMarkdown ? ` (${detectionMethod})` : '';
77107
console.log(
78-
`[Middleware] ${url.pathname} - ${contentType} - User-Agent: ${userAgent}`
108+
`[Middleware] ${url.pathname} - ${contentType}${methodInfo} - User-Agent: ${userAgent}`
79109
);
80110
}
81111

@@ -95,12 +125,11 @@ const handleAIClientRedirect = (request: NextRequest) => {
95125
return undefined;
96126
}
97127

98-
// Check for AI client detection
99-
100-
if (isAIClient || forceMarkdown) {
128+
// Check for markdown request (Accept header, user-agent, or manual)
129+
if (clientWantsMarkdown || forceMarkdown) {
101130
// Log the redirect for debugging
102131
console.log(
103-
`[Middleware] Redirecting to markdown: ${isAIClient ? 'AI client detected' : 'Manual format=md'}`
132+
`[Middleware] Redirecting to markdown: ${forceMarkdown ? 'Manual format=md' : detectionMethod}`
104133
);
105134

106135
// Create new URL with .md extension

0 commit comments

Comments
 (0)