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..b4c7699d --- /dev/null +++ b/src/proxy.ts @@ -0,0 +1,42 @@ +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 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. An unrecognised + * value falls through to the normal page. + */ +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(); +} + +// 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: [ + { + source: '/:path*', + has: [{ type: 'query', key: 'format' }], + }, + ], +};