Skip to content

Commit dd07537

Browse files
committed
fix(llms-txt): move all handling to middleware for Vercel compatibility
Vercel ISR glob rules (/package/**/llms.txt) create catch-all serverless functions that intercept requests before Nitro's file-based routes can resolve them, breaking scoped packages and versioned paths. Move all llms.txt/llms_full.txt handling into the middleware, remove ISR route rules, and delete file-based route files.
1 parent 8911b38 commit dd07537

File tree

7 files changed

+41
-104
lines changed

7 files changed

+41
-104
lines changed

nuxt.config.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,6 @@ export default defineNuxtConfig({
112112
'/api/registry/file/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } },
113113
'/api/registry/provenance/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } },
114114
'/api/registry/files/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } },
115-
'/package/**/llms.txt': { isr: 3600 },
116-
'/package/**/llms_full.txt': { isr: 3600 },
117-
'/llms.txt': { isr: 3600 },
118115
'/api/registry/package-meta/**': { isr: 300 },
119116
'/:pkg/.well-known/skills/**': { isr: 3600 },
120117
'/:scope/:pkg/.well-known/skills/**': { isr: 3600 },

server/middleware/llms-txt.ts

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,27 @@ import { PackageRouteParamsSchema } from '#shared/schemas/package'
33
import { handleApiError } from '#server/utils/error-handler'
44
import { 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
*/
2128
export 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, {

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

Lines changed: 0 additions & 3 deletions
This file was deleted.

server/routes/package/[name]/llms_full.txt.get.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.

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

Lines changed: 0 additions & 3 deletions
This file was deleted.

server/routes/package/[org]/[name]/llms_full.txt.get.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.

server/utils/llms-txt.ts

Lines changed: 1 addition & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
1-
import * as v from 'valibot'
21
import type { Packument } from '@npm/types'
32
import type { JsDelivrFileNode, AgentFile, LlmsTxtResult } from '#shared/types'
4-
import { PackageRouteParamsSchema } from '#shared/schemas/package'
5-
import {
6-
NPM_MISSING_README_SENTINEL,
7-
NPM_REGISTRY,
8-
CACHE_MAX_AGE_ONE_HOUR,
9-
} from '#shared/utils/constants'
10-
import { handleApiError } from '#server/utils/error-handler'
3+
import { NPM_MISSING_README_SENTINEL, NPM_REGISTRY } from '#shared/utils/constants'
114

125
/** Well-known agent instruction files at the package root */
136
const ROOT_AGENT_FILES: Record<string, string> = {
@@ -368,55 +361,3 @@ export function generateRootLlmsTxt(baseUrl: string): string {
368361

369362
return lines.join('\n').trimEnd() + '\n'
370363
}
371-
372-
/**
373-
* Create a cached event handler for package-level llms.txt or llms_full.txt.
374-
*
375-
* Each route file should call this factory and `export default` the result.
376-
* This avoids the re-export pattern that Nitro doesn't register as routes.
377-
*/
378-
export function createPackageLlmsTxtHandler(options?: { full?: boolean }) {
379-
const full = options?.full ?? false
380-
381-
return defineCachedEventHandler(
382-
async event => {
383-
const org = getRouterParam(event, 'org')
384-
const name = getRouterParam(event, 'name')
385-
const rawVersion = getRouterParam(event, 'version')
386-
387-
if (!name) {
388-
throw createError({ statusCode: 404, message: 'Package name is required.' })
389-
}
390-
391-
const rawPackageName = org ? `${org}/${name}` : name
392-
393-
try {
394-
const { packageName, version } = v.parse(PackageRouteParamsSchema, {
395-
packageName: rawPackageName,
396-
version: rawVersion,
397-
})
398-
399-
const content = await handleLlmsTxt(packageName, version, { includeAgentFiles: full })
400-
setHeader(event, 'Content-Type', 'text/markdown; charset=utf-8')
401-
return content
402-
} catch (error: unknown) {
403-
handleApiError(error, {
404-
statusCode: 502,
405-
message: `Failed to generate ${full ? 'llms_full.txt' : 'llms.txt'}.`,
406-
})
407-
}
408-
},
409-
{
410-
maxAge: CACHE_MAX_AGE_ONE_HOUR,
411-
swr: true,
412-
getKey: event => {
413-
const org = getRouterParam(event, 'org')
414-
const name = getRouterParam(event, 'name')
415-
const version = getRouterParam(event, 'version')
416-
const pkg = org ? `${org}/${name}` : name
417-
const prefix = full ? 'llms-full-txt' : 'llms-txt'
418-
return version ? `${prefix}:${pkg}@${version}` : `${prefix}:${pkg}`
419-
},
420-
},
421-
)
422-
}

0 commit comments

Comments
 (0)