From 30cd3c8c906b45d54ab77bb39e8b653d6b8ee17d Mon Sep 17 00:00:00 2001 From: Andrew Kingston <9075550+aptkingston@users.noreply.github.com> Date: Fri, 29 May 2026 21:16:07 +0100 Subject: [PATCH 1/2] Serve raw MDX from any docs page by appending ?format=mdx --- src/app/mdx/[...slug]/route.ts | 33 +++++++++++++++++++++++++++++++++ src/lib/markdown/util.ts | 24 ++++++++++++++++++++++++ src/proxy.ts | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+) create mode 100644 src/app/mdx/[...slug]/route.ts create mode 100644 src/proxy.ts diff --git a/src/app/mdx/[...slug]/route.ts b/src/app/mdx/[...slug]/route.ts new file mode 100644 index 00000000..0bcaad5d --- /dev/null +++ b/src/app/mdx/[...slug]/route.ts @@ -0,0 +1,33 @@ +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; + +import { loadAllMdxPaths } from '@/lib/markdown/util'; + +// Statically generated so the source file read happens at build time, where +// src/content/** is guaranteed to be available (rather than at runtime). +export const dynamic = 'force-static'; +export const dynamicParams = false; + +export const generateStaticParams = async () => { + const pages = await loadAllMdxPaths(); + // Segments mirror the original URL path so the proxy rewrite target matches. + return pages.map((page) => ({ slug: page.urlPath.replace(/^\//, '').split('/') })); +}; + +export const GET = async (_req: Request, { params }: { params: Promise<{ slug: string[] }> }) => { + const { slug } = await params; + const urlPath = `/${slug.join('/')}`; + + const pages = await loadAllMdxPaths(); + const match = pages.find((page) => page.urlPath === urlPath); + + if (!match) { + return new Response('Not found', { status: 404 }); + } + + const source = await readFile(path.join(process.cwd(), 'src/content', match.file), 'utf-8'); + + return new Response(source, { + headers: { 'content-type': 'text/markdown; charset=utf-8' }, + }); +}; diff --git a/src/lib/markdown/util.ts b/src/lib/markdown/util.ts index 335812a2..a6ccaab9 100644 --- a/src/lib/markdown/util.ts +++ b/src/lib/markdown/util.ts @@ -38,6 +38,25 @@ export const load = async (pattern: string[], slugReplace?: RegExp): Promise => { + const [docs, guides, api] = await Promise.all([ + loadMdxInfo('documentation'), + loadMdxInfo('guides'), + loadMdxInfo('api'), + ]); + + return [ + ...docs.map((info) => ({ urlPath: `/${info.slug}`, file: info.file })), + ...guides.filter((info) => info.slug !== '').map((info) => ({ urlPath: `/guides/${info.slug}`, file: info.file })), + ...api.filter((info) => info.slug !== '').map((info) => ({ urlPath: `/api/${info.slug}`, file: info.file })), + ]; +}; + /** * Simple wrapper to turn a markdown slug into a fully qualified path. * Which right now just means adding a '/' at the beginning. @@ -49,3 +68,8 @@ interface SlugDefinition { slug: string; segments: string[]; } + +interface MdxPath { + urlPath: string; + file: string; +} diff --git a/src/proxy.ts b/src/proxy.ts new file mode 100644 index 00000000..b81876b7 --- /dev/null +++ b/src/proxy.ts @@ -0,0 +1,34 @@ +import { NextRequest, NextResponse } from 'next/server'; + +/** + * Central request interception for alternative content formats. + * + * Currently one format is supported on any MDX-backed page (optimised for LLMs + * reading the docs): + * ?format=mdx - the .mdx source (with JSX comment blocks stripped) + * The format param is the extension point for future formats - add new values + * here, each mapping to its own rewrite target / route handler. + */ +const FORMAT_REWRITES: Record = { + mdx: '/mdx', +}; + +export function proxy(req: NextRequest) { + const format = req.nextUrl.searchParams.get('format'); + const prefix = format ? FORMAT_REWRITES[format] : undefined; + + if (prefix) { + const url = req.nextUrl.clone(); + url.pathname = `${prefix}${url.pathname}`; + url.searchParams.delete('format'); + return NextResponse.rewrite(url); + } + + return NextResponse.next(); +} + +// Skip Next internals, the analytics proxy, the rewrite targets themselves, and +// any request with a file extension (static assets). +export const config = { + matcher: '/((?!_next/|api/phrp|mdx/|.*\\.).*)', +}; From 72a856e7c095f41d63b2e4f1458bd9baf2dfe85b Mon Sep 17 00:00:00 2001 From: Andrew Kingston <9075550+aptkingston@users.noreply.github.com> Date: Fri, 29 May 2026 21:28:16 +0100 Subject: [PATCH 2/2] Scope proxy to requests with a format query param --- src/proxy.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/proxy.ts b/src/proxy.ts index b81876b7..b4c7699d 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -5,9 +5,10 @@ import { NextRequest, NextResponse } from 'next/server'; * * Currently one format is supported on any MDX-backed page (optimised for LLMs * reading the docs): - * ?format=mdx - the .mdx source (with JSX comment blocks stripped) + * ?format=mdx - the raw .mdx source * The format param is the extension point for future formats - add new values - * here, each mapping to its own rewrite target / route handler. + * here, each mapping to its own rewrite target / route handler. An unrecognised + * value falls through to the normal page. */ const FORMAT_REWRITES: Record = { mdx: '/mdx', @@ -27,8 +28,15 @@ export function proxy(req: NextRequest) { return NextResponse.next(); } -// Skip Next internals, the analytics proxy, the rewrite targets themselves, and -// any request with a file extension (static assets). +// Only run when a `format` query param is present, on any path. This scopes the +// proxy to exactly the requests it cares about (rather than every page request +// filtered by pathname). The rewrite targets carry no `format` param, so there's +// no risk of a rewrite loop. export const config = { - matcher: '/((?!_next/|api/phrp|mdx/|.*\\.).*)', + matcher: [ + { + source: '/:path*', + has: [{ type: 'query', key: 'format' }], + }, + ], };