Skip to content

Commit 9437e27

Browse files
authored
fix(llm): honor llm:false on markdown routes and share the public .md url (#107)
The llms.mdx route served pages that opt out of LLM surfaces with llm: false frontmatter (changelog entries, cookbook author pages) at .md and content-negotiated URLs. It now 404s them via shouldIncludeLLMPage, matching llms-full.txt and the llms.txt generator. The LLM share hook returned the internal /llms.mdx rewrite target. It now returns the public convention: the page path with a .md suffix, with the docs root pointing at /llms.txt since the landing page has no markdown source.
1 parent 0049665 commit 9437e27

5 files changed

Lines changed: 82 additions & 17 deletions

File tree

app/llms.mdx/[[...slug]]/route.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { notFound } from 'next/navigation';
22
import { type NextRequest, NextResponse } from 'next/server';
3-
import { getLLMText } from '@/lib/get-llm-text';
3+
import { getLLMText, shouldIncludeLLMPage } from '@/lib/get-llm-text';
44
import { appendMarkdownVaryHeader } from '@/lib/markdown-negotiation';
55
import { source } from '@/lib/source';
66

@@ -20,6 +20,9 @@ export async function GET(_req: NextRequest, { params }: { params: Promise<{ slu
2020
const page = getPage(slug);
2121
if (!page) notFound();
2222

23+
// Pages opted out of LLM surfaces (llm: false) are not served as markdown.
24+
if (!shouldIncludeLLMPage(page)) notFound();
25+
2326
// This markdown duplicates the canonical HTML page, so keep it out of search
2427
// results. Crawlers may still fetch it: noindex only suppresses indexing.
2528
const headers = new Headers({

hooks/use-llms-txt.ts

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -47,24 +47,27 @@ export function useLLMsTxt() {
4747
});
4848
}
4949

50-
// Hook for getting the current page's markdown URL
51-
export function useCurrentPageMarkdown() {
52-
const pathname = usePathname();
50+
// Builds the public markdown URL for a page: the page path with a .md suffix
51+
// (e.g. /overview/intro.md). The docs root has no markdown page, so it points
52+
// at the /llms.txt index instead. Pure so the convention is unit-testable.
53+
export function getPublicMarkdownUrl(pathname: string | null, origin: string) {
54+
if (!pathname) return "";
5355

54-
const getMarkdownUrl = () => {
55-
if (!pathname) return "";
56+
// Remove /docs prefix if present
57+
let basePath = pathname;
58+
if (basePath.startsWith("/docs")) {
59+
basePath = basePath.substring(5);
60+
}
5661

57-
// Remove /docs prefix if present
58-
let basePath = pathname;
59-
if (basePath.startsWith("/docs")) {
60-
basePath = basePath.substring(5);
61-
}
62+
const mdPath = basePath.startsWith("/") ? basePath : "/" + basePath;
6263

63-
const mdPath = basePath.startsWith("/") ? basePath : "/" + basePath;
64+
return mdPath === "/" ? `${origin}/llms.txt` : `${origin}${mdPath}.md`;
65+
}
6466

65-
const baseUrl = typeof window !== "undefined" ? window.location.origin : "";
66-
return `${baseUrl}/llms.mdx${mdPath === "/" ? "" : mdPath}`;
67-
};
67+
// Hook for getting the current page's markdown URL
68+
export function useCurrentPageMarkdown() {
69+
const pathname = usePathname();
6870

69-
return getMarkdownUrl();
71+
const origin = typeof window !== "undefined" ? window.location.origin : "";
72+
return getPublicMarkdownUrl(pathname, origin);
7073
}

tests/e2e/llm-endpoints.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,13 @@ describe('.md suffix end-to-end', () => {
472472
expect(response.status).toBe(404);
473473
});
474474

475+
test('returns 404 for an llm: false page at its .md URL', async () => {
476+
const response = await fetch(`${BASE_URL}/cookbook/authors/hussufo.md`, {
477+
headers: BROWSER_HEADERS,
478+
});
479+
expect(response.status).toBe(404);
480+
});
481+
475482
test('still serves HTML at the canonical URL for browsers', async () => {
476483
const response = await fetch(`${BASE_URL}/overview/sessions-api/quickstart`, {
477484
headers: BROWSER_HEADERS,

tests/get-llm-text.test.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// ABOUTME: Tests for getLLMText, which renders a page as LLM-facing markdown
22
// ABOUTME: with an absolute URL and an optional llms.txt index pointer.
33
import { describe, expect, test } from 'bun:test';
4-
import { getLLMText, LLMS_INDEX_POINTER } from '../lib/get-llm-text';
4+
import { getLLMText, LLMS_INDEX_POINTER, shouldIncludeLLMPage } from '../lib/get-llm-text';
55

66
// Minimal stand-in for a fumadocs page; typed any since building a real
77
// InferPageType requires the full fumadocs loader.
@@ -16,6 +16,26 @@ function fakePage(overrides: Record<string, unknown> = {}): any {
1616
};
1717
}
1818

19+
describe('shouldIncludeLLMPage', () => {
20+
test('includes pages by default', () => {
21+
expect(shouldIncludeLLMPage(fakePage())).toBe(true);
22+
});
23+
24+
test('excludes pages with llm: false in page data', () => {
25+
expect(shouldIncludeLLMPage(fakePage({ llm: false }))).toBe(false);
26+
});
27+
28+
test('excludes pages with llm: false in raw frontmatter', () => {
29+
const content = ['---', 'title: Hidden', 'llm: false', '---', '', 'Body.'].join('\n');
30+
expect(shouldIncludeLLMPage(fakePage({ content }))).toBe(false);
31+
});
32+
33+
test('keeps pages whose frontmatter sets llm: true', () => {
34+
const content = ['---', 'title: Visible', 'llm: true', '---', '', 'Body.'].join('\n');
35+
expect(shouldIncludeLLMPage(fakePage({ content }))).toBe(true);
36+
});
37+
});
38+
1939
describe('getLLMText', () => {
2040
test('renders the title and an absolute URL', async () => {
2141
const text = await getLLMText(fakePage());

tests/use-llms-txt.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// ABOUTME: Tests the public markdown share URL built for the LLM share UI:
2+
// ABOUTME: page path + .md suffix, with the docs root pointing at /llms.txt.
3+
import { describe, expect, test } from 'bun:test';
4+
import { getPublicMarkdownUrl } from '../hooks/use-llms-txt';
5+
6+
const ORIGIN = 'https://docs.steel.dev';
7+
8+
describe('getPublicMarkdownUrl', () => {
9+
test('appends .md to the page path', () => {
10+
expect(getPublicMarkdownUrl('/overview/sessions-api/quickstart', ORIGIN)).toBe(
11+
'https://docs.steel.dev/overview/sessions-api/quickstart.md',
12+
);
13+
});
14+
15+
test('never exposes the internal /llms.mdx rewrite target', () => {
16+
expect(getPublicMarkdownUrl('/overview/intro', ORIGIN)).not.toContain('/llms.mdx');
17+
});
18+
19+
test('strips a legacy /docs prefix before appending .md', () => {
20+
expect(getPublicMarkdownUrl('/docs/overview/intro', ORIGIN)).toBe(
21+
'https://docs.steel.dev/overview/intro.md',
22+
);
23+
});
24+
25+
test('points the docs root at the llms.txt index', () => {
26+
expect(getPublicMarkdownUrl('/', ORIGIN)).toBe('https://docs.steel.dev/llms.txt');
27+
});
28+
29+
test('returns an empty string without a pathname', () => {
30+
expect(getPublicMarkdownUrl(null, ORIGIN)).toBe('');
31+
});
32+
});

0 commit comments

Comments
 (0)