-
Notifications
You must be signed in to change notification settings - Fork 5
Markdown content negotiation #1267
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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).*)", | ||
| ], | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Path traversal allows reading arbitrary filesHigh Severity The API endpoint uses user-provided slug segments to construct a file path via |
||
| } | ||
|
|
||
| // 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()); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dot check blocks valid paths with periods
Medium Severity
The check
pathname.includes(".")is intended to skip files with extensions like.pngor.css, but it's too broad. It blocks any path containing a period anywhere, including legitimate documentation paths with version numbers like/docs/v1.2/introor/docs/node.js/setup. These paths would incorrectly skip markdown serving.