mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
Chore: unify .mdx suffix to .md
This commit is contained in:
@@ -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.
|
||||
|
||||

|
||||
|
||||
@@ -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.
|
||||
@@ -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`,
|
||||
);
|
||||
|
||||
|
||||
@@ -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`,
|
||||
);
|
||||
|
||||
|
||||
@@ -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,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();
|
||||
|
||||
|
||||
@@ -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,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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user