Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/app/mdx/[...slug]/route.ts
Original file line number Diff line number Diff line change
@@ -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' },
});
};
24 changes: 24 additions & 0 deletions src/lib/markdown/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,25 @@ export const load = async (pattern: string[], slugReplace?: RegExp): Promise<Slu
});
};

/**
* Returns every MDX-backed page across all sections, keyed by its full URL path.
* Used to map an incoming request path back to its underlying source file (e.g.
* for serving raw markdown via ?format=mdx).
*/
export const loadAllMdxPaths = async (): Promise<MdxPath[]> => {
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 })),
Comment on lines +55 to +56
];
};

/**
* Simple wrapper to turn a markdown slug into a fully qualified path.
* Which right now just means adding a '/' at the beginning.
Expand All @@ -49,3 +68,8 @@ interface SlugDefinition {
slug: string;
segments: string[];
}

interface MdxPath {
urlPath: string;
file: string;
}
42 changes: 42 additions & 0 deletions src/proxy.ts
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +6 to +10
* value falls through to the normal page.
*/
const FORMAT_REWRITES: Record<string, string> = {
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' }],
},
],
};