From 7cf37f72f1e3d387cf188eebb16f6fe2e1467b27 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 15 Jan 2026 03:56:52 +0000 Subject: [PATCH 1/2] feat: add markdown content negotiation support When a request includes Accept: text/markdown header, return the raw markdown content instead of the rendered HTML page. - Add middleware.ts to intercept requests with Accept: text/markdown - Add /api/markdown/[...slug] API route to serve raw markdown content - Support both text/markdown and text/x-markdown MIME types - Skip API routes, static files, and special paths in middleware Co-authored-by: chris --- middleware.ts | 55 ++++++++++++++++++++++++++++++ pages/api/markdown/[...slug].ts | 60 +++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 middleware.ts create mode 100644 pages/api/markdown/[...slug].ts diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 000000000..a853abbc9 --- /dev/null +++ b/middleware.ts @@ -0,0 +1,55 @@ +import { NextRequest, NextResponse } from "next/server"; + +/** + * Middleware that handles content negotiation for markdown requests. + * When a request includes Accept: text/markdown header, it rewrites + * the request to the markdown API endpoint to return raw markdown content. + */ +export function middleware(request: NextRequest) { + const acceptHeader = request.headers.get("accept") || ""; + + // Check if the client is requesting markdown content + const wantsMarkdown = + acceptHeader.includes("text/markdown") || + acceptHeader.includes("text/x-markdown"); + + if (!wantsMarkdown) { + return NextResponse.next(); + } + + const pathname = request.nextUrl.pathname; + + // Skip API routes, static files, and special paths + if ( + pathname.startsWith("/api/") || + pathname.startsWith("/_next/") || + pathname.startsWith("/images/") || + pathname.includes(".") // Skip files with extensions (e.g., .png, .js, .css) + ) { + return NextResponse.next(); + } + + // Skip the root path + if (pathname === "/") { + return NextResponse.next(); + } + + // Rewrite to the markdown API endpoint + const url = request.nextUrl.clone(); + url.pathname = `/api/markdown${pathname}`; + + return NextResponse.rewrite(url); +} + +export const config = { + // Match all paths except static files and API routes + matcher: [ + /* + * Match all request paths except for: + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico (favicon file) + */ + "/((?!_next/static|_next/image|favicon.ico).*)", + ], +}; diff --git a/pages/api/markdown/[...slug].ts b/pages/api/markdown/[...slug].ts new file mode 100644 index 000000000..8c6fe9c2e --- /dev/null +++ b/pages/api/markdown/[...slug].ts @@ -0,0 +1,60 @@ +import fs from "fs"; +import { join } from "path"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { CONTENT_DIR, DOCS_FILE_EXTENSIONS } from "@/lib/content.server"; + +/** + * API route that serves raw markdown/MDX content for a given slug. + * This enables content negotiation - when a client requests a page with + * Accept: text/markdown, they receive the raw markdown instead of HTML. + */ +export default function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== "GET") { + return res + .status(405) + .setHeader("Allow", "GET") + .json({ error: `${req.method} method is not accepted.` }); + } + + const { slug } = req.query; + + if (!slug || !Array.isArray(slug)) { + return res.status(400).json({ error: "Invalid slug parameter" }); + } + + // Try to find the content file with .mdx or .md extension + let source: Buffer | null = null; + let sourcePath: string | null = null; + + for (const ext of DOCS_FILE_EXTENSIONS) { + const candidatePath = join( + CONTENT_DIR, + ...slug.slice(0, slug.length - 1), + `${slug[slug.length - 1]}${ext}`, + ); + + if (fs.existsSync(candidatePath)) { + source = fs.readFileSync(candidatePath); + sourcePath = candidatePath; + break; + } + } + + // Skip API reference pages + if ( + sourcePath && + (sourcePath.includes("content/__mapi-reference") || + sourcePath.includes("content/__api-reference")) + ) { + return res.status(404).json({ error: "Page not found" }); + } + + if (!source) { + return res.status(404).json({ error: "Page not found" }); + } + + // Return the raw markdown content + res.setHeader("Content-Type", "text/markdown; charset=utf-8"); + res.setHeader("Cache-Control", "public, max-age=3600, s-maxage=3600"); + res.status(200).send(source.toString()); +} From 6f2686de46320362aacf3023aff9ca25a11d696e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 15 Jan 2026 04:04:55 +0000 Subject: [PATCH 2/2] fix: parse slug from URL path for middleware rewrites When the middleware rewrites requests to the markdown API route, the slug parameter isn't automatically populated in req.query. This fix parses the slug directly from the URL path when needed. Co-authored-by: chris --- pages/api/markdown/[...slug].ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pages/api/markdown/[...slug].ts b/pages/api/markdown/[...slug].ts index 8c6fe9c2e..8a6a7827b 100644 --- a/pages/api/markdown/[...slug].ts +++ b/pages/api/markdown/[...slug].ts @@ -16,12 +16,24 @@ export default function handler(req: NextApiRequest, res: NextApiResponse) { .json({ error: `${req.method} method is not accepted.` }); } - const { slug } = req.query; + // Get slug from query params, or parse from URL path if not available + // (middleware rewrites may not populate req.query properly) + let slug: string | string[] | undefined = req.query.slug; - if (!slug || !Array.isArray(slug)) { + if (!slug || (Array.isArray(slug) && slug.length === 0)) { + // Parse slug from the URL path: /api/markdown/path/to/page -> ["path", "to", "page"] + const urlPath = req.url?.split("?")[0] || ""; + const pathSegments = urlPath.replace(/^\/api\/markdown\/?/, "").split("/"); + slug = pathSegments.filter((s) => s.length > 0); + } + + if (!slug || (Array.isArray(slug) && slug.length === 0)) { return res.status(400).json({ error: "Invalid slug parameter" }); } + // Ensure slug is an array + const slugArray = Array.isArray(slug) ? slug : [slug]; + // Try to find the content file with .mdx or .md extension let source: Buffer | null = null; let sourcePath: string | null = null; @@ -29,8 +41,8 @@ export default function handler(req: NextApiRequest, res: NextApiResponse) { for (const ext of DOCS_FILE_EXTENSIONS) { const candidatePath = join( CONTENT_DIR, - ...slug.slice(0, slug.length - 1), - `${slug[slug.length - 1]}${ext}`, + ...slugArray.slice(0, slugArray.length - 1), + `${slugArray[slugArray.length - 1]}${ext}`, ); if (fs.existsSync(candidatePath)) {