Skip to content

Commit fcd8ca1

Browse files
committed
feat(llms-txt): add middleware for versioned, org, and root routes
Add server middleware to handle llms.txt routes that Nitro's radix3 file-based router cannot resolve (parameterized intermediate segments don't match literal children). Handles versioned package paths, org-level package listings, and root /llms.txt discovery page. Remove broken versioned route files and add llms_full.txt routes.
1 parent d2e0a30 commit fcd8ca1

5 files changed

Lines changed: 100 additions & 2 deletions

File tree

server/middleware/llms-txt.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import * as v from 'valibot'
2+
import { PackageRouteParamsSchema } from '#shared/schemas/package'
3+
import { handleApiError } from '#server/utils/error-handler'
4+
import { handleLlmsTxt, handleOrgLlmsTxt, generateRootLlmsTxt } from '#server/utils/llms-txt'
5+
6+
/**
7+
* Middleware to handle llms.txt / llms_full.txt routes that can't be served
8+
* by Nitro's file-based routing (versioned paths hit a radix3 limitation
9+
* where parameterized intermediate segments don't resolve literal children).
10+
*
11+
* Handles:
12+
* - /llms.txt (root — file-based route is blocked by canonical-redirects)
13+
* - /package/:name/v/:version/llms.txt
14+
* - /package/:name/v/:version/llms_full.txt
15+
* - /package/@:org/:name/v/:version/llms.txt
16+
* - /package/@:org/:name/v/:version/llms_full.txt
17+
* - /package/@:org/llms.txt (org listing)
18+
*
19+
* Non-versioned package routes are left to file-based handlers.
20+
*/
21+
export default defineEventHandler(async event => {
22+
const path = event.path.split('?')[0]
23+
24+
if (!path.endsWith('/llms.txt') && !path.endsWith('/llms_full.txt')) return
25+
26+
const full = path.endsWith('/llms_full.txt')
27+
const suffix = full ? '/llms_full.txt' : '/llms.txt'
28+
29+
// Root /llms.txt
30+
if (path === '/llms.txt') {
31+
const url = getRequestURL(event)
32+
const baseUrl = `${url.protocol}//${url.host}`
33+
setHeader(event, 'Content-Type', 'text/markdown; charset=utf-8')
34+
setHeader(event, 'Cache-Control', 's-maxage=3600, stale-while-revalidate=86400')
35+
return generateRootLlmsTxt(baseUrl)
36+
}
37+
38+
if (!path.startsWith('/package/')) return
39+
40+
// Strip /package/ prefix and /llms[_full].txt suffix
41+
const inner = path.slice('/package/'.length, -suffix.length)
42+
43+
// Org-level: /package/@org/llms.txt (inner = "@org")
44+
if (!full && inner.startsWith('@') && !inner.includes('/')) {
45+
const orgName = inner.slice(1)
46+
try {
47+
const url = getRequestURL(event)
48+
const baseUrl = `${url.protocol}//${url.host}`
49+
const content = await handleOrgLlmsTxt(orgName, baseUrl)
50+
setHeader(event, 'Content-Type', 'text/markdown; charset=utf-8')
51+
setHeader(event, 'Cache-Control', 's-maxage=3600, stale-while-revalidate=86400')
52+
return content
53+
} catch (error: unknown) {
54+
handleApiError(error, { statusCode: 502, message: 'Failed to generate org llms.txt.' })
55+
}
56+
}
57+
58+
// Versioned paths — only handle if /v/ is present (non-versioned are handled by file routes)
59+
if (!inner.includes('/v/')) return
60+
61+
let rawPackageName: string
62+
let rawVersion: string
63+
64+
if (inner.startsWith('@')) {
65+
// Scoped: @org/name/v/version
66+
const match = inner.match(/^(@[^/]+\/[^/]+)\/v\/(.+)$/)
67+
if (!match) return
68+
rawPackageName = match[1]
69+
rawVersion = match[2]
70+
} else {
71+
// Unscoped: name/v/version
72+
const match = inner.match(/^([^/]+)\/v\/(.+)$/)
73+
if (!match) return
74+
rawPackageName = match[1]
75+
rawVersion = match[2]
76+
}
77+
78+
try {
79+
const { packageName, version } = v.parse(PackageRouteParamsSchema, {
80+
packageName: rawPackageName,
81+
version: rawVersion,
82+
})
83+
84+
const content = await handleLlmsTxt(packageName, version, { includeAgentFiles: full })
85+
setHeader(event, 'Content-Type', 'text/markdown; charset=utf-8')
86+
setHeader(event, 'Cache-Control', 's-maxage=3600, stale-while-revalidate=86400')
87+
return content
88+
} catch (error: unknown) {
89+
handleApiError(error, {
90+
statusCode: 502,
91+
message: `Failed to generate ${full ? 'llms_full.txt' : 'llms.txt'}.`,
92+
})
93+
}
94+
})
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { createPackageLlmsTxtHandler } from '#server/utils/llms-txt'
2+
3+
export default createPackageLlmsTxtHandler({ full: true })

server/routes/package/[name]/v/[version]/llms.txt.get.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { createPackageLlmsTxtHandler } from '#server/utils/llms-txt'
2+
3+
export default createPackageLlmsTxtHandler({ full: true })

server/routes/package/[org]/[name]/v/[version]/llms.txt.get.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)