Skip to content

Commit 456017b

Browse files
committed
fix(llm-docs): remove versioned .md routes due to Vercel ISR conflict
Versioned .md paths (e.g. /package/nuxt/v/3.16.2.md) conflict with Vercel's ISR route rules which match /package/:name/v/:version and intercept the request before middleware can handle it. Keep .md for latest-only (unscoped and scoped).
1 parent 8d6efc1 commit 456017b

5 files changed

Lines changed: 77 additions & 35 deletions

File tree

scripts/smoke-test-llm-docs.sh

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Smoke test all llm-docs routes (llms.txt, llms_full.txt, .md)
4+
# Usage: ./scripts/smoke-test-llm-docs.sh http://localhost:3333
5+
6+
set -euo pipefail
7+
8+
BASE="${1:?Usage: $0 <base-url>}"
9+
BASE="${BASE%/}" # strip trailing slash
10+
11+
PASS=0
12+
FAIL=0
13+
14+
check() {
15+
local label="$1"
16+
local url="$2"
17+
local expect_status="${3:-200}"
18+
19+
status=$(curl -s -o /dev/null -w "%{http_code}" -L "$url")
20+
21+
if [ "$status" = "$expect_status" ]; then
22+
echo " PASS GET $url $status $label"
23+
PASS=$((PASS + 1))
24+
else
25+
echo " FAIL GET $url $status $label (expected $expect_status)"
26+
FAIL=$((FAIL + 1))
27+
fi
28+
}
29+
30+
echo "=== Root ==="
31+
check "Root llms.txt" "$BASE/llms.txt"
32+
33+
echo ""
34+
echo "=== Unscoped package (latest) ==="
35+
check "llms.txt" "$BASE/package/nuxt/llms.txt"
36+
check "llms_full.txt" "$BASE/package/nuxt/llms_full.txt"
37+
check ".md" "$BASE/package/nuxt.md"
38+
39+
echo ""
40+
echo "=== Unscoped package (versioned) ==="
41+
check "llms.txt" "$BASE/package/nuxt/v/3.16.2/llms.txt"
42+
check "llms_full.txt" "$BASE/package/nuxt/v/3.16.2/llms_full.txt"
43+
44+
echo ""
45+
echo "=== Scoped package (latest) ==="
46+
check "llms.txt" "$BASE/package/@nuxt/kit/llms.txt"
47+
check "llms_full.txt" "$BASE/package/@nuxt/kit/llms_full.txt"
48+
check ".md" "$BASE/package/@nuxt/kit.md"
49+
50+
echo ""
51+
echo "=== Scoped package (versioned) ==="
52+
check "llms.txt" "$BASE/package/@nuxt/kit/v/4.3.1/llms.txt"
53+
check "llms_full.txt" "$BASE/package/@nuxt/kit/v/4.3.1/llms_full.txt"
54+
55+
echo ""
56+
echo "=== Org-level ==="
57+
check "Org llms.txt" "$BASE/package/@nuxt/llms.txt"
58+
59+
echo ""
60+
echo "=== Shorthand redirects (follow → 200) ==="
61+
check "Unscoped .md redirect" "$BASE/nuxt.md"
62+
check "Scoped .md redirect" "$BASE/@nuxt/kit.md"
63+
check "Unscoped llms.txt redirect" "$BASE/nuxt/llms.txt"
64+
check "Scoped llms.txt redirect" "$BASE/@nuxt/kit/llms.txt"
65+
66+
echo ""
67+
echo "=== Results ==="
68+
echo " $PASS passed, $FAIL failed"
69+
exit $FAIL

