@@ -3,20 +3,27 @@ import { PackageRouteParamsSchema } from '#shared/schemas/package'
33import { handleApiError } from '#server/utils/error-handler'
44import { handleLlmsTxt , handleOrgLlmsTxt , generateRootLlmsTxt } from '#server/utils/llms-txt'
55
6+ const CACHE_HEADER = 's-maxage=3600, stale-while-revalidate=86400'
7+
68/**
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).
9+ * Middleware to handle ALL llms.txt / llms_full.txt routes.
1010 *
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)
11+ * All llms.txt handling lives here rather than in file-based routes because
12+ * Vercel's ISR route rules with glob patterns (e.g. `/package/ ** /llms.txt`)
13+ * create catch-all serverless functions that interfere with Nitro's file-based
14+ * route resolution — scoped packages and versioned paths fail to match.
1815 *
19- * Non-versioned package routes are left to file-based handlers.
16+ * Handles:
17+ * - /llms.txt (root discovery page)
18+ * - /package/@:org/llms.txt (org package listing)
19+ * - /package/:name/llms.txt (unscoped, latest)
20+ * - /package/:name/llms_full.txt (unscoped, latest, full)
21+ * - /package/@:org/:name/llms.txt (scoped, latest)
22+ * - /package/@:org/:name/llms_full.txt (scoped, latest, full)
23+ * - /package/:name/v/:version/llms.txt (unscoped, versioned)
24+ * - /package/:name/v/:version/llms_full.txt (unscoped, versioned, full)
25+ * - /package/@:org/:name/v/:version/llms.txt (scoped, versioned)
26+ * - /package/@:org/:name/v/:version/llms_full.txt (scoped, versioned, full)
2027 */
2128export default defineEventHandler ( async event => {
2229 const path = event . path . split ( '?' ) [ 0 ]
@@ -31,7 +38,7 @@ export default defineEventHandler(async event => {
3138 const url = getRequestURL ( event )
3239 const baseUrl = `${ url . protocol } //${ url . host } `
3340 setHeader ( event , 'Content-Type' , 'text/markdown; charset=utf-8' )
34- setHeader ( event , 'Cache-Control' , 's-maxage=3600, stale-while-revalidate=86400' )
41+ setHeader ( event , 'Cache-Control' , CACHE_HEADER )
3542 return generateRootLlmsTxt ( baseUrl )
3643 }
3744
@@ -48,33 +55,37 @@ export default defineEventHandler(async event => {
4855 const baseUrl = `${ url . protocol } //${ url . host } `
4956 const content = await handleOrgLlmsTxt ( orgName , baseUrl )
5057 setHeader ( event , 'Content-Type' , 'text/markdown; charset=utf-8' )
51- setHeader ( event , 'Cache-Control' , 's-maxage=3600, stale-while-revalidate=86400' )
58+ setHeader ( event , 'Cache-Control' , CACHE_HEADER )
5259 return content
5360 } catch ( error : unknown ) {
5461 handleApiError ( error , { statusCode : 502 , message : 'Failed to generate org llms.txt.' } )
5562 }
5663 }
5764
58- // Versioned paths — only handle if /v/ is present (non-versioned are handled by file routes)
59- if ( ! inner . includes ( '/v/' ) ) return
60-
65+ // Parse package name and optional version from inner path
6166 let rawPackageName : string
62- let rawVersion : string
67+ let rawVersion : string | undefined
6368
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 ]
69+ if ( inner . includes ( '/v/' ) ) {
70+ // Versioned path
71+ if ( inner . startsWith ( '@' ) ) {
72+ const match = inner . match ( / ^ ( @ [ ^ / ] + \/ [ ^ / ] + ) \/ v \/ ( .+ ) $ / )
73+ if ( ! match ) return
74+ rawPackageName = match [ 1 ]
75+ rawVersion = match [ 2 ]
76+ } else {
77+ const match = inner . match ( / ^ ( [ ^ / ] + ) \/ v \/ ( .+ ) $ / )
78+ if ( ! match ) return
79+ rawPackageName = match [ 1 ]
80+ rawVersion = match [ 2 ]
81+ }
7082 } else {
71- // Unscoped: name/v/version
72- const match = inner . match ( / ^ ( [ ^ / ] + ) \/ v \/ ( .+ ) $ / )
73- if ( ! match ) return
74- rawPackageName = match [ 1 ]
75- rawVersion = match [ 2 ]
83+ // Latest version — inner is just the package name
84+ rawPackageName = inner
7685 }
7786
87+ if ( ! rawPackageName ) return
88+
7889 try {
7990 const { packageName, version } = v . parse ( PackageRouteParamsSchema , {
8091 packageName : rawPackageName ,
@@ -83,7 +94,7 @@ export default defineEventHandler(async event => {
8394
8495 const content = await handleLlmsTxt ( packageName , version , { includeAgentFiles : full } )
8596 setHeader ( event , 'Content-Type' , 'text/markdown; charset=utf-8' )
86- setHeader ( event , 'Cache-Control' , 's-maxage=3600, stale-while-revalidate=86400' )
97+ setHeader ( event , 'Cache-Control' , CACHE_HEADER )
8798 return content
8899 } catch ( error : unknown ) {
89100 handleApiError ( error , {
0 commit comments