|
| 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 | +}) |
0 commit comments