diff --git a/src/middleware.ts b/src/middleware.ts index d84e86501bf50..fb21ff92c9e5a 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -19,12 +19,144 @@ export const config = { // This function can be marked `async` if using `await` inside export function middleware(request: NextRequest) { - return handleRedirects(request); + // First, handle canonical URL redirects for deprecated paths + const canonicalRedirect = handleRedirects(request); + if (canonicalRedirect) { + return canonicalRedirect; + } + + // Then, check for AI/LLM clients and redirect to markdown if appropriate + return handleAIClientRedirect(request); } // don't send Permanent Redirects (301) in dev mode - it gets cached for "localhost" by the browser const redirectStatusCode = process.env.NODE_ENV === 'development' ? 302 : 301; +/** + * Detects if the user agent belongs to an AI/LLM tool or development environment + * that would benefit from markdown format + */ +function isAIOrDevTool(userAgent: string): boolean { + const patterns = [ + /claude/i, // Claude Desktop/Code + /cursor/i, // Cursor IDE + /copilot/i, // GitHub Copilot + /chatgpt/i, // ChatGPT + /openai/i, // OpenAI tools + /anthropic/i, // Anthropic tools + /vscode/i, // VS Code extensions + /intellij/i, // IntelliJ plugins + /sublime/i, // Sublime Text plugins + /got/i, // Got HTTP library (sindresorhus/got) + // Add more patterns as needed + ]; + + return patterns.some(pattern => pattern.test(userAgent)); +} + +/** + * Detects if client wants markdown via Accept header (standards-compliant) + */ +function wantsMarkdownViaAccept(acceptHeader: string): boolean { + return ( + acceptHeader.includes('text/markdown') || acceptHeader.includes('text/x-markdown') + ); +} + +/** + * Detects if client wants markdown via Accept header or user-agent + */ +function wantsMarkdown(request: NextRequest): boolean { + const userAgent = request.headers.get('user-agent') || ''; + const acceptHeader = request.headers.get('accept') || ''; + + // Strategy 1: Accept header content negotiation (standards-compliant) + if (wantsMarkdownViaAccept(acceptHeader)) { + return true; + } + + // Strategy 2: User-agent detection (fallback for tools that don't set Accept) + return isAIOrDevTool(userAgent); +} + +/** + * Handles redirection to markdown versions for AI/LLM clients + */ +const handleAIClientRedirect = (request: NextRequest) => { + const userAgent = request.headers.get('user-agent') || ''; + const acceptHeader = request.headers.get('accept') || ''; + const url = request.nextUrl; + + // Determine if this will be served as markdown + const forceMarkdown = url.searchParams.get('format') === 'md'; + const clientWantsMarkdown = wantsMarkdown(request); + const willServeMarkdown = + (clientWantsMarkdown || forceMarkdown) && !url.pathname.endsWith('.md'); + + // Determine detection method for logging + const detectionMethod = wantsMarkdownViaAccept(acceptHeader) + ? 'Accept header' + : isAIOrDevTool(userAgent) + ? 'User-agent' + : 'Manual'; + + // Log user agent for debugging (only for non-static assets) + if ( + !url.pathname.startsWith('/_next/') && + !url.pathname.includes('.') && + !url.pathname.startsWith('/api/') + ) { + const contentType = willServeMarkdown ? '📄 MARKDOWN' : '🌐 HTML'; + const methodInfo = willServeMarkdown ? ` (${detectionMethod})` : ''; + console.log( + `[Middleware] ${url.pathname} - ${contentType}${methodInfo} - User-Agent: ${userAgent}` + ); + } + + // Skip if already requesting a markdown file + if (url.pathname.endsWith('.md')) { + return undefined; + } + + // Skip API routes and static assets (should already be filtered by matcher) + if ( + url.pathname.startsWith('/api/') || + url.pathname.startsWith('/_next/') || + /\.(js|json|png|jpg|jpeg|gif|ico|pdf|css|woff|woff2|ttf|map|xml|txt|zip|svg)$/i.test( + url.pathname + ) + ) { + return undefined; + } + + // Check for markdown request (Accept header, user-agent, or manual) + if (clientWantsMarkdown || forceMarkdown) { + // Log the redirect for debugging + console.log( + `[Middleware] Redirecting to markdown: ${forceMarkdown ? 'Manual format=md' : detectionMethod}` + ); + + // Create new URL with .md extension + const newUrl = url.clone(); + // Handle root path and ensure proper .md extension + let pathname = url.pathname === '/' ? '/index' : url.pathname; + // Remove all trailing slashes if present, then add .md + pathname = pathname.replace(/\/+$/, '') + '.md'; + newUrl.pathname = pathname; + + // Clean up the format query parameter if it was used + if (forceMarkdown) { + newUrl.searchParams.delete('format'); + } + + return NextResponse.redirect(newUrl, { + status: redirectStatusCode, + }); + } + + return undefined; +}; + const handleRedirects = (request: NextRequest) => { const urlPath = request.nextUrl.pathname;