Skip to content

Commit e202a5f

Browse files
committed
Improve docs markdown negotiation
1 parent 6a1ae5e commit e202a5f

12 files changed

Lines changed: 373 additions & 29 deletions

source.config.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,26 @@ export const docs = defineDocs({
1616
docs: {
1717
async: isDevelopment,
1818
postprocess: {
19-
includeProcessedMarkdown: true,
19+
includeProcessedMarkdown: {
20+
mdxAsPlaceholder: [
21+
"Accordion",
22+
"AccordionGroup",
23+
"Callout",
24+
"Card",
25+
"CardGroup",
26+
"CodeGroup",
27+
"Frame",
28+
"Info",
29+
"Mermaid",
30+
"Note",
31+
"Step",
32+
"Steps",
33+
"Tab",
34+
"Tabs",
35+
"Tip",
36+
"Warning",
37+
],
38+
},
2039
},
2140
},
2241
});

src/lib/llms.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export function getLLMSectionConfig(section?: string) {
5151
return llmsSectionConfigs[section as LLMSection];
5252
}
5353

54-
export function getPagesForLLMSection(pages: LLMPageLike[], section?: string) {
54+
export function getPagesForLLMSection<TPage extends LLMPageLike>(pages: TPage[], section?: string) {
5555
const config = getLLMSectionConfig(section);
5656
if (!config) return pages;
5757

src/lib/markdown-alternate.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { DOCS_BASE } from "./url-base";
2+
3+
const MARKDOWN_ROUTE_PREFIX = `${DOCS_BASE}/llms.mdx/docs`;
4+
5+
function normalizeDocsPath(pathname: string): string {
6+
const normalized = pathname.replace(/\/+$/, "");
7+
return normalized || DOCS_BASE;
8+
}
9+
10+
export function buildMarkdownAlternatePath(pathname: string): string {
11+
const normalized = normalizeDocsPath(pathname);
12+
if (normalized === DOCS_BASE) return MARKDOWN_ROUTE_PREFIX;
13+
return `${normalized}.md`;
14+
}
15+
16+
export function buildHtmlPathFromMarkdownRoute(slugs: string[]): string {
17+
if (slugs.length === 0) return DOCS_BASE;
18+
return `${DOCS_BASE}/${slugs.join("/")}`;
19+
}
20+
21+
export function appendHeaderValue(headers: Headers, name: string, value: string) {
22+
const current = headers.get(name);
23+
if (!current) {
24+
headers.set(name, value);
25+
return;
26+
}
27+
28+
const values = current.split(",").map((entry) => entry.trim().toLowerCase());
29+
if (!values.includes(value.toLowerCase())) {
30+
headers.set(name, `${current}, ${value}`);
31+
}
32+
}
33+
34+
export function appendVaryAccept(headers: Headers) {
35+
appendHeaderValue(headers, "Vary", "Accept");
36+
}
37+
38+
export function buildMarkdownAlternateLinkHeader(pathname: string): string {
39+
return `<${buildMarkdownAlternatePath(pathname)}>; rel="alternate"; type="text/markdown"`;
40+
}

src/lib/source.ts

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import { InferPageType, loader } from "fumadocs-core/source";
2+
import {
3+
renderPlaceholder,
4+
type PlaceholderData,
5+
} from "fumadocs-core/mdx-plugins/remark-llms.runtime";
26
import { docs } from "fumadocs-mdx:collections/server";
37
import { createElement, type SVGProps } from "react";
48
import {
@@ -134,17 +138,76 @@ export const source = loader({
134138
},
135139
});
136140

141+
function getStringAttribute(attributes: Record<string, unknown>, key: string): string | undefined {
142+
const value = attributes[key];
143+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
144+
}
145+
146+
function joinMarkdownBlocks(blocks: Array<string | undefined>) {
147+
return blocks
148+
.map((block) => block?.trim())
149+
.filter(Boolean)
150+
.join("\n\n");
151+
}
152+
153+
function renderMarkdownBlock(blocks: Array<string | undefined>) {
154+
const block = joinMarkdownBlocks(blocks);
155+
return block ? `${block}\n\n` : "";
156+
}
157+
158+
function renderCallout(label: string, data: PlaceholderData) {
159+
const title = getStringAttribute(data.attributes, "title") ?? label;
160+
return renderMarkdownBlock([`> **${title}**`, data.children]);
161+
}
162+
163+
const markdownPlaceholderRenderers: Record<string, (data: PlaceholderData) => string> = {
164+
Accordion: (data) =>
165+
renderMarkdownBlock([
166+
`## ${getStringAttribute(data.attributes, "title") ?? "Details"}`,
167+
data.children,
168+
]),
169+
AccordionGroup: (data) => data.children,
170+
Callout: (data) => renderCallout("Callout", data),
171+
Card: (data) =>
172+
renderMarkdownBlock([
173+
`## ${getStringAttribute(data.attributes, "title") ?? "Link"}`,
174+
getStringAttribute(data.attributes, "href"),
175+
data.children,
176+
]),
177+
CardGroup: (data) => data.children,
178+
CodeGroup: (data) => data.children,
179+
Frame: (data) => data.children,
180+
Info: (data) => renderCallout("Info", data),
181+
Mermaid: (data) => `\`\`\`mermaid\n${data.children.trim()}\n\`\`\`\n\n`,
182+
Note: (data) => renderCallout("Note", data),
183+
Step: (data) =>
184+
renderMarkdownBlock([
185+
`## ${getStringAttribute(data.attributes, "title") ?? "Step"}`,
186+
data.children,
187+
]),
188+
Steps: (data) => data.children,
189+
Tab: (data) =>
190+
renderMarkdownBlock([
191+
`## ${getStringAttribute(data.attributes, "title") ?? "Tab"}`,
192+
data.children,
193+
]),
194+
Tabs: (data) => data.children,
195+
Tip: (data) => renderCallout("Tip", data),
196+
Warning: (data) => renderCallout("Warning", data),
197+
};
198+
137199
export async function getPageMarkdownText(
138200
page: InferPageType<typeof source>,
139201
_type: "raw" | "processed" = "processed",
140202
) {
141-
return page.data.getText("processed");
203+
const processed = await renderPlaceholder(
204+
await page.data.getText("processed"),
205+
markdownPlaceholderRenderers,
206+
);
207+
208+
return joinMarkdownBlocks([`# ${page.data.title}`, page.data.description, processed]);
142209
}
143210

144211
export async function getLLMText(page: InferPageType<typeof source>) {
145-
const processed = await getPageMarkdownText(page);
146-
147-
return `# ${page.data.title}
148-
149-
${processed}`;
212+
return getPageMarkdownText(page);
150213
}

src/routeTree.gen.ts

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ import { Route as SplatRouteImport } from './routes/$'
1919
import { Route as IndexRouteImport } from './routes/index'
2020
import { Route as SdkIndexRouteImport } from './routes/sdk/index'
2121
import { Route as SdkSplatRouteImport } from './routes/sdk/$'
22+
import { Route as LlmsDotmdxDocsRouteImport } from './routes/llms[.]mdx.docs'
2223
import { Route as ApiSearchRouteImport } from './routes/api/search'
2324
import { Route as ApiFeedbackRouteImport } from './routes/api/feedback'
25+
import { Route as LlmsDotmdxDocsIndexRouteImport } from './routes/llms[.]mdx.docs.index'
2426
import { Route as LlmsDotmdxDocsSplatRouteImport } from './routes/llms[.]mdx.docs.$'
2527
import { Route as ApiRawSplatRouteImport } from './routes/api/raw/$'
2628

@@ -76,6 +78,11 @@ const SdkSplatRoute = SdkSplatRouteImport.update({
7678
path: '/sdk/$',
7779
getParentRoute: () => rootRouteImport,
7880
} as any)
81+
const LlmsDotmdxDocsRoute = LlmsDotmdxDocsRouteImport.update({
82+
id: '/llms.mdx/docs',
83+
path: '/llms.mdx/docs',
84+
getParentRoute: () => rootRouteImport,
85+
} as any)
7986
const ApiSearchRoute = ApiSearchRouteImport.update({
8087
id: '/api/search',
8188
path: '/api/search',
@@ -86,10 +93,15 @@ const ApiFeedbackRoute = ApiFeedbackRouteImport.update({
8693
path: '/api/feedback',
8794
getParentRoute: () => rootRouteImport,
8895
} as any)
96+
const LlmsDotmdxDocsIndexRoute = LlmsDotmdxDocsIndexRouteImport.update({
97+
id: '/',
98+
path: '/',
99+
getParentRoute: () => LlmsDotmdxDocsRoute,
100+
} as any)
89101
const LlmsDotmdxDocsSplatRoute = LlmsDotmdxDocsSplatRouteImport.update({
90-
id: '/llms.mdx/docs/$',
91-
path: '/llms.mdx/docs/$',
92-
getParentRoute: () => rootRouteImport,
102+
id: '/$',
103+
path: '/$',
104+
getParentRoute: () => LlmsDotmdxDocsRoute,
93105
} as any)
94106
const ApiRawSplatRoute = ApiRawSplatRouteImport.update({
95107
id: '/api/raw/$',
@@ -108,10 +120,12 @@ export interface FileRoutesByFullPath {
108120
'/sitemap.xml': typeof SitemapDotxmlRoute
109121
'/api/feedback': typeof ApiFeedbackRoute
110122
'/api/search': typeof ApiSearchRoute
123+
'/llms.mdx/docs': typeof LlmsDotmdxDocsRouteWithChildren
111124
'/sdk/$': typeof SdkSplatRoute
112125
'/sdk/': typeof SdkIndexRoute
113126
'/api/raw/$': typeof ApiRawSplatRoute
114127
'/llms.mdx/docs/$': typeof LlmsDotmdxDocsSplatRoute
128+
'/llms.mdx/docs/': typeof LlmsDotmdxDocsIndexRoute
115129
}
116130
export interface FileRoutesByTo {
117131
'/': typeof IndexRoute
@@ -128,6 +142,7 @@ export interface FileRoutesByTo {
128142
'/sdk': typeof SdkIndexRoute
129143
'/api/raw/$': typeof ApiRawSplatRoute
130144
'/llms.mdx/docs/$': typeof LlmsDotmdxDocsSplatRoute
145+
'/llms.mdx/docs': typeof LlmsDotmdxDocsIndexRoute
131146
}
132147
export interface FileRoutesById {
133148
__root__: typeof rootRouteImport
@@ -141,10 +156,12 @@ export interface FileRoutesById {
141156
'/sitemap.xml': typeof SitemapDotxmlRoute
142157
'/api/feedback': typeof ApiFeedbackRoute
143158
'/api/search': typeof ApiSearchRoute
159+
'/llms.mdx/docs': typeof LlmsDotmdxDocsRouteWithChildren
144160
'/sdk/$': typeof SdkSplatRoute
145161
'/sdk/': typeof SdkIndexRoute
146162
'/api/raw/$': typeof ApiRawSplatRoute
147163
'/llms.mdx/docs/$': typeof LlmsDotmdxDocsSplatRoute
164+
'/llms.mdx/docs/': typeof LlmsDotmdxDocsIndexRoute
148165
}
149166
export interface FileRouteTypes {
150167
fileRoutesByFullPath: FileRoutesByFullPath
@@ -159,10 +176,12 @@ export interface FileRouteTypes {
159176
| '/sitemap.xml'
160177
| '/api/feedback'
161178
| '/api/search'
179+
| '/llms.mdx/docs'
162180
| '/sdk/$'
163181
| '/sdk/'
164182
| '/api/raw/$'
165183
| '/llms.mdx/docs/$'
184+
| '/llms.mdx/docs/'
166185
fileRoutesByTo: FileRoutesByTo
167186
to:
168187
| '/'
@@ -179,6 +198,7 @@ export interface FileRouteTypes {
179198
| '/sdk'
180199
| '/api/raw/$'
181200
| '/llms.mdx/docs/$'
201+
| '/llms.mdx/docs'
182202
id:
183203
| '__root__'
184204
| '/'
@@ -191,10 +211,12 @@ export interface FileRouteTypes {
191211
| '/sitemap.xml'
192212
| '/api/feedback'
193213
| '/api/search'
214+
| '/llms.mdx/docs'
194215
| '/sdk/$'
195216
| '/sdk/'
196217
| '/api/raw/$'
197218
| '/llms.mdx/docs/$'
219+
| '/llms.mdx/docs/'
198220
fileRoutesById: FileRoutesById
199221
}
200222
export interface RootRouteChildren {
@@ -208,10 +230,10 @@ export interface RootRouteChildren {
208230
SitemapDotxmlRoute: typeof SitemapDotxmlRoute
209231
ApiFeedbackRoute: typeof ApiFeedbackRoute
210232
ApiSearchRoute: typeof ApiSearchRoute
233+
LlmsDotmdxDocsRoute: typeof LlmsDotmdxDocsRouteWithChildren
211234
SdkSplatRoute: typeof SdkSplatRoute
212235
SdkIndexRoute: typeof SdkIndexRoute
213236
ApiRawSplatRoute: typeof ApiRawSplatRoute
214-
LlmsDotmdxDocsSplatRoute: typeof LlmsDotmdxDocsSplatRoute
215237
}
216238

217239
declare module '@tanstack/react-router' {
@@ -286,6 +308,13 @@ declare module '@tanstack/react-router' {
286308
preLoaderRoute: typeof SdkSplatRouteImport
287309
parentRoute: typeof rootRouteImport
288310
}
311+
'/llms.mdx/docs': {
312+
id: '/llms.mdx/docs'
313+
path: '/llms.mdx/docs'
314+
fullPath: '/llms.mdx/docs'
315+
preLoaderRoute: typeof LlmsDotmdxDocsRouteImport
316+
parentRoute: typeof rootRouteImport
317+
}
289318
'/api/search': {
290319
id: '/api/search'
291320
path: '/api/search'
@@ -300,12 +329,19 @@ declare module '@tanstack/react-router' {
300329
preLoaderRoute: typeof ApiFeedbackRouteImport
301330
parentRoute: typeof rootRouteImport
302331
}
332+
'/llms.mdx/docs/': {
333+
id: '/llms.mdx/docs/'
334+
path: '/'
335+
fullPath: '/llms.mdx/docs/'
336+
preLoaderRoute: typeof LlmsDotmdxDocsIndexRouteImport
337+
parentRoute: typeof LlmsDotmdxDocsRoute
338+
}
303339
'/llms.mdx/docs/$': {
304340
id: '/llms.mdx/docs/$'
305-
path: '/llms.mdx/docs/$'
341+
path: '/$'
306342
fullPath: '/llms.mdx/docs/$'
307343
preLoaderRoute: typeof LlmsDotmdxDocsSplatRouteImport
308-
parentRoute: typeof rootRouteImport
344+
parentRoute: typeof LlmsDotmdxDocsRoute
309345
}
310346
'/api/raw/$': {
311347
id: '/api/raw/$'
@@ -317,6 +353,20 @@ declare module '@tanstack/react-router' {
317353
}
318354
}
319355

356+
interface LlmsDotmdxDocsRouteChildren {
357+
LlmsDotmdxDocsSplatRoute: typeof LlmsDotmdxDocsSplatRoute
358+
LlmsDotmdxDocsIndexRoute: typeof LlmsDotmdxDocsIndexRoute
359+
}
360+
361+
const LlmsDotmdxDocsRouteChildren: LlmsDotmdxDocsRouteChildren = {
362+
LlmsDotmdxDocsSplatRoute: LlmsDotmdxDocsSplatRoute,
363+
LlmsDotmdxDocsIndexRoute: LlmsDotmdxDocsIndexRoute,
364+
}
365+
366+
const LlmsDotmdxDocsRouteWithChildren = LlmsDotmdxDocsRoute._addFileChildren(
367+
LlmsDotmdxDocsRouteChildren,
368+
)
369+
320370
const rootRouteChildren: RootRouteChildren = {
321371
IndexRoute: IndexRoute,
322372
SplatRoute: SplatRoute,
@@ -329,10 +379,10 @@ const rootRouteChildren: RootRouteChildren = {
329379
SitemapDotxmlRoute: SitemapDotxmlRoute,
330380
ApiFeedbackRoute: ApiFeedbackRoute,
331381
ApiSearchRoute: ApiSearchRoute,
382+
LlmsDotmdxDocsRoute: LlmsDotmdxDocsRouteWithChildren,
332383
SdkSplatRoute: SdkSplatRoute,
333384
SdkIndexRoute: SdkIndexRoute,
334385
ApiRawSplatRoute: ApiRawSplatRoute,
335-
LlmsDotmdxDocsSplatRoute: LlmsDotmdxDocsSplatRoute,
336386
}
337387
export const routeTree = rootRouteImport
338388
._addFileChildren(rootRouteChildren)

src/routes/$.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
} from "@/lib/metadata";
2424
import { getSdkContextFromRouterPath, SDK_CONTEXT_SLUGS } from "@/lib/sdk-navigation";
2525
import { buildDocsApiPath } from "@/lib/url-base";
26+
import { buildMarkdownAlternatePath } from "@/lib/markdown-alternate";
2627

2728
import { staticCacheMiddleware } from "@/lib/static-cache-middleware";
2829

@@ -68,7 +69,18 @@ export const Route = createFileRoute("/$")({
6869
{ name: "twitter:description", content: pageDescription },
6970
{ name: "twitter:image", content: DEFAULT_SOCIAL_IMAGE_URL },
7071
],
71-
links: [{ rel: "canonical", href: canonicalUrl }],
72+
links: [
73+
{ rel: "canonical", href: canonicalUrl },
74+
...(loaderData
75+
? [
76+
{
77+
rel: "alternate",
78+
type: "text/markdown",
79+
href: buildMarkdownAlternatePath(loaderData.url),
80+
},
81+
]
82+
: []),
83+
],
7284
};
7385
},
7486
});
@@ -138,8 +150,8 @@ const clientLoader = browserCollections.docs.createClientLoader({
138150
<DocsTitle>{frontmatter.title}</DocsTitle>
139151
<DocsDescription>{frontmatter.description}</DocsDescription>
140152
<div className="flex flex-row gap-2 items-center border-b -mt-4 pb-6">
141-
<LLMCopyButton markdownUrl={`${url}.mdx`} />
142-
<ViewOptions markdownUrl={`${url}.mdx`} githubUrl={githubUrl} />
153+
<LLMCopyButton markdownUrl={buildMarkdownAlternatePath(url)} />
154+
<ViewOptions markdownUrl={buildMarkdownAlternatePath(url)} githubUrl={githubUrl} />
143155
</div>
144156
<DocsBody>
145157
<MDX components={getMDXComponents()} />

0 commit comments

Comments
 (0)