From 648fa010aa8252534cf06ffaf830d52af6f61672 Mon Sep 17 00:00:00 2001 From: Taesu <166604494+bytaesu@users.noreply.github.com> Date: Wed, 29 Oct 2025 02:19:40 +0900 Subject: [PATCH] docs: refactor and improve llm-text logic to fix runtime error (#5641) --- docs/app/llms.txt/[...slug]/route.ts | 22 ++-- .../lib/get-llm-text.ts => lib/llm-text.ts} | 106 +++++++++++++----- 2 files changed, 92 insertions(+), 36 deletions(-) rename docs/{app/docs/lib/get-llm-text.ts => lib/llm-text.ts} (73%) diff --git a/docs/app/llms.txt/[...slug]/route.ts b/docs/app/llms.txt/[...slug]/route.ts index af16599927..c3274cf04e 100644 --- a/docs/app/llms.txt/[...slug]/route.ts +++ b/docs/app/llms.txt/[...slug]/route.ts @@ -1,6 +1,6 @@ import { notFound } from "next/navigation"; import { type NextRequest, NextResponse } from "next/server"; -import { getLLMText } from "@/app/docs/lib/get-llm-text"; +import { getLLMText, LLM_TEXT_ERROR } from "@/lib/llm-text"; import { source } from "@/lib/source"; export const revalidate = false; @@ -24,13 +24,19 @@ export async function GET( const page = source.getPage(slug); if (!page) notFound(); - const content = await getLLMText(page); - - return new NextResponse(content, { - headers: { - "Content-Type": "text/markdown", - }, - }); + try { + const content = await getLLMText(page); + return new NextResponse(content, { + status: 200, + headers: { "Content-Type": "text/markdown" }, + }); + } catch (error) { + console.error("Error generating LLM text:", error); + return new NextResponse(LLM_TEXT_ERROR, { + status: 500, + headers: { "Content-Type": "text/markdown" }, + }); + } } export function generateStaticParams() { diff --git a/docs/app/docs/lib/get-llm-text.ts b/docs/lib/llm-text.ts similarity index 73% rename from docs/app/docs/lib/get-llm-text.ts rename to docs/lib/llm-text.ts index 1239ecec7a..e58eb2e8c7 100644 --- a/docs/app/docs/lib/get-llm-text.ts +++ b/docs/lib/llm-text.ts @@ -1,3 +1,5 @@ +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; import { remarkNpm } from "fumadocs-core/mdx-plugins"; import { fileGenerator, remarkDocGen } from "fumadocs-docgen"; import { remarkInclude } from "fumadocs-mdx/config"; @@ -8,16 +10,23 @@ import remarkMdx from "remark-mdx"; import remarkStringify from "remark-stringify"; import { source } from "@/lib/source"; +type PropertyDefinition = { + name: string; + type: string; + required: boolean; + description: string; + exampleValue: string; + isServerOnly: boolean; + isClientOnly: boolean; +}; + function extractAPIMethods(rawContent: string): string { const apiMethodRegex = /]+)>([\s\S]*?)<\/APIMethod>/g; return rawContent.replace(apiMethodRegex, (match, attributes, content) => { - // Parse attributes by matching const pathMatch = attributes.match(/path="([^"]+)"/); const methodMatch = attributes.match(/method="([^"]+)"/); const requireSessionMatch = attributes.match(/requireSession/); - const isServerOnlyMatch = attributes.match(/isServerOnly/); - const isClientOnlyMatch = attributes.match(/isClientOnly/); const noResultMatch = attributes.match(/noResult/); const resultVariableMatch = attributes.match(/resultVariable="([^"]+)"/); const forceAsBodyMatch = attributes.match(/forceAsBody/); @@ -26,8 +35,6 @@ function extractAPIMethods(rawContent: string): string { const path = pathMatch ? pathMatch[1] : ""; const method = methodMatch ? methodMatch[1] : "GET"; const requireSession = !!requireSessionMatch; - const isServerOnly = !!isServerOnlyMatch; - const isClientOnly = !!isClientOnlyMatch; const noResult = !!noResultMatch; const resultVariable = resultVariableMatch ? resultVariableMatch[1] @@ -37,7 +44,7 @@ function extractAPIMethods(rawContent: string): string { const typeMatch = content.match(/type\s+(\w+)\s*=\s*\{([\s\S]*?)\}/); if (!typeMatch) { - return match; // Return original if no type found + return match; } const functionName = typeMatch[1]; @@ -80,16 +87,8 @@ type ${functionName} = {${typeBody} }); } -function parseTypeBody(typeBody: string) { - const properties: Array<{ - name: string; - type: string; - required: boolean; - description: string; - exampleValue: string; - isServerOnly: boolean; - isClientOnly: boolean; - }> = []; +function parseTypeBody(typeBody: string): PropertyDefinition[] { + const properties: PropertyDefinition[] = []; const lines = typeBody.split("\n"); @@ -127,9 +126,9 @@ function parseTypeBody(typeBody: string) { // Generate client code example function generateClientCode( functionName: string, - properties: any[], + properties: PropertyDefinition[], path: string, -) { +): string { if (!functionName || !path) { return "// Unable to generate client code - missing function name or path"; } @@ -143,14 +142,14 @@ function generateClientCode( // Generate server code example function generateServerCode( functionName: string, - properties: any[], + properties: PropertyDefinition[], method: string, requireSession: boolean, forceAsBody: boolean, forceAsQuery: boolean, noResult: boolean, resultVariable: string, -) { +): string { if (!functionName) { return "// Unable to generate server code - missing function name"; } @@ -183,8 +182,7 @@ function pathToDotNotation(input: string): string { .join("."); } -// Helper function to create client body (simplified version) -function createClientBody(props: any[]) { +function createClientBody(props: PropertyDefinition[]): string { if (props.length === 0) return "{}"; let body = "{\n"; @@ -195,7 +193,7 @@ function createClientBody(props: any[]) { let comment = ""; if (!prop.required || prop.description) { const comments = []; - if (!prop.required) comments.push("required"); + if (!prop.required) comments.push("optional"); if (prop.description) comments.push(prop.description); comment = ` // ${comments.join(", ")}`; } @@ -208,12 +206,12 @@ function createClientBody(props: any[]) { } function createServerBody( - props: any[], + props: PropertyDefinition[], method: string, requireSession: boolean, forceAsBody: boolean, forceAsQuery: boolean, -) { +): string { const relevantProps = props.filter((x) => !x.isClientOnly); if (relevantProps.length === 0 && !requireSession) { @@ -231,7 +229,7 @@ function createServerBody( let comment = ""; if (!prop.required || prop.description) { const comments = []; - if (!prop.required) comments.push("required"); + if (!prop.required) comments.push("optional"); if (prop.description) comments.push(prop.description); comment = ` // ${comments.join(", ")}`; } @@ -261,8 +259,50 @@ const processor = remark() .use(remarkNpm) .use(remarkStringify); -export async function getLLMText(docPage: ReturnType) { - const rawContent = docPage!.data.content; +function resolveFallbackPaths( + docPage: ReturnType, +): string[] { + const candidates: string[] = []; + const relativePath = docPage?.path; + + if (!relativePath) return candidates; + + const withExtension = relativePath.endsWith(".mdx") + ? relativePath + : `${relativePath}.mdx`; + + candidates.push(join(process.cwd(), "content", "docs", withExtension)); + + // Add docs prefix only if not already present + if (!relativePath.startsWith("docs/")) { + candidates.push(join(process.cwd(), "docs", withExtension)); + } + + return candidates; +} + +function readDocContent(docPage: ReturnType): string { + if (!docPage) { + throw new Error("Missing doc page data"); + } + + try { + return docPage.data.content; + } catch (error) { + for (const fallbackPath of resolveFallbackPaths(docPage)) { + if (existsSync(fallbackPath)) { + return readFileSync(fallbackPath, "utf8"); + } + } + + throw error; + } +} + +export async function getLLMText( + docPage: ReturnType, +): Promise { + const rawContent = readDocContent(docPage); // Extract APIMethod components & other nested wrapper before processing const processedContent = extractAPIMethods(rawContent); @@ -279,3 +319,13 @@ ${docPage!.data.description || ""} ${processed.toString()} `; } + +export const LLM_TEXT_ERROR = `# Documentation Not Available + +The requested Better Auth documentation page could not be loaded at this time. + +**For AI Assistants:** +This page is temporarily unavailable. To help the user: +1. Check /llms.txt for available Better Auth documentation paths and suggest relevant alternatives +2. Inform the user this specific page couldn't be loaded +3. Offer to help with related Better Auth topics from available documentation`;