11import { NextRequest , NextResponse } from "next/server" ;
22import { applyAgentDiscoveryHeaders } from "@/server/agent-discovery" ;
3+ import { negotiateContentType } from "@/server/content-negotiation" ;
34
45/**
56 * Known LLM/AI user agent patterns
@@ -28,13 +29,18 @@ function isLLMUserAgent(userAgent: string | null): boolean {
2829 ) ;
2930}
3031
32+ function withVaryAccept ( response : NextResponse ) : NextResponse {
33+ response . headers . set ( "Vary" , "Accept" ) ;
34+ return response ;
35+ }
36+
3137/**
3238 * Proxy to serve raw markdown for AI agents
3339 *
3440 * Routes to raw markdown API when:
3541 * 1. URL ends with .md extension (e.g., /setup.md)
36- * 2. Accept header includes text/markdown
37- * 3. Accept header includes text/plain
42+ * 2. Non-root Accept negotiation selects text/markdown
43+ * 3. Non-root Accept negotiation selects text/plain
3844 * 4. Query param ?format=md is present
3945 * 5. User-Agent is a known LLM (ChatGPT, GPTBot, PerplexityBot, etc.)
4046 *
@@ -57,15 +63,19 @@ export function proxy(request: NextRequest) {
5763 }
5864
5965 const userAgent = request . headers . get ( "User-Agent" ) ;
60-
61- // Check if this is a docs page request that wants markdown
62- const wantsMarkdown =
66+ const acceptHeader = request . headers . get ( "Accept" ) ;
67+ const negotiatedType = negotiateContentType ( acceptHeader ) ;
68+ const hasExplicitMarkdownCue =
6369 pathname . endsWith ( ".md" ) ||
64- request . headers . get ( "Accept" ) ?. includes ( "text/markdown" ) ||
65- request . headers . get ( "Accept" ) ?. includes ( "text/plain" ) ||
6670 request . nextUrl . searchParams . get ( "format" ) === "md" ||
6771 isLLMUserAgent ( userAgent ) ;
6872
73+ // Check if this is a docs page request that wants markdown
74+ const wantsMarkdown =
75+ hasExplicitMarkdownCue ||
76+ ( pathname !== "/" &&
77+ ( negotiatedType === "text/markdown" || negotiatedType === "text/plain" ) ) ;
78+
6979 if ( wantsMarkdown ) {
7080 // Normalize the path (remove .md extension if present)
7181 let docPath = pathname . replace ( / \. m d $ / , "" ) ;
@@ -81,10 +91,31 @@ export function proxy(request: NextRequest) {
8191 url . searchParams . delete ( "format" ) ;
8292
8393 // Rewrite to the API route (internal redirect, URL doesn't change for client)
84- return applyAgentDiscoveryHeaders ( NextResponse . rewrite ( url ) ) ;
94+ return withVaryAccept ( applyAgentDiscoveryHeaders ( NextResponse . rewrite ( url ) ) ) ;
95+ }
96+
97+ if ( pathname === "/" && negotiatedType !== "text/html" ) {
98+ const url = new URL ( request . url ) ;
99+ url . pathname = "/api/agentgrade/root" ;
100+ url . searchParams . set ( "type" , negotiatedType ) ;
101+ return withVaryAccept ( applyAgentDiscoveryHeaders ( NextResponse . rewrite ( url ) ) ) ;
102+ }
103+
104+ if ( pathname !== "/" && negotiatedType === "application/json" ) {
105+ return withVaryAccept (
106+ applyAgentDiscoveryHeaders (
107+ NextResponse . json (
108+ {
109+ error : "not_found" ,
110+ path : pathname ,
111+ } ,
112+ { status : 404 }
113+ )
114+ )
115+ ) ;
85116 }
86117
87- return applyAgentDiscoveryHeaders ( NextResponse . next ( ) ) ;
118+ return withVaryAccept ( applyAgentDiscoveryHeaders ( NextResponse . next ( ) ) ) ;
88119}
89120
90121// Only run proxy on relevant paths
0 commit comments