Update pnpm-lock.yaml and middleware for LLM redirects

### Summary of Changes
- Added `rimraf` as a devDependency.
- Updated `zod` version from 4.1.12 to 4.3.6 in multiple dependencies.
- Introduced redirects in middleware for `/SKILL.md` and `/SKILLS.md` to `/llms.txt`.
- Created new routes for handling LLM text retrieval and redirection.
- Updated `llms.txt` to include sorted URLs for documentation and API pages.

### Context
These changes enhance the documentation routing and ensure compatibility with the latest package versions.
This commit is contained in:
mantrakp04 2026-03-20 14:26:19 -07:00
parent 1e44a8c5ba
commit 67515d477a
6 changed files with 181 additions and 49 deletions

View File

@ -99,3 +99,9 @@ A: Update affected inline snapshots in `apps/e2e/tests/backend/endpoints/api/v1/
Q: How should `createOrUpdateProjectWithLegacyConfig` handle `onboardingStatus` for forward-compat checks?
A: Only write `onboardingStatus` when the `Project.onboardingStatus` column exists (for example by checking `information_schema.columns` in-transaction) so current code can still run against older schemas where that column is absent.
Q: How can this docs app use Fumadocs `llms()` without upgrading the whole site to newer Fumadocs core types?
A: Keep the main docs app on `fumadocs-core@15.3.3`, add an aliased dependency like `fumadocs-core-llms: npm:fumadocs-core@16.6.17`, and call the helper from a small JS shim (`docs/lib/fumadocs-llms.js`). That isolates the newer helper from the older app-wide Fumadocs types and keeps docs typecheck/lint passing.
Q: What shape should Stack Auth docs `llms.txt` use for low-noise retrieval?
A: Use a flat deduplicated list of route URLs derived from `source.getPages()` and `apiSource.getPages()`, sorted with `stringCompare()` from `@stackframe/stack-shared/dist/utils/strings`. Keep `/llm.txt`, `/skill.md`, and `/skills.md` as redirects to `/llms.txt`.

View File

@ -0,0 +1,7 @@
import { NextResponse, type NextRequest } from 'next/server';
export const revalidate = false;
export function GET(request: NextRequest) {
return NextResponse.redirect(new URL('/llms.txt', request.url), 307);
}

View File

@ -1,25 +1,36 @@
import { getLLMText } from 'lib/get-llm-text';
import { stringCompare } from '@stackframe/stack-shared/dist/utils/strings';
import { apiSource, source } from 'lib/source';
// cached forever
export const revalidate = false;
export async function GET() {
// Get all pages from both main docs and API docs
const docsPages = source.getPages();
const apiPages = apiSource.getPages();
const docsUrls = new Set<string>();
const apiUrls = new Set<string>();
// Process all pages
const docsPromises = docsPages.map(getLLMText);
const apiPromises = apiPages.map(getLLMText);
for (const page of source.getPages()) {
docsUrls.add(`/llms${page.url}`.slice('/llms/docs/'.length));
}
const [docsContent, apiContent] = await Promise.all([
Promise.all(docsPromises),
Promise.all(apiPromises)
]);
for (const page of apiSource.getPages()) {
apiUrls.add(`/llms${page.url}`.slice('/llms/api/'.length));
}
// Combine all content
const allContent = [...docsContent, ...apiContent];
const body = [
'# Stack Auth Docs',
'docs base url: https://docs.stack-auth.com/llms/docs/',
'',
...[...docsUrls].sort((left, right) => stringCompare(left, right)),
'',
'api base url: https://docs.stack-auth.com/llms/api/',
'',
...[...apiUrls].sort((left, right) => stringCompare(left, right)),
'',
].join('\n');
return new Response(allContent.join('\n\n'));
return new Response(body, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
},
});
}

View File

@ -0,0 +1,68 @@
import { redirect } from 'next/navigation';
import { NextResponse, type NextRequest } from 'next/server';
import { getLLMText } from '../../../../lib/get-llm-text';
import { apiSource, source } from '../../../../lib/source';
export const revalidate = false;
function resolvePage(slug: string[] | undefined) {
if (slug == null || slug.length === 0) {
return null;
}
const [prefix, ...rest] = slug;
if (prefix === 'docs') {
return source.getPage(rest);
}
if (prefix === 'api') {
return apiSource.getPage(rest);
}
return source.getPage(slug) ?? apiSource.getPage(slug);
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ slug?: string[] }> },
) {
const { slug } = await params;
if (slug == null || slug.length === 0) {
return NextResponse.redirect(new URL('/llms.txt', request.url), 307);
}
const page = resolvePage(slug);
if (!page) {
redirect("/");
}
try {
return new NextResponse(await getLLMText(page), {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
},
});
} catch (error) {
console.error('Error generating LLM text:', error);
return new NextResponse('Error generating content', { status: 500 });
}
}
export function generateStaticParams() {
try {
const docsParams = source.generateParams().map((param) => ({
slug: ['docs', ...param.slug],
}));
const apiParams = apiSource.generateParams().map((param) => ({
slug: ['api', ...param.slug],
}));
return [...docsParams, ...apiParams];
} catch (error) {
console.error('Error generating static params:', error);
return [];
}
}

View File

@ -16,6 +16,15 @@ export default function middleware(request: NextRequest, event: NextFetchEvent)
runAsynchronously(trackPromise);
event.waitUntil(trackPromise);
if (
pathname === '/SKILL.md' ||
pathname === '/SKILLS.md'
) {
const url = request.nextUrl.clone();
url.pathname = '/llms.txt';
return NextResponse.redirect(url, 307);
}
// Redirect old concepts paths to new apps paths
const movedToApps = [
'api-keys',
@ -70,8 +79,9 @@ export default function middleware(request: NextRequest, event: NextFetchEvent)
export const config = {
matcher: [
'/SKILL.md',
'/SKILLS.md',
'/docs/:path*',
'/api/:path*',
],
};

View File

@ -1696,6 +1696,12 @@ importers:
specifier: ^0.20.3
version: 0.20.3(typescript@5.9.3)
packages/private:
devDependencies:
rimraf:
specifier: ^6.1.2
version: 6.1.2
packages/react:
dependencies:
'@hookform/resolvers':
@ -1969,7 +1975,7 @@ importers:
dependencies:
'@anthropic-ai/claude-agent-sdk':
specifier: ^0.2.73
version: 0.2.73(zod@4.1.12)
version: 0.2.73(zod@4.3.6)
'@inquirer/prompts':
specifier: ^7.0.0
version: 7.10.1(@types/node@20.17.6)
@ -6056,8 +6062,8 @@ packages:
peerDependencies:
'@opentelemetry/api': ^1.1.0
'@orama/orama@3.1.16':
resolution: {integrity: sha512-scSmQBD8eANlMUOglxHrN1JdSW8tDghsPuS83otqealBiIeMukCQMOf/wc0JJjDXomqwNdEQFLXLGHrU6PGxuA==}
'@orama/orama@3.1.18':
resolution: {integrity: sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA==}
engines: {node: '>= 20.0.0'}
'@oslojs/asn1@1.0.0':
@ -14029,6 +14035,9 @@ packages:
oniguruma-to-es@4.3.3:
resolution: {integrity: sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==}
oniguruma-to-es@4.3.5:
resolution: {integrity: sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==}
only-allow@1.2.1:
resolution: {integrity: sha512-M7CJbmv7UCopc0neRKdzfoGWaVZC+xC1925GitKH9EAqYFzX9//25Q7oX4+jw0tiCCj+t5l6VZh8UPH23NZkMA==}
hasBin: true
@ -14204,9 +14213,8 @@ packages:
path-to-regexp@6.2.2:
resolution: {integrity: sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==}
path-to-regexp@8.2.0:
resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==}
engines: {node: '>=16'}
path-to-regexp@8.3.0:
resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==}
path-type@3.0.0:
resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==}
@ -14982,6 +14990,9 @@ packages:
regex@6.0.1:
resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==}
regex@6.1.0:
resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==}
regexp-to-ast@0.5.0:
resolution: {integrity: sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==}
@ -15894,9 +15905,6 @@ packages:
tinycolor2@1.6.0:
resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==}
tinyexec@1.0.1:
resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==}
tinyexec@1.0.2:
resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==}
engines: {node: '>=18'}
@ -16208,6 +16216,9 @@ packages:
unist-util-visit@5.0.0:
resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==}
unist-util-visit@5.1.0:
resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==}
universalify@0.1.2:
resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
engines: {node: '>= 4.0.0'}
@ -16981,6 +16992,9 @@ packages:
zod@4.1.12:
resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==}
zod@4.3.6:
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
zustand@5.0.6:
resolution: {integrity: sha512-ihAqNeUVhe0MAD+X8M5UzqyZ9k3FFZLBTtqo6JLPwV53cbRB/mJwBI0PxcIgqhBBHlEs8G45OTDTMq3gNcLq3A==}
engines: {node: '>=12.20.0'}
@ -17116,13 +17130,13 @@ snapshots:
'@antfu/install-pkg@1.1.0':
dependencies:
package-manager-detector: 1.3.0
tinyexec: 1.0.1
tinyexec: 1.0.2
'@antfu/utils@8.1.1': {}
'@anthropic-ai/claude-agent-sdk@0.2.73(zod@4.1.12)':
'@anthropic-ai/claude-agent-sdk@0.2.73(zod@4.3.6)':
dependencies:
zod: 4.1.12
zod: 4.3.6
optionalDependencies:
'@img/sharp-darwin-arm64': 0.34.4
'@img/sharp-darwin-x64': 0.34.4
@ -17138,7 +17152,7 @@ snapshots:
dependencies:
'@jsdevtools/ono': 7.1.3
'@types/json-schema': 7.0.15
js-yaml: 4.1.0
js-yaml: 4.1.1
'@assistant-ui/react-ai-sdk@0.10.14(@assistant-ui/react@0.10.24(@types/react-dom@18.3.1)(@types/react@18.3.12)(immer@9.0.21)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)))(@types/react-dom@18.3.1)(@types/react@18.3.12)(immer@9.0.21)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3))':
dependencies:
@ -19565,7 +19579,7 @@ snapshots:
fumadocs-core: 15.3.3(@types/react@18.3.12)(next@15.5.10(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
gray-matter: 4.0.3
react: 18.3.1
zod: 4.1.12
zod: 4.3.6
optionalDependencies:
'@types/react': 18.3.12
transitivePeerDependencies:
@ -20001,7 +20015,7 @@ snapshots:
unified: 11.0.5
unist-util-position-from-estree: 2.0.0
unist-util-stringify-position: 4.0.0
unist-util-visit: 5.0.0
unist-util-visit: 5.1.0
vfile: 6.0.3
transitivePeerDependencies:
- supports-color
@ -21233,7 +21247,7 @@ snapshots:
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0)
'@orama/orama@3.1.16': {}
'@orama/orama@3.1.18': {}
'@oslojs/asn1@1.0.0':
dependencies:
@ -24680,7 +24694,7 @@ snapshots:
dependencies:
'@shikijs/types': 3.14.0
'@shikijs/vscode-textmate': 10.0.2
oniguruma-to-es: 4.3.3
oniguruma-to-es: 4.3.5
'@shikijs/engine-javascript@3.6.0':
dependencies:
@ -24713,7 +24727,7 @@ snapshots:
hast-util-to-string: 3.0.1
shiki: 3.14.0
unified: 11.0.5
unist-util-visit: 5.0.0
unist-util-visit: 5.1.0
'@shikijs/themes@3.14.0':
dependencies:
@ -29427,7 +29441,7 @@ snapshots:
fumadocs-core@15.3.3(@types/react@18.3.12)(next@15.5.10(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@formatjs/intl-localematcher': 0.6.2
'@orama/orama': 3.1.16
'@orama/orama': 3.1.18
'@shikijs/rehype': 3.14.0
'@shikijs/transformers': 3.14.0
github-slugger: 2.0.0
@ -29440,7 +29454,7 @@ snapshots:
remark-gfm: 4.0.1
scroll-into-view-if-needed: 3.1.0
shiki: 3.14.0
unist-util-visit: 5.0.0
unist-util-visit: 5.1.0
optionalDependencies:
next: 15.5.10(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
@ -29452,7 +29466,7 @@ snapshots:
fumadocs-core@15.3.4(@types/react@18.3.12)(next@15.5.10(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@formatjs/intl-localematcher': 0.6.2
'@orama/orama': 3.1.16
'@orama/orama': 3.1.18
'@shikijs/rehype': 3.14.0
'@shikijs/transformers': 3.14.0
github-slugger: 2.0.0
@ -29465,7 +29479,7 @@ snapshots:
remark-gfm: 4.0.1
scroll-into-view-if-needed: 3.1.0
shiki: 3.14.0
unist-util-visit: 5.0.0
unist-util-visit: 5.1.0
optionalDependencies:
next: 15.5.10(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
@ -29477,7 +29491,7 @@ snapshots:
fumadocs-mdx@11.6.4(@fumadocs/mdx-remote@1.4.0(@types/react@18.3.12)(fumadocs-core@15.3.3(@types/react@18.3.12)(next@15.5.10(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1))(fumadocs-core@15.3.3(@types/react@18.3.12)(next@15.5.10(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(next@15.5.10(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)):
dependencies:
'@mdx-js/mdx': 3.1.1
'@standard-schema/spec': 1.0.0
'@standard-schema/spec': 1.1.0
chokidar: 4.0.3
cross-spawn: 7.0.6
esbuild: 0.25.11
@ -29485,11 +29499,11 @@ snapshots:
fast-glob: 3.3.3
fumadocs-core: 15.3.3(@types/react@18.3.12)(next@15.5.10(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
gray-matter: 4.0.3
js-yaml: 4.1.0
js-yaml: 4.1.1
lru-cache: 11.2.2
next: 15.5.10(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
picocolors: 1.1.1
unist-util-visit: 5.0.0
unist-util-visit: 5.1.0
zod: 3.25.76
optionalDependencies:
'@fumadocs/mdx-remote': 1.4.0(@types/react@18.3.12)(fumadocs-core@15.3.3(@types/react@18.3.12)(next@15.5.10(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
@ -29787,7 +29801,7 @@ snapshots:
glob@13.0.0:
dependencies:
minimatch: 10.1.1
minimatch: 10.2.4
minipass: 7.1.2
path-scurry: 2.0.0
@ -31106,7 +31120,7 @@ snapshots:
micromark-util-sanitize-uri: 2.0.1
trim-lines: 3.0.1
unist-util-position: 5.0.0
unist-util-visit: 5.0.0
unist-util-visit: 5.1.0
vfile: 6.0.3
mdast-util-to-markdown@2.1.2:
@ -31118,7 +31132,7 @@ snapshots:
mdast-util-to-string: 4.0.0
micromark-util-classify-character: 2.0.1
micromark-util-decode-string: 2.0.1
unist-util-visit: 5.0.0
unist-util-visit: 5.1.0
zwitch: 2.0.4
mdast-util-to-string@4.0.0:
@ -32048,7 +32062,7 @@ snapshots:
consola: 3.4.2
pathe: 2.0.3
pkg-types: 2.3.0
tinyexec: 1.0.1
tinyexec: 1.0.2
oauth4webapi@3.8.3: {}
@ -32157,6 +32171,12 @@ snapshots:
regex: 6.0.1
regex-recursion: 6.0.2
oniguruma-to-es@4.3.5:
dependencies:
oniguruma-parser: 0.12.1
regex: 6.1.0
regex-recursion: 6.0.2
only-allow@1.2.1:
dependencies:
which-pm-runs: 1.1.0
@ -32343,7 +32363,7 @@ snapshots:
path-to-regexp@6.2.2: {}
path-to-regexp@8.2.0: {}
path-to-regexp@8.3.0: {}
path-type@3.0.0:
dependencies:
@ -32930,7 +32950,7 @@ snapshots:
remark-parse: 11.0.0
remark-rehype: 11.1.2
unified: 11.0.5
unist-util-visit: 5.0.0
unist-util-visit: 5.1.0
vfile: 6.0.3
transitivePeerDependencies:
- supports-color
@ -33328,6 +33348,10 @@ snapshots:
dependencies:
regex-utilities: 2.3.0
regex@6.1.0:
dependencies:
regex-utilities: 2.3.0
regexp-to-ast@0.5.0: {}
regexp.prototype.flags@1.5.3:
@ -33684,7 +33708,7 @@ snapshots:
depd: 2.0.0
is-promise: 4.0.0
parseurl: 1.3.3
path-to-regexp: 8.2.0
path-to-regexp: 8.3.0
transitivePeerDependencies:
- supports-color
@ -34606,8 +34630,6 @@ snapshots:
tinycolor2@1.6.0: {}
tinyexec@1.0.1: {}
tinyexec@1.0.2: {}
tinyglobby@0.2.15:
@ -34926,6 +34948,12 @@ snapshots:
unist-util-is: 6.0.0
unist-util-visit-parents: 6.0.1
unist-util-visit@5.1.0:
dependencies:
'@types/unist': 3.0.3
unist-util-is: 6.0.0
unist-util-visit-parents: 6.0.1
universalify@0.1.2: {}
universalify@0.2.0: {}
@ -35693,6 +35721,8 @@ snapshots:
zod@4.1.12: {}
zod@4.3.6: {}
zustand@5.0.6(@types/react@18.3.12)(immer@9.0.21)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)):
optionalDependencies:
'@types/react': 18.3.12