Chore: unify .mdx suffix to .md

This commit is contained in:
Fuma Nama
2026-05-05 15:12:14 +08:00
parent 563bdd4b02
commit 7a6ff319ac
21 changed files with 116 additions and 104 deletions
@@ -179,11 +179,9 @@ export async function getConfig() {
}
```
### `*.mdx` [#mdx-extension]
### `*.md` [#md-extension]
Allow AI agents to get the content of a page as Markdown/MDX, by appending `.mdx` to the end of path.
Make a route handler to return page content, and a middleware to point to it:
Allow AI agents to get the content of a page as Markdown/MDX, by appending `.md` to the end of path.
```ts tab="Next.js" title="app/llms.mdx/docs/[[...slug]]/route.ts"
import { getLLMText } from '@/lib/get-llm-text';
@@ -217,7 +215,7 @@ const config: NextConfig = {
async rewrites() {
return [
{
source: '/docs/:path*.mdx',
source: '/docs/:path*.md',
destination: '/llms.mdx/docs/:path*',
},
];
@@ -259,7 +257,7 @@ import { rewritePath } from 'fumadocs-core/negotiation';
import type { Route } from './+types/root';
// [!code ++:20]
const { rewrite: rewriteLLM } = rewritePath('/docs{/*path}.mdx', '/llms.mdx/docs{/*path}');
const { rewrite: rewriteLLM } = rewritePath('/docs{/*path}.md', '/llms.mdx/docs{/*path}');
const serverMiddleware: Route.MiddlewareFunction = async ({ request }, next) => {
const url = new URL(request.url);
const path = rewriteLLM(url.pathname);
@@ -282,16 +280,43 @@ export default {
} satisfies Config;
```
```ts tab="Tanstack Start" title="src/routes/llms[.]mdx.docs.$.ts"
```ts tab="Tanstack Start" title="lib/source.ts"
// create the following utils:
export function markdownPathToSlugs(segs: string[]) {
if (segs.length === 0) return [];
const out = [...segs];
out[out.length - 1] = out[out.length - 1].replace(/\.md$/, '');
if (out.length === 1 && out[0] === 'index') out.pop();
return out;
}
export function slugsToMarkdownPath(slugs: string[]) {
const segments = [...slugs];
if (segments.length === 0) {
segments.push('index.md');
} else {
segments[segments.length - 1] += '.md';
}
return {
segments,
url: `/docs/${segments.join('/')}`,
};
}
```
```ts tab="Tanstack Start" title="src/routes/docs/{$}[.]md.ts"
import { createFileRoute, notFound } from '@tanstack/react-router';
import { source } from '@/lib/source';
import { source, markdownPathToSlugs } from '@/lib/source';
import { getLLMText } from '@/lib/get-llm-text';
export const Route = createFileRoute('/llms.mdx/docs/$')({
export const Route = createFileRoute('/docs/{$}.md')({
server: {
handlers: {
GET: async ({ params }) => {
const slugs = params._splat?.split('/') ?? [];
const slugs = markdownPathToSlugs(params._splat?.split('/') ?? []);
const page = source.getPage(slugs);
if (!page) throw notFound();
@@ -306,31 +331,6 @@ export const Route = createFileRoute('/llms.mdx/docs/$')({
});
```
```ts tab="Tanstack Start" title="src/start.ts"
import { createMiddleware, createStart } from '@tanstack/react-start';
import { rewritePath } from 'fumadocs-core/negotiation';
import { redirect } from '@tanstack/react-router';
const { rewrite: rewriteLLM } = rewritePath('/docs{/*path}.mdx', '/llms.mdx/docs{/*path}');
const llmMiddleware = createMiddleware().server(({ next, request }) => {
const url = new URL(request.url);
const path = rewriteLLM(url.pathname);
if (path) {
throw redirect(new URL(path, url));
}
return next();
});
export const startInstance = createStart(() => {
return {
requestMiddleware: [llmMiddleware],
};
});
```
#### `Accept`
To serve the Markdown content instead for AI agents, you can leverage the `Accept` header.
@@ -356,7 +356,7 @@ export default function proxy(request: NextRequest) {
### Page Actions
Common page actions for AI, require [`*.mdx`](#mdx-extension) to be implemented first.
Common page actions for AI, require [`*.md`](#md-extension) to be implemented first.
![AI Page Actions](/docs/ai-page-actions.png)
@@ -368,6 +368,7 @@ Use it in your docs page like:
```tsx title="app/docs/[[...slug]]/page.tsx"
// URL to fetch Markdown content, only need to append .mdx to URL if you have `*.mdx` configured.
// Use .md instead for the Tanstack Start example above.
const markdownUrl = `${page.url}.mdx`
<div className="flex flex-row gap-2 items-center border-b pt-2 pb-6">
@@ -182,8 +182,8 @@ const serverLoader = createServerFn({
return {
type: 'docs',
path: page.path,
markdownUrl: getPageMarkdownUrl(page).url,
pageTree,
// ...
};
});
@@ -1,35 +0,0 @@
---
name: read-docs
description: Enables efficient, accurate retrieval of content from Fumadocs-powered documentation sites.
---
A specialized skill for exploring and retrieving accurate, clean content from documentation websites built with Fumadocs.
Fumadocs-powered sites typically expose three LLM-friendly features:
1. **Page Discovery via llms.txt**
The site provides a plain-text file at the root URL: `/llms.txt`.
This file contains a complete list of crawlable routes (one per line), often with comments or metadata indicating which paths are documentation pages.
2. **Processed Markdown for Docs Pages**
Fumadocs templates often expose **processed Markdown** (plain Markdown derived from MDX) at `/llms.mdx/docs/<path>/content.md` for each page. Some sites also rewrite `/docs/.../page.mdx` or negotiate `Accept: text/markdown` to that URL via middleware.
Example:
- Rendered page: https://example.com/docs/installation
- Markdown for LLMs: https://example.com/llms.mdx/docs/installation/content.md
Prefer this over scraping HTML when the site exposes it.
3. **Search API**
The site exposes a JSON search endpoint:
GET /api/search?query=<search-term>
It returns structured results (usually an array of objects with title, excerpt, URL, and sometimes hierarchy).
When a user query involves a library, tool, framework, or project whose official documentation is hosted on a Fumadocs site:
- First, confirm it is Fumadocs-powered (user confirmation, known projects, or successfully fetching `/llms.txt`).
- Fetch `/llms.txt` to obtain the full list of available pages.
- When retrieving the content of a specific docs page, prefer the markdown endpoint (often `.../content.md` under `/llms.mdx/docs/`) or a `.mdx` suffix on the docs path if the site uses that pattern.
- When the user needs to find something (keyword, feature, concept), use the `/api/search` endpoint with a precise query.
- Parse and reason over the raw MDX or JSON results to provide accurate, up-to-date answers.
- Cite the exact source URL (prefer the original rendered URL for user readability, but base your understanding on the raw version).
Prioritize these endpoints over scraping rendered HTML to minimize noise and maximize accuracy.
+1 -1
View File
@@ -7,7 +7,7 @@ const { rewrite: rewriteDocs } = rewritePath(
`${docsContentRoute}{/*path}/content.md`,
);
const { rewrite: rewriteSuffix } = rewritePath(
`${docsRoute}{/*path}.mdx`,
`${docsRoute}{/*path}.md`,
`${docsContentRoute}{/*path}/content.md`,
);
+1 -1
View File
@@ -7,7 +7,7 @@ const { rewrite: rewriteDocs } = rewritePath(
`${docsContentRoute}{/*path}/content.md`,
);
const { rewrite: rewriteSuffix } = rewritePath(
`${docsRoute}{/*path}.mdx`,
`${docsRoute}{/*path}.md`,
`${docsContentRoute}{/*path}/content.md`,
);
+1 -1
View File
@@ -80,7 +80,7 @@ const { rewrite: rewriteDocs } = rewritePath(
`${docsContentRoute}{/*path}/content.md`,
);
const { rewrite: rewriteSuffix } = rewritePath(
`${docsRoute}{/*path}.mdx`,
`${docsRoute}{/*path}.md`,
`${docsContentRoute}{/*path}/content.md`,
);
const serverMiddleware: Route.MiddlewareFunction = async ({ request }, next) => {
@@ -20,7 +20,16 @@ export async function getSource() {
return source.get();
}
export function getPageMarkdownUrl(slugs: string[]) {
export function markdownPathToSlugs(segs: string[]) {
if (segs.length === 0) return [];
const out = [...segs];
out[out.length - 1] = out[out.length - 1].replace(/\.md$/, '');
if (out.length === 1 && out[0] === 'index') out.pop();
return out;
}
export function slugsToMarkdownPath(slugs: string[]) {
const segments = [...slugs];
if (segments.length === 0) {
segments.push('index.md');
@@ -1,7 +1,7 @@
import { createFileRoute, notFound } from '@tanstack/react-router';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { createServerFn } from '@tanstack/react-start';
import { getPageMarkdownUrl, getSource } from '@/lib/source';
import { getSource, slugsToMarkdownPath } from '@/lib/source';
import {
DocsBody,
DocsDescription,
@@ -40,7 +40,7 @@ const serverLoader = createServerFn({
path: page.path,
frontmatter: page.data.frontmatter,
render: serialize(),
markdownUrl: getPageMarkdownUrl(page.slugs).url,
markdownUrl: slugsToMarkdownPath(page.slugs).url,
pageTree: await source.serializePageTree(source.getPageTree()),
};
});
@@ -1,12 +1,11 @@
import { createFileRoute, notFound } from '@tanstack/react-router';
import { getLLMText, getSource } from '@/lib/source';
import { getLLMText, getSource, markdownPathToSlugs } from '@/lib/source';
export const Route = createFileRoute('/docs/{$}.md')({
server: {
handlers: {
GET: async ({ params }) => {
const slugs = params._splat?.split('/') ?? [];
slugs.pop();
const slugs = markdownPathToSlugs(params._splat?.split('/') ?? []);
const source = await getSource();
const page = source.getPage(slugs);
if (!page) throw notFound();
@@ -2,7 +2,7 @@ import { createMiddleware, createStart } from '@tanstack/react-start';
import { isMarkdownPreferred } from 'fumadocs-core/negotiation';
import { redirect } from '@tanstack/react-router';
import { docsRoute } from '@/lib/shared';
import { getPageMarkdownUrl } from './lib/source';
import { slugsToMarkdownPath } from './lib/source';
const llmMiddleware = createMiddleware().server(({ next, request }) => {
const url = new URL(request.url);
@@ -16,7 +16,7 @@ const llmMiddleware = createMiddleware().server(({ next, request }) => {
.slice(docsRoute.length)
.split('/')
.filter((v) => v.length > 0);
url.pathname = getPageMarkdownUrl(slugs).url;
url.pathname = slugsToMarkdownPath(slugs).url;
throw redirect(url);
}
@@ -18,7 +18,16 @@ export const source = loader(
},
);
export function getPageMarkdownUrl(slugs: string[]) {
export function markdownPathToSlugs(segs: string[]) {
if (segs.length === 0) return [];
const out = [...segs];
out[out.length - 1] = out[out.length - 1].replace(/\.md$/, '');
if (out.length === 1 && out[0] === 'index') out.pop();
return out;
}
export function slugsToMarkdownPath(slugs: string[]) {
const segments = [...slugs];
if (segments.length === 0) {
segments.push('index.md');
@@ -1,7 +1,7 @@
import { createFileRoute, notFound } from '@tanstack/react-router';
import { DocsLayout } from 'fumadocs-ui/layouts/notebook';
import { createServerFn } from '@tanstack/react-start';
import { getPageMarkdownUrl, source } from '@/lib/source';
import { slugsToMarkdownPath, source } from '@/lib/source';
import browserCollections from 'collections/browser';
import {
DocsBody,
@@ -53,7 +53,7 @@ const serverLoader = createServerFn({
return {
type: 'docs',
path: page.path,
markdownUrl: getPageMarkdownUrl(page.slugs).url,
markdownUrl: slugsToMarkdownPath(page.slugs).url,
pageTree,
};
});
@@ -1,12 +1,11 @@
import { createFileRoute, notFound } from '@tanstack/react-router';
import { getLLMText, source } from '@/lib/source';
import { getLLMText, markdownPathToSlugs, source } from '@/lib/source';
export const Route = createFileRoute('/docs/{$}.md')({
server: {
handlers: {
GET: async ({ params }) => {
const slugs = params._splat?.split('/') ?? [];
slugs.pop();
const slugs = markdownPathToSlugs(params._splat?.split('/') ?? []);
const page = source.getPage(slugs);
if (!page) throw notFound();
+2 -2
View File
@@ -2,7 +2,7 @@ import { createMiddleware, createStart } from '@tanstack/react-start';
import { isMarkdownPreferred } from 'fumadocs-core/negotiation';
import { redirect } from '@tanstack/react-router';
import { docsRoute } from '@/lib/shared';
import { getPageMarkdownUrl } from './lib/source';
import { slugsToMarkdownPath } from './lib/source';
const llmMiddleware = createMiddleware().server(({ next, request }) => {
const url = new URL(request.url);
@@ -16,7 +16,7 @@ const llmMiddleware = createMiddleware().server(({ next, request }) => {
.slice(docsRoute.length)
.split('/')
.filter((v) => v.length > 0);
url.pathname = getPageMarkdownUrl(slugs).url;
url.pathname = slugsToMarkdownPath(slugs).url;
throw redirect(url);
}
@@ -9,6 +9,29 @@ export const source = loader({
plugins: [lucideIconsPlugin()],
});
export function markdownPathToSlugs(segs: string[]) {
if (segs.length === 0) return [];
const out = [...segs];
out[out.length - 1] = out[out.length - 1].replace(/\.md$/, '');
if (out.length === 1 && out[0] === 'index') out.pop();
return out;
}
export function slugsToMarkdownPath(slugs: string[]) {
const segments = [...slugs];
if (segments.length === 0) {
segments.push('index.md');
} else {
segments[segments.length - 1] += '.md';
}
return {
segments,
url: `${docsRoute}/${segments.join('/')}`,
};
}
export function getPageMarkdownUrl(slugs: string[]) {
const segments = [...slugs];
if (segments.length === 0) {
@@ -1,7 +1,7 @@
import { createFileRoute, Link, notFound } from '@tanstack/react-router';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { createServerFn } from '@tanstack/react-start';
import { getPageMarkdownUrl, source } from '@/lib/source';
import { slugsToMarkdownPath, source } from '@/lib/source';
import browserCollections from 'collections/browser';
import {
DocsBody,
@@ -39,7 +39,7 @@ const loader = createServerFn({
return {
path: page.path,
markdownUrl: getPageMarkdownUrl(page.slugs).url,
markdownUrl: slugsToMarkdownPath(page.slugs).url,
pageTree: await source.serializePageTree(source.getPageTree()),
};
});
@@ -1,12 +1,11 @@
import { createFileRoute, notFound } from '@tanstack/react-router';
import { getLLMText, source } from '@/lib/source';
import { getLLMText, markdownPathToSlugs, source } from '@/lib/source';
export const Route = createFileRoute('/docs/{$}.md')({
server: {
handlers: {
GET: async ({ params }) => {
const slugs = params._splat?.split('/') ?? [];
slugs.pop();
const slugs = markdownPathToSlugs(params._splat?.split('/') ?? []);
const page = source.getPage(slugs);
if (!page) throw notFound();
+10 -1
View File
@@ -9,7 +9,16 @@ export const source = loader({
plugins: [lucideIconsPlugin()],
});
export function getPageMarkdownUrl(slugs: string[]) {
export function markdownPathToSlugs(segs: string[]) {
if (segs.length === 0) return [];
const out = [...segs];
out[out.length - 1] = out[out.length - 1].replace(/\.md$/, '');
if (out.length === 1 && out[0] === 'index') out.pop();
return out;
}
export function slugsToMarkdownPath(slugs: string[]) {
const segments = [...slugs];
if (segments.length === 0) {
segments.push('index.md');
@@ -1,7 +1,7 @@
import { createFileRoute, notFound } from '@tanstack/react-router';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { createServerFn } from '@tanstack/react-start';
import { getPageMarkdownUrl, source } from '@/lib/source';
import { slugsToMarkdownPath, source } from '@/lib/source';
import browserCollections from 'collections/browser';
import {
DocsBody,
@@ -37,7 +37,7 @@ const serverLoader = createServerFn({
return {
path: page.path,
markdownUrl: getPageMarkdownUrl(page.slugs).url,
markdownUrl: slugsToMarkdownPath(page.slugs).url,
pageTree: await source.serializePageTree(source.getPageTree()),
};
});
@@ -1,12 +1,11 @@
import { createFileRoute, notFound } from '@tanstack/react-router';
import { getLLMText, source } from '@/lib/source';
import { getLLMText, markdownPathToSlugs, source } from '@/lib/source';
export const Route = createFileRoute('/docs/{$}.md')({
server: {
handlers: {
GET: async ({ params }) => {
const slugs = params._splat?.split('/') ?? [];
slugs.pop();
const slugs = markdownPathToSlugs(params._splat?.split('/') ?? []);
const page = source.getPage(slugs);
if (!page) throw notFound();
+2 -2
View File
@@ -2,7 +2,7 @@ import { createMiddleware, createStart } from '@tanstack/react-start';
import { isMarkdownPreferred } from 'fumadocs-core/negotiation';
import { redirect } from '@tanstack/react-router';
import { docsRoute } from '@/lib/shared';
import { getPageMarkdownUrl } from './lib/source';
import { slugsToMarkdownPath } from './lib/source';
const llmMiddleware = createMiddleware().server(({ next, request }) => {
const url = new URL(request.url);
@@ -16,7 +16,7 @@ const llmMiddleware = createMiddleware().server(({ next, request }) => {
.slice(docsRoute.length)
.split('/')
.filter((v) => v.length > 0);
url.pathname = getPageMarkdownUrl(slugs).url;
url.pathname = slugsToMarkdownPath(slugs).url;
throw redirect(url);
}