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..8a6a7827b --- /dev/null +++ b/pages/api/markdown/[...slug].ts @@ -0,0 +1,72 @@ +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.` }); + } + + // 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) && 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; + + for (const ext of DOCS_FILE_EXTENSIONS) { + const candidatePath = join( + CONTENT_DIR, + ...slugArray.slice(0, slugArray.length - 1), + `${slugArray[slugArray.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()); +}