server/middleware/canonical-redirects.global.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,10 @@ export default defineEventHandler(async event => {
7272
// Also handles trailing /llms.txt or /llms_full.txt suffixes
7373
const pkgVersionMatch =
7474
path.match(
75-
/^\/(?:(?<org>@[^/]+)\/)?(?<name>[^/@]+)\/v\/(?<version>[^/]+?)(?<suffix>\.md|\/(?:llms\.txt|llms_full\.txt))?$/,
75+
/^\/(?:(?<org>@[^/]+)\/)?(?<name>[^/@]+)\/v\/(?<version>[^/]+)(?<suffix>\/(?:llms\.txt|llms_full\.txt))?$/,
7676
) ||
7777
path.match(
78-
/^\/(?:(?<org>@[^/]+)\/)?(?<name>[^/@]+)@(?<version>[^/]+?)(?<suffix>\.md|\/(?:llms\.txt|llms_full\.txt))?$/,
78+
/^\/(?:(?<org>@[^/]+)\/)?(?<name>[^/@]+)@(?<version>[^/]+)(?<suffix>\/(?:llms\.txt|llms_full\.txt))?$/,
7979
)
8080

8181
if (pkgVersionMatch?.groups) {

server/middleware/llm-docs.ts

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ const CACHE_HEADER = 's-maxage=3600, stale-while-revalidate=86400'
2222
* - /llms.txt (root discovery page)
2323
* - /package/:name.md (unscoped, latest, raw README)
2424
* - /package/@:org/:name.md (scoped, latest, raw README)
25-
* - /package/:name/v/:version.md (unscoped, versioned, raw README)
26-
* - /package/@:org/:name/v/:version.md (scoped, versioned, raw README)
2725
* - /package/@:org/llms.txt (org package listing)
2826
* - /package/:name/llms.txt (unscoped, latest)
2927
* - /package/:name/llms_full.txt (unscoped, latest, full)
@@ -37,38 +35,18 @@ const CACHE_HEADER = 's-maxage=3600, stale-while-revalidate=86400'
3735
export default defineEventHandler(async event => {
3836
const path = event.path.split('?')[0] ?? '/'
3937

40-
// Handle .md routes — raw README markdown
41-
if (path.startsWith('/package/') && path.endsWith('.md')) {
42-
const inner = path.slice('/package/'.length, -'.md'.length)
43-
44-
let rawPackageName: string
45-
let rawVersion: string | undefined
46-
47-
if (inner.includes('/v/')) {
48-
if (inner.startsWith('@')) {
49-
const match = inner.match(/^(@[^/]+\/[^/]+)\/v\/(.+)$/)
50-
if (!match?.[1] || !match[2]) return
51-
rawPackageName = match[1]
52-
rawVersion = match[2]
53-
} else {
54-
const match = inner.match(/^([^/]+)\/v\/(.+)$/)
55-
if (!match?.[1] || !match[2]) return
56-
rawPackageName = match[1]
57-
rawVersion = match[2]
58-
}
59-
} else {
60-
rawPackageName = inner
61-
}
38+
// Handle .md routes — raw README markdown (latest version only)
39+
if (path.startsWith('/package/') && path.endsWith('.md') && !path.includes('/v/')) {
40+
const rawPackageName = path.slice('/package/'.length, -'.md'.length)
6241

6342
if (!rawPackageName) return
6443

6544
try {
66-
const { packageName, version } = v.parse(PackageRouteParamsSchema, {
45+
const { packageName } = v.parse(PackageRouteParamsSchema, {
6746
packageName: rawPackageName,
68-
version: rawVersion,
6947
})
7048

71-
const content = await handlePackageMd(packageName, version)
49+
const content = await handlePackageMd(packageName)
7250
setHeader(event, 'Content-Type', 'text/markdown; charset=utf-8')
7351
setHeader(event, 'Cache-Control', CACHE_HEADER)
7452
return content

server/utils/llm-docs.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -386,11 +386,7 @@ export function generateRootLlmsTxt(baseUrl: string): string {
386386
lines.push('Raw README content for a package, with no metadata wrapper.')
387387
lines.push('')
388388
lines.push(`- \`${baseUrl}/package/<name>.md\` — unscoped package (latest version)`)
389-
lines.push(`- \`${baseUrl}/package/<name>/v/<version>.md\` — unscoped package (specific version)`)
390389
lines.push(`- \`${baseUrl}/package/@<org>/<name>.md\` — scoped package (latest version)`)
391-
lines.push(
392-
`- \`${baseUrl}/package/@<org>/<name>/v/<version>.md\` — scoped package (specific version)`,
393-
)
394390
lines.push('')
395391
lines.push('## Examples')
396392
lines.push('')

test/unit/server/utils/llm-docs.spec.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,9 +337,8 @@ describe('generateRootLlmsTxt', () => {
337337
const output = generateRootLlmsTxt('https://npmx.dev')
338338

339339
expect(output).toContain('https://npmx.dev/package/<name>.md')
340-
expect(output).toContain('https://npmx.dev/package/<name>/v/<version>.md')
341340
expect(output).toContain('https://npmx.dev/package/@<org>/<name>.md')
342-
expect(output).toContain('https://npmx.dev/package/@<org>/<name>/v/<version>.md')
341+
expect(output).not.toContain('<name>/v/<version>.md')
343342
})
344343

345344
it('includes .md example links', () => {

0 commit comments

Comments
 (0)