Skip to content

Commit 92521ee

Browse files
fix(docs): make the docs site readable to agents that fetch it (#34)
Agents pointed at the docs hit a client-rendered SPA: a wrong URL guess (e.g. /docs) returns a 200 shell whose only text is "OpenIslands", with no pointer to the plain-text docs that already exist (/llms.txt, /llms-full.txt, per-page .md). And once on a .md page, its internal links pointed back at the JS-rendered HTML routes, bouncing the agent out of markdown. - __root.tsx: signpost the plain-text docs from the root (so a JS-less fetch of any path, incl. the SPA fallback shell, finds them): a rel="alternate" text/markdown link plus a <noscript> listing /llms-full.txt, /llms.txt, the append-.md trick, and /start.md. - source.ts: linkToMarkdownSiblings() rewrites internal absolute doc links to their .md sibling (skips the root, asset files, and external links). - {$}.md route: rewrite cross-links to .md and append a footer pointing at /llms-full.txt and /llms.txt. - llms.txt route: rewrite the index links to .md and prepend a note telling the agent the .md suffix works for any page (per the llms.txt convention of highlighting markdown versions). Content negotiation (Accept: text/markdown on the same URL) is intentionally not done — it needs a request-time server, and the docs deploy is assets-only static. The .md suffix is the static-compatible equivalent.
1 parent cb31423 commit 92521ee

4 files changed

Lines changed: 64 additions & 4 deletions

File tree

apps/docs/src/lib/source.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,16 @@ export async function getLLMText(page: (typeof source)["$inferPage"]) {
3636
3737
${processed}`;
3838
}
39+
40+
const INTERNAL_DOC_LINK = /\]\((\/[^)\s#]*)(#[^)\s]*)?\)/g;
41+
42+
// An agent that opens one page's .md and follows a link should land on the next page's
43+
// .md, not the JS-rendered HTML. Rewrite internal absolute doc links to their .md sibling
44+
// (the same /foo → /foo.md mapping the site already serves), leaving the site root and
45+
// asset files (anything with an extension) and external links untouched.
46+
export function linkToMarkdownSiblings(md: string): string {
47+
return md.replace(INTERNAL_DOC_LINK, (match, path: string, anchor = "") => {
48+
if (path === "/" || /\.[a-z0-9]+$/i.test(path)) return match;
49+
return `](${path}.md${anchor})`;
50+
});
51+
}

apps/docs/src/routes/__root.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ export const Route = createRootRoute({
1919
links: [
2020
{ rel: "stylesheet", href: appCss },
2121
{ rel: "icon", href: "/favicon.svg", type: "image/svg+xml" },
22+
// Agent/LLM discovery: every page advertises the plain-text docs. Pairs with the
23+
// <noscript> below so a JS-less fetch of ANY url (incl. wrong guesses → the SPA
24+
// shell) finds /llms.txt instead of an empty "OpenIslands" page.
25+
{
26+
rel: "alternate",
27+
type: "text/markdown",
28+
href: "/llms.txt",
29+
title: "OpenIslands docs as plain text (for LLMs)",
30+
},
2231
],
2332
}),
2433
component: RootComponent,
@@ -58,6 +67,29 @@ function RootComponent() {
5867
<HeadContent />
5968
</head>
6069
<body className="flex flex-col min-h-screen">
70+
{/* Rendered into the static shell that serves every unmatched path. A JS-less
71+
client (WebFetch, crawlers, agents) sees this pointer home instead of a blank
72+
page; real JS clients never paint it. */}
73+
<noscript>
74+
<p>
75+
These docs render with JavaScript. Reading as an LLM or agent? Plain-text docs:
76+
</p>
77+
<ul>
78+
<li>
79+
<a href="/llms-full.txt">/llms-full.txt</a> — every page, one file
80+
</li>
81+
<li>
82+
<a href="/llms.txt">/llms.txt</a> — page index
83+
</li>
84+
<li>
85+
Any page as Markdown: append <code>.md</code> (e.g.{" "}
86+
<a href="/introduction.md">/introduction.md</a>)
87+
</li>
88+
<li>
89+
<a href="/start.md">/start.md</a> — agent onboarding
90+
</li>
91+
</ul>
92+
</noscript>
6193
<RootProvider theme={{ defaultTheme: "dark", enabled: false }} search={{ SearchDialog }}>
6294
<Outlet />
6395
</RootProvider>

apps/docs/src/routes/llms[.]txt.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
1-
import { source } from "@/lib/source";
1+
import { linkToMarkdownSiblings, source } from "@/lib/source";
22
import { createFileRoute } from "@tanstack/react-router";
33
import { llms } from "fumadocs-core/source";
44

5+
// llms.txt is the agent's entry point, so it points straight at the markdown: links
6+
// below are rewritten to .md, and the note tells an agent the .md suffix works for any
7+
// page it constructs itself (per the llms.txt convention of highlighting markdown).
8+
const MARKDOWN_NOTE = `> These docs are markdown-first for agents. Each link below points to a page's raw \`.md\` — append \`.md\` to any page URL yourself, too. For every page concatenated into one file, fetch [/llms-full.txt](/llms-full.txt).
9+
10+
`;
11+
512
export const Route = createFileRoute("/llms.txt")({
613
server: {
714
handlers: {
815
GET() {
9-
return new Response(llms(source).index());
16+
return new Response(MARKDOWN_NOTE + linkToMarkdownSiblings(llms(source).index()));
1017
},
1118
},
1219
},

apps/docs/src/routes/{$}[.]md.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import { createFileRoute, notFound } from "@tanstack/react-router";
2-
import { getLLMText, markdownPathToSlugs, source } from "@/lib/source";
2+
import { getLLMText, linkToMarkdownSiblings, markdownPathToSlugs, source } from "@/lib/source";
3+
4+
const MARKDOWN_FOOTER = `
5+
6+
---
7+
8+
*This is one page of the OpenIslands docs. Every page in one file: [/llms-full.txt](/llms-full.txt). Page index: [/llms.txt](/llms.txt). Links above point to \`.md\` siblings — append \`.md\` to any page URL for its raw markdown.*
9+
`;
310

411
export const Route = createFileRoute("/{$}.md")({
512
server: {
@@ -9,7 +16,8 @@ export const Route = createFileRoute("/{$}.md")({
916
const page = source.getPage(slugs);
1017
if (!page) throw notFound();
1118

12-
return new Response(await getLLMText(page), {
19+
const markdown = linkToMarkdownSiblings(await getLLMText(page)) + MARKDOWN_FOOTER;
20+
return new Response(markdown, {
1321
headers: {
1422
"Content-Type": "text/markdown",
1523
},

0 commit comments

Comments
 (0)