From 37ed1e49b78eb9339e7b62fd2fd5b60d451707d6 Mon Sep 17 00:00:00 2001 From: Dries Augustyns Date: Mon, 20 Apr 2026 17:20:34 +0200 Subject: [PATCH] fix: Implement content negotiation for markdown and html in middleware --- apps/landing/src/middleware.ts | 42 +++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/apps/landing/src/middleware.ts b/apps/landing/src/middleware.ts index 1e1d072..1a89f7b 100644 --- a/apps/landing/src/middleware.ts +++ b/apps/landing/src/middleware.ts @@ -1,22 +1,48 @@ import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; -function acceptsMarkdown(accept: string): boolean { - for (const part of accept.split(',')) { +type Negotiated = 'markdown' | 'html' | 'none'; + +function parseAccept(accept: string): Array<{ type: string; q: number }> { + return accept.split(',').map(part => { const segments = part.trim().split(';'); - if (segments[0]?.trim().toLowerCase() !== 'text/markdown') continue; + const type = segments[0]?.trim().toLowerCase() ?? ''; const qParam = segments.slice(1).find(s => s.trim().startsWith('q=')); - const q = qParam ? parseFloat((qParam.split('=')[1]) ?? '1') : 1; - return !isNaN(q) && q > 0; - } - return false; + const q = qParam ? parseFloat(qParam.split('=')[1] ?? '1') : 1; + return { type, q: isNaN(q) ? 1 : q }; + }); +} + +function getQ(types: Array<{ type: string; q: number }>, target: string): number { + const exact = types.find(t => t.type === target); + if (exact) return exact.q; + const [main] = target.split('/'); + const sub = types.find(t => t.type === `${main}/*`); + if (sub) return sub.q; + const wildcard = types.find(t => t.type === '*/*'); + return wildcard ? wildcard.q : -1; +} + +function negotiate(accept: string): Negotiated { + if (!accept) return 'html'; + const types = parseAccept(accept); + const mdQ = getQ(types, 'text/markdown'); + const htmlQ = getQ(types, 'text/html'); + if (mdQ <= 0 && htmlQ <= 0) return 'none'; + if (mdQ > 0 && mdQ >= htmlQ) return 'markdown'; + return 'html'; } export function middleware(request: NextRequest) { const accept = request.headers.get('accept') ?? ''; const { pathname } = request.nextUrl; + const result = negotiate(accept); - if (acceptsMarkdown(accept)) { + if (result === 'none') { + return new NextResponse(null, { status: 406, headers: { 'Vary': 'Accept' } }); + } + + if (result === 'markdown') { const headers = new Headers(request.headers); headers.set('x-md-path', pathname); const response = NextResponse.rewrite(new URL('/api/md', request.url), { request: { headers } });