|
1 | 1 | import type { NextRequest } from 'next/server'; |
2 | 2 | import { NextResponse } from 'next/server'; |
| 3 | +import { appendMarkdownVaryHeader, shouldServeMarkdown } from '@/lib/markdown-negotiation'; |
| 4 | + |
| 5 | +const EXCLUDED_EXACT_PATHS = new Set([ |
| 6 | + '/.well-known/llms.txt', |
| 7 | + '/favicon.ico', |
| 8 | + '/llms-full.txt', |
| 9 | + '/llms.txt', |
| 10 | + '/overview/llms-full.txt', |
| 11 | + '/robots.txt', |
| 12 | + '/sitemap.xml', |
| 13 | +]); |
| 14 | + |
| 15 | +const EXCLUDED_PATH_PREFIXES = ['/_next', '/api', '/llms.mdx', '/og']; |
| 16 | +const EXCLUDED_ASSET_EXTENSIONS = new Set([ |
| 17 | + '.avif', |
| 18 | + '.css', |
| 19 | + '.gif', |
| 20 | + '.ico', |
| 21 | + '.jpeg', |
| 22 | + '.jpg', |
| 23 | + '.js', |
| 24 | + '.json', |
| 25 | + '.map', |
| 26 | + '.otf', |
| 27 | + '.pdf', |
| 28 | + '.png', |
| 29 | + '.svg', |
| 30 | + '.ttf', |
| 31 | + '.txt', |
| 32 | + '.webp', |
| 33 | + '.woff', |
| 34 | + '.woff2', |
| 35 | + '.xml', |
| 36 | + '.zip', |
| 37 | +]); |
3 | 38 |
|
4 | 39 | function isProgrammaticClient(request: NextRequest): boolean { |
5 | 40 | // Browsers always send Sec-Fetch-Dest; curl/WebFetch/python-requests do not |
6 | 41 | return !request.headers.has('sec-fetch-dest'); |
7 | 42 | } |
8 | 43 |
|
| 44 | +function matchesPathPrefix(pathname: string, prefix: string): boolean { |
| 45 | + return pathname === prefix || pathname.startsWith(`${prefix}/`); |
| 46 | +} |
| 47 | + |
| 48 | +function hasExcludedAssetExtension(pathname: string): boolean { |
| 49 | + const extension = pathname.match(/\.[^./]+$/)?.[0]?.toLowerCase(); |
| 50 | + return extension ? EXCLUDED_ASSET_EXTENSIONS.has(extension) : false; |
| 51 | +} |
| 52 | + |
| 53 | +function isNegotiableDocsPath(pathname: string): boolean { |
| 54 | + if (EXCLUDED_EXACT_PATHS.has(pathname)) return false; |
| 55 | + if (EXCLUDED_PATH_PREFIXES.some((prefix) => matchesPathPrefix(pathname, prefix))) return false; |
| 56 | + |
| 57 | + return !hasExcludedAssetExtension(pathname); |
| 58 | +} |
| 59 | + |
| 60 | +function isNegotiableMethod(method: string): boolean { |
| 61 | + return method === 'GET' || method === 'HEAD'; |
| 62 | +} |
| 63 | + |
| 64 | +function withMarkdownVary(response: NextResponse): NextResponse { |
| 65 | + appendMarkdownVaryHeader(response.headers); |
| 66 | + return response; |
| 67 | +} |
| 68 | + |
9 | 69 | export default function middleware(request: NextRequest) { |
10 | 70 | const { pathname } = request.nextUrl; |
11 | 71 |
|
12 | | - if (pathname === '/' && isProgrammaticClient(request)) { |
| 72 | + if (!isNegotiableMethod(request.method)) { |
| 73 | + return NextResponse.next(); |
| 74 | + } |
| 75 | + |
| 76 | + const wantsMarkdown = shouldServeMarkdown(request.headers); |
| 77 | + |
| 78 | + if (pathname === '/' && (wantsMarkdown || isProgrammaticClient(request))) { |
13 | 79 | const redirectUrl = request.nextUrl.clone(); |
14 | 80 | redirectUrl.pathname = '/llms.txt'; |
15 | | - return NextResponse.redirect(redirectUrl); |
| 81 | + return withMarkdownVary(NextResponse.redirect(redirectUrl)); |
| 82 | + } |
| 83 | + |
| 84 | + if (!isNegotiableDocsPath(pathname)) { |
| 85 | + return NextResponse.next(); |
| 86 | + } |
| 87 | + |
| 88 | + if (wantsMarkdown) { |
| 89 | + const rewriteUrl = request.nextUrl.clone(); |
| 90 | + rewriteUrl.pathname = `/llms.mdx${pathname}`; |
| 91 | + return withMarkdownVary(NextResponse.rewrite(rewriteUrl)); |
16 | 92 | } |
17 | 93 |
|
18 | | - return NextResponse.next(); |
| 94 | + return withMarkdownVary(NextResponse.next()); |
19 | 95 | } |
20 | 96 |
|
21 | 97 | export const config = { |
|
0 commit comments