From a6d86978b5023ae1526655d0503df8f32fb5c6a3 Mon Sep 17 00:00:00 2001 From: Duncan Crawbuck Date: Wed, 3 Jun 2026 16:46:18 -0700 Subject: [PATCH 1/9] Improve docs markdown negotiation --- source.config.ts | 21 +++++++- src/lib/llms.ts | 2 +- src/lib/markdown-alternate.ts | 40 +++++++++++++++ src/lib/source.ts | 75 ++++++++++++++++++++++++++--- src/routeTree.gen.ts | 64 +++++++++++++++++++++--- src/routes/$.tsx | 18 +++++-- src/routes/index.tsx | 13 ++++- src/routes/llms[.]mdx.docs.$.ts | 19 ++++++-- src/routes/llms[.]mdx.docs.index.ts | 20 ++++++++ src/routes/llms[.]mdx.docs.ts | 3 ++ src/start.test.ts | 63 +++++++++++++++++++++++- src/start.ts | 64 ++++++++++++++++++++++-- 12 files changed, 373 insertions(+), 29 deletions(-) create mode 100644 src/lib/markdown-alternate.ts create mode 100644 src/routes/llms[.]mdx.docs.index.ts create mode 100644 src/routes/llms[.]mdx.docs.ts diff --git a/source.config.ts b/source.config.ts index 0710dda..f221ede 100644 --- a/source.config.ts +++ b/source.config.ts @@ -16,7 +16,26 @@ export const docs = defineDocs({ docs: { async: isDevelopment, postprocess: { - includeProcessedMarkdown: true, + includeProcessedMarkdown: { + mdxAsPlaceholder: [ + "Accordion", + "AccordionGroup", + "Callout", + "Card", + "CardGroup", + "CodeGroup", + "Frame", + "Info", + "Mermaid", + "Note", + "Step", + "Steps", + "Tab", + "Tabs", + "Tip", + "Warning", + ], + }, }, }, }); diff --git a/src/lib/llms.ts b/src/lib/llms.ts index 6090775..c5bd6a4 100644 --- a/src/lib/llms.ts +++ b/src/lib/llms.ts @@ -51,7 +51,7 @@ export function getLLMSectionConfig(section?: string) { return llmsSectionConfigs[section as LLMSection]; } -export function getPagesForLLMSection(pages: LLMPageLike[], section?: string) { +export function getPagesForLLMSection(pages: TPage[], section?: string) { const config = getLLMSectionConfig(section); if (!config) return pages; diff --git a/src/lib/markdown-alternate.ts b/src/lib/markdown-alternate.ts new file mode 100644 index 0000000..16e96c9 --- /dev/null +++ b/src/lib/markdown-alternate.ts @@ -0,0 +1,40 @@ +import { DOCS_BASE } from "./url-base"; + +const MARKDOWN_ROUTE_PREFIX = `${DOCS_BASE}/llms.mdx/docs`; + +function normalizeDocsPath(pathname: string): string { + const normalized = pathname.replace(/\/+$/, ""); + return normalized || DOCS_BASE; +} + +export function buildMarkdownAlternatePath(pathname: string): string { + const normalized = normalizeDocsPath(pathname); + if (normalized === DOCS_BASE) return MARKDOWN_ROUTE_PREFIX; + return `${normalized}.md`; +} + +export function buildHtmlPathFromMarkdownRoute(slugs: string[]): string { + if (slugs.length === 0) return DOCS_BASE; + return `${DOCS_BASE}/${slugs.join("/")}`; +} + +export function appendHeaderValue(headers: Headers, name: string, value: string) { + const current = headers.get(name); + if (!current) { + headers.set(name, value); + return; + } + + const values = current.split(",").map((entry) => entry.trim().toLowerCase()); + if (!values.includes(value.toLowerCase())) { + headers.set(name, `${current}, ${value}`); + } +} + +export function appendVaryAccept(headers: Headers) { + appendHeaderValue(headers, "Vary", "Accept"); +} + +export function buildMarkdownAlternateLinkHeader(pathname: string): string { + return `<${buildMarkdownAlternatePath(pathname)}>; rel="alternate"; type="text/markdown"`; +} diff --git a/src/lib/source.ts b/src/lib/source.ts index a6ba994..4e11fe6 100644 --- a/src/lib/source.ts +++ b/src/lib/source.ts @@ -1,4 +1,8 @@ import { InferPageType, loader } from "fumadocs-core/source"; +import { + renderPlaceholder, + type PlaceholderData, +} from "fumadocs-core/mdx-plugins/remark-llms.runtime"; import { docs } from "fumadocs-mdx:collections/server"; import { createElement, type SVGProps } from "react"; import { @@ -134,17 +138,76 @@ export const source = loader({ }, }); +function getStringAttribute(attributes: Record, key: string): string | undefined { + const value = attributes[key]; + return typeof value === "string" && value.trim() ? value.trim() : undefined; +} + +function joinMarkdownBlocks(blocks: Array) { + return blocks + .map((block) => block?.trim()) + .filter(Boolean) + .join("\n\n"); +} + +function renderMarkdownBlock(blocks: Array) { + const block = joinMarkdownBlocks(blocks); + return block ? `${block}\n\n` : ""; +} + +function renderCallout(label: string, data: PlaceholderData) { + const title = getStringAttribute(data.attributes, "title") ?? label; + return renderMarkdownBlock([`> **${title}**`, data.children]); +} + +const markdownPlaceholderRenderers: Record string> = { + Accordion: (data) => + renderMarkdownBlock([ + `## ${getStringAttribute(data.attributes, "title") ?? "Details"}`, + data.children, + ]), + AccordionGroup: (data) => data.children, + Callout: (data) => renderCallout("Callout", data), + Card: (data) => + renderMarkdownBlock([ + `## ${getStringAttribute(data.attributes, "title") ?? "Link"}`, + getStringAttribute(data.attributes, "href"), + data.children, + ]), + CardGroup: (data) => data.children, + CodeGroup: (data) => data.children, + Frame: (data) => data.children, + Info: (data) => renderCallout("Info", data), + Mermaid: (data) => `\`\`\`mermaid\n${data.children.trim()}\n\`\`\`\n\n`, + Note: (data) => renderCallout("Note", data), + Step: (data) => + renderMarkdownBlock([ + `## ${getStringAttribute(data.attributes, "title") ?? "Step"}`, + data.children, + ]), + Steps: (data) => data.children, + Tab: (data) => + renderMarkdownBlock([ + `## ${getStringAttribute(data.attributes, "title") ?? "Tab"}`, + data.children, + ]), + Tabs: (data) => data.children, + Tip: (data) => renderCallout("Tip", data), + Warning: (data) => renderCallout("Warning", data), +}; + export async function getPageMarkdownText( page: InferPageType, _type: "raw" | "processed" = "processed", ) { - return page.data.getText("processed"); + const processed = await renderPlaceholder( + await page.data.getText("processed"), + markdownPlaceholderRenderers, + ); + + return joinMarkdownBlocks([`# ${page.data.title}`, page.data.description, processed]); } export async function getLLMText(page: InferPageType) { - const processed = await getPageMarkdownText(page); - - return `# ${page.data.title} - -${processed}`; + return getPageMarkdownText(page); } diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index f9050ae..a611e4e 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -19,8 +19,10 @@ import { Route as SplatRouteImport } from './routes/$' import { Route as IndexRouteImport } from './routes/index' import { Route as SdkIndexRouteImport } from './routes/sdk/index' import { Route as SdkSplatRouteImport } from './routes/sdk/$' +import { Route as LlmsDotmdxDocsRouteImport } from './routes/llms[.]mdx.docs' import { Route as ApiSearchRouteImport } from './routes/api/search' import { Route as ApiFeedbackRouteImport } from './routes/api/feedback' +import { Route as LlmsDotmdxDocsIndexRouteImport } from './routes/llms[.]mdx.docs.index' import { Route as LlmsDotmdxDocsSplatRouteImport } from './routes/llms[.]mdx.docs.$' import { Route as ApiRawSplatRouteImport } from './routes/api/raw/$' @@ -76,6 +78,11 @@ const SdkSplatRoute = SdkSplatRouteImport.update({ path: '/sdk/$', getParentRoute: () => rootRouteImport, } as any) +const LlmsDotmdxDocsRoute = LlmsDotmdxDocsRouteImport.update({ + id: '/llms.mdx/docs', + path: '/llms.mdx/docs', + getParentRoute: () => rootRouteImport, +} as any) const ApiSearchRoute = ApiSearchRouteImport.update({ id: '/api/search', path: '/api/search', @@ -86,10 +93,15 @@ const ApiFeedbackRoute = ApiFeedbackRouteImport.update({ path: '/api/feedback', getParentRoute: () => rootRouteImport, } as any) +const LlmsDotmdxDocsIndexRoute = LlmsDotmdxDocsIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => LlmsDotmdxDocsRoute, +} as any) const LlmsDotmdxDocsSplatRoute = LlmsDotmdxDocsSplatRouteImport.update({ - id: '/llms.mdx/docs/$', - path: '/llms.mdx/docs/$', - getParentRoute: () => rootRouteImport, + id: '/$', + path: '/$', + getParentRoute: () => LlmsDotmdxDocsRoute, } as any) const ApiRawSplatRoute = ApiRawSplatRouteImport.update({ id: '/api/raw/$', @@ -108,10 +120,12 @@ export interface FileRoutesByFullPath { '/sitemap.xml': typeof SitemapDotxmlRoute '/api/feedback': typeof ApiFeedbackRoute '/api/search': typeof ApiSearchRoute + '/llms.mdx/docs': typeof LlmsDotmdxDocsRouteWithChildren '/sdk/$': typeof SdkSplatRoute '/sdk/': typeof SdkIndexRoute '/api/raw/$': typeof ApiRawSplatRoute '/llms.mdx/docs/$': typeof LlmsDotmdxDocsSplatRoute + '/llms.mdx/docs/': typeof LlmsDotmdxDocsIndexRoute } export interface FileRoutesByTo { '/': typeof IndexRoute @@ -128,6 +142,7 @@ export interface FileRoutesByTo { '/sdk': typeof SdkIndexRoute '/api/raw/$': typeof ApiRawSplatRoute '/llms.mdx/docs/$': typeof LlmsDotmdxDocsSplatRoute + '/llms.mdx/docs': typeof LlmsDotmdxDocsIndexRoute } export interface FileRoutesById { __root__: typeof rootRouteImport @@ -141,10 +156,12 @@ export interface FileRoutesById { '/sitemap.xml': typeof SitemapDotxmlRoute '/api/feedback': typeof ApiFeedbackRoute '/api/search': typeof ApiSearchRoute + '/llms.mdx/docs': typeof LlmsDotmdxDocsRouteWithChildren '/sdk/$': typeof SdkSplatRoute '/sdk/': typeof SdkIndexRoute '/api/raw/$': typeof ApiRawSplatRoute '/llms.mdx/docs/$': typeof LlmsDotmdxDocsSplatRoute + '/llms.mdx/docs/': typeof LlmsDotmdxDocsIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath @@ -159,10 +176,12 @@ export interface FileRouteTypes { | '/sitemap.xml' | '/api/feedback' | '/api/search' + | '/llms.mdx/docs' | '/sdk/$' | '/sdk/' | '/api/raw/$' | '/llms.mdx/docs/$' + | '/llms.mdx/docs/' fileRoutesByTo: FileRoutesByTo to: | '/' @@ -179,6 +198,7 @@ export interface FileRouteTypes { | '/sdk' | '/api/raw/$' | '/llms.mdx/docs/$' + | '/llms.mdx/docs' id: | '__root__' | '/' @@ -191,10 +211,12 @@ export interface FileRouteTypes { | '/sitemap.xml' | '/api/feedback' | '/api/search' + | '/llms.mdx/docs' | '/sdk/$' | '/sdk/' | '/api/raw/$' | '/llms.mdx/docs/$' + | '/llms.mdx/docs/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -208,10 +230,10 @@ export interface RootRouteChildren { SitemapDotxmlRoute: typeof SitemapDotxmlRoute ApiFeedbackRoute: typeof ApiFeedbackRoute ApiSearchRoute: typeof ApiSearchRoute + LlmsDotmdxDocsRoute: typeof LlmsDotmdxDocsRouteWithChildren SdkSplatRoute: typeof SdkSplatRoute SdkIndexRoute: typeof SdkIndexRoute ApiRawSplatRoute: typeof ApiRawSplatRoute - LlmsDotmdxDocsSplatRoute: typeof LlmsDotmdxDocsSplatRoute } declare module '@tanstack/react-router' { @@ -286,6 +308,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SdkSplatRouteImport parentRoute: typeof rootRouteImport } + '/llms.mdx/docs': { + id: '/llms.mdx/docs' + path: '/llms.mdx/docs' + fullPath: '/llms.mdx/docs' + preLoaderRoute: typeof LlmsDotmdxDocsRouteImport + parentRoute: typeof rootRouteImport + } '/api/search': { id: '/api/search' path: '/api/search' @@ -300,12 +329,19 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiFeedbackRouteImport parentRoute: typeof rootRouteImport } + '/llms.mdx/docs/': { + id: '/llms.mdx/docs/' + path: '/' + fullPath: '/llms.mdx/docs/' + preLoaderRoute: typeof LlmsDotmdxDocsIndexRouteImport + parentRoute: typeof LlmsDotmdxDocsRoute + } '/llms.mdx/docs/$': { id: '/llms.mdx/docs/$' - path: '/llms.mdx/docs/$' + path: '/$' fullPath: '/llms.mdx/docs/$' preLoaderRoute: typeof LlmsDotmdxDocsSplatRouteImport - parentRoute: typeof rootRouteImport + parentRoute: typeof LlmsDotmdxDocsRoute } '/api/raw/$': { id: '/api/raw/$' @@ -317,6 +353,20 @@ declare module '@tanstack/react-router' { } } +interface LlmsDotmdxDocsRouteChildren { + LlmsDotmdxDocsSplatRoute: typeof LlmsDotmdxDocsSplatRoute + LlmsDotmdxDocsIndexRoute: typeof LlmsDotmdxDocsIndexRoute +} + +const LlmsDotmdxDocsRouteChildren: LlmsDotmdxDocsRouteChildren = { + LlmsDotmdxDocsSplatRoute: LlmsDotmdxDocsSplatRoute, + LlmsDotmdxDocsIndexRoute: LlmsDotmdxDocsIndexRoute, +} + +const LlmsDotmdxDocsRouteWithChildren = LlmsDotmdxDocsRoute._addFileChildren( + LlmsDotmdxDocsRouteChildren, +) + const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, SplatRoute: SplatRoute, @@ -329,10 +379,10 @@ const rootRouteChildren: RootRouteChildren = { SitemapDotxmlRoute: SitemapDotxmlRoute, ApiFeedbackRoute: ApiFeedbackRoute, ApiSearchRoute: ApiSearchRoute, + LlmsDotmdxDocsRoute: LlmsDotmdxDocsRouteWithChildren, SdkSplatRoute: SdkSplatRoute, SdkIndexRoute: SdkIndexRoute, ApiRawSplatRoute: ApiRawSplatRoute, - LlmsDotmdxDocsSplatRoute: LlmsDotmdxDocsSplatRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/src/routes/$.tsx b/src/routes/$.tsx index 2e5185e..e9d5c0a 100644 --- a/src/routes/$.tsx +++ b/src/routes/$.tsx @@ -25,6 +25,7 @@ import { import { getSdkContextFromRouterPath, SDK_CONTEXT_SLUGS } from "@/lib/sdk-navigation"; import { buildDocsApiPath } from "@/lib/url-base"; import { Code } from "lucide-react"; +import { buildMarkdownAlternatePath } from "@/lib/markdown-alternate"; import { staticCacheMiddleware } from "@/lib/static-cache-middleware"; @@ -70,7 +71,18 @@ export const Route = createFileRoute("/$")({ { name: "twitter:description", content: pageDescription }, { name: "twitter:image", content: DEFAULT_SOCIAL_IMAGE_URL }, ], - links: [{ rel: "canonical", href: canonicalUrl }], + links: [ + { rel: "canonical", href: canonicalUrl }, + ...(loaderData + ? [ + { + rel: "alternate", + type: "text/markdown", + href: buildMarkdownAlternatePath(loaderData.url), + }, + ] + : []), + ], }; }, }); @@ -140,8 +152,8 @@ const clientLoader = browserCollections.docs.createClientLoader({ {frontmatter.title} {frontmatter.description}
- - + +
diff --git a/src/routes/index.tsx b/src/routes/index.tsx index a13a416..071b35c 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -23,6 +23,7 @@ import { import { normalizeDocsInternalHref } from "@/lib/docs-url"; import { resolveSdkAwareDocsHref } from "@/lib/sdk-navigation"; import { buildDocsPath, toRouterPath } from "@/lib/url-base"; +import { buildMarkdownAlternatePath } from "@/lib/markdown-alternate"; export const Route = createFileRoute("/")({ component: DocsOverview, @@ -48,7 +49,14 @@ export const Route = createFileRoute("/")({ { name: "twitter:description", content: description }, { name: "twitter:image", content: DEFAULT_SOCIAL_IMAGE_URL }, ], - links: [{ rel: "canonical", href: canonicalUrl }], + links: [ + { rel: "canonical", href: canonicalUrl }, + { + rel: "alternate", + type: "text/markdown", + href: buildMarkdownAlternatePath("/docs"), + }, + ], }; }, }); @@ -108,7 +116,8 @@ const docsCards: DocCard[] = [ }, { title: "Superwall Agents", - description: "Analyze experiments, automate reports, connect tools, and work with hosted machines.", + description: + "Analyze experiments, automate reports, connect tools, and work with hosted machines.", href: buildDocsPath("agents"), icon: , }, diff --git a/src/routes/llms[.]mdx.docs.$.ts b/src/routes/llms[.]mdx.docs.$.ts index 737e36f..357a583 100644 --- a/src/routes/llms[.]mdx.docs.$.ts +++ b/src/routes/llms[.]mdx.docs.$.ts @@ -1,21 +1,32 @@ import { createFileRoute, notFound } from "@tanstack/react-router"; import { getPageMarkdownText, source } from "@/lib/source"; +import { buildLLMSummaryText } from "@/lib/llms"; +import { buildHtmlPathFromMarkdownRoute } from "@/lib/markdown-alternate"; export const Route = createFileRoute("/llms.mdx/docs/$")({ server: { handlers: { GET: async ({ params }) => { const slugs = params._splat?.split("/") ?? []; - const page = source.getPage(slugs); - if (!page) throw notFound(); + const body = + slugs.length === 0 ? await buildLLMSummaryText() : await getMarkdownForPage(slugs); - return new Response(await getPageMarkdownText(page), { + return new Response(body, { headers: { - "Content-Type": "text/markdown", + "Content-Type": "text/markdown; charset=utf-8", "Access-Control-Allow-Origin": "*", + Vary: "Accept", + Link: `<${buildHtmlPathFromMarkdownRoute(slugs)}>; rel="canonical"; type="text/html"`, }, }); }, }, }, }); + +async function getMarkdownForPage(slugs: string[]) { + const page = source.getPage(slugs); + if (!page) throw notFound(); + + return getPageMarkdownText(page); +} diff --git a/src/routes/llms[.]mdx.docs.index.ts b/src/routes/llms[.]mdx.docs.index.ts new file mode 100644 index 0000000..bf072a0 --- /dev/null +++ b/src/routes/llms[.]mdx.docs.index.ts @@ -0,0 +1,20 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { buildLLMSummaryText } from "@/lib/llms"; +import { DOCS_BASE } from "@/lib/url-base"; + +export const Route = createFileRoute("/llms.mdx/docs/")({ + server: { + handlers: { + GET: async () => { + return new Response(await buildLLMSummaryText(), { + headers: { + "Content-Type": "text/markdown; charset=utf-8", + "Access-Control-Allow-Origin": "*", + Vary: "Accept", + Link: `<${DOCS_BASE}>; rel="canonical"; type="text/html"`, + }, + }); + }, + }, + }, +}); diff --git a/src/routes/llms[.]mdx.docs.ts b/src/routes/llms[.]mdx.docs.ts new file mode 100644 index 0000000..5472217 --- /dev/null +++ b/src/routes/llms[.]mdx.docs.ts @@ -0,0 +1,3 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/llms.mdx/docs")({}); diff --git a/src/start.test.ts b/src/start.test.ts index 040c195..a2a858e 100644 --- a/src/start.test.ts +++ b/src/start.test.ts @@ -1,6 +1,12 @@ import assert from "node:assert/strict"; import { describe, test } from "node:test"; -import { resolveLegacyRedirect } from "./start"; +import { negotiateDocsRepresentation, resolveLegacyRedirect, resolveLLMPath } from "./start"; + +function docsRequest(accept?: string, path = "/docs/dashboard") { + return new Request(`https://example.com${path}`, { + headers: accept === undefined ? undefined : { Accept: accept }, + }); +} describe("resolveLegacyRedirect", () => { test("does not redirect sdk placeholder routes", () => { @@ -20,3 +26,58 @@ describe("resolveLegacyRedirect", () => { ); }); }); + +describe("negotiateDocsRepresentation", () => { + test("selects markdown when text/markdown is preferred", () => { + assert.equal(negotiateDocsRepresentation(docsRequest("text/markdown")), "text/markdown"); + }); + + test("preserves legacy markdown media types", () => { + assert.equal(negotiateDocsRepresentation(docsRequest("text/plain")), "text/markdown"); + assert.equal(negotiateDocsRepresentation(docsRequest("text/x-markdown")), "text/markdown"); + }); + + test("selects html when html has a higher q-value", () => { + assert.equal( + negotiateDocsRepresentation(docsRequest("text/html;q=1,text/markdown;q=0.5")), + "text/html", + ); + }); + + test("respects q=0 for markdown", () => { + assert.equal( + negotiateDocsRepresentation(docsRequest("text/markdown;q=0,text/html;q=1")), + "text/html", + ); + }); + + test("defaults missing Accept and wildcards to html", () => { + assert.equal(negotiateDocsRepresentation(docsRequest()), "text/html"); + assert.equal(negotiateDocsRepresentation(docsRequest("*/*")), "text/html"); + }); + + test("returns undefined for unsupported explicit types", () => { + assert.equal( + negotiateDocsRepresentation(docsRequest("application/x-content-negotiation-probe")), + undefined, + ); + }); +}); + +describe("resolveLLMPath", () => { + test("rewrites canonical docs paths only when markdown wins negotiation", () => { + assert.equal(resolveLLMPath(docsRequest("text/markdown")), "/docs/llms.mdx/docs/dashboard"); + assert.equal(resolveLLMPath(docsRequest("text/html;q=1,text/markdown;q=0.5")), undefined); + }); + + test("rewrites direct markdown suffixes regardless of Accept", () => { + assert.equal( + resolveLLMPath(docsRequest("text/html", "/docs/dashboard.md")), + "/docs/llms.mdx/docs/dashboard", + ); + assert.equal( + resolveLLMPath(docsRequest("text/html", "/docs/dashboard.mdx")), + "/docs/llms.mdx/docs/dashboard", + ); + }); +}); diff --git a/src/start.ts b/src/start.ts index 9171d03..9792180 100644 --- a/src/start.ts +++ b/src/start.ts @@ -1,8 +1,13 @@ import { createMiddleware, createStart } from "@tanstack/react-start"; -import { isMarkdownPreferred, rewritePath } from "fumadocs-core/negotiation"; +import { getNegotiator, rewritePath } from "fumadocs-core/negotiation"; import { redirect } from "@tanstack/react-router"; import { buildRedirectRouteRules } from "./lib/redirect-route-rules"; import { DOCS_BASE } from "./lib/url-base"; +import { + appendHeaderValue, + appendVaryAccept, + buildMarkdownAlternateLinkHeader, +} from "./lib/markdown-alternate"; const { rewrite: rewriteLLMMarkdown } = rewritePath( `${DOCS_BASE}{/*path}.md`, @@ -23,6 +28,9 @@ const redirectBypassPrefixes = [ `${DOCS_BASE}/assets`, ]; const staticAssetPathPattern = /\.[a-z0-9]{2,8}$/i; +const markdownMediaTypes = ["text/markdown", "text/plain", "text/x-markdown"] as const; +const negotiatedMediaTypes = ["text/html", ...markdownMediaTypes] as const; +type DocsRepresentation = "text/html" | "text/markdown"; function normalizePathname(pathname: string): string { const normalized = pathname.replace(/\/+$/, ""); @@ -70,6 +78,39 @@ function shouldBypassLLMRewrite(pathname: string): boolean { return /\.(?!mdx?$)[a-z0-9]{2,8}$/i.test(pathname); } +export function negotiateDocsRepresentation(request: Request): DocsRepresentation | undefined { + const mediaType = getNegotiator(request).mediaType([...negotiatedMediaTypes]); + if (!mediaType) return undefined; + if (mediaType === "text/html") return "text/html"; + return "text/markdown"; +} + +function buildNotAcceptableResponse(): Response { + return new Response("Not Acceptable", { + status: 406, + headers: { + "Content-Type": "text/plain; charset=utf-8", + Vary: "Accept", + }, + }); +} + +function withDocsNegotiationHeaders(response: Response, pathname: string): Response { + const headers = new Headers(response.headers); + appendVaryAccept(headers); + + const contentType = headers.get("Content-Type"); + if (contentType?.toLowerCase().startsWith("text/html")) { + appendHeaderValue(headers, "Link", buildMarkdownAlternateLinkHeader(pathname)); + } + + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers, + }); +} + export function resolveLLMPath(request: Request): string | undefined { const url = new URL(request.url); if (shouldBypassLLMRewrite(url.pathname)) return undefined; @@ -77,7 +118,7 @@ export function resolveLLMPath(request: Request): string | undefined { const directRewrite = rewriteLLMMarkdown(url.pathname) || rewriteLLMMdx(url.pathname); if (directRewrite) return directRewrite; - if (!isMarkdownPreferred(request)) return undefined; + if (negotiateDocsRepresentation(request) !== "text/markdown") return undefined; return rewriteLLMPreferred(url.pathname) || undefined; } @@ -112,12 +153,14 @@ const legacyRedirectMiddleware = createMiddleware().server(({ next, request }) = return next(); }); -const llmMiddleware = createMiddleware().server(({ next, request }) => { +const llmMiddleware = createMiddleware().server(async ({ next, request }) => { const url = new URL(request.url); let destination: string | undefined; + let representation: DocsRepresentation | undefined; try { destination = resolveLLMPath(request); + representation = negotiateDocsRepresentation(request); } catch { return next(); } @@ -126,10 +169,23 @@ const llmMiddleware = createMiddleware().server(({ next, request }) => { throw redirect({ href: withRequestSearch(url, destination).toString(), statusCode: 307, + headers: { + Vary: "Accept", + }, }); } - return next(); + if (!shouldBypassLLMRewrite(url.pathname) && !representation) { + return buildNotAcceptableResponse(); + } + + const result = await next(); + return { + ...result, + response: !shouldBypassLLMRewrite(url.pathname) + ? withDocsNegotiationHeaders(result.response, url.pathname) + : result.response, + }; }); export const startInstance = createStart(() => { From fb04fbd22d299fc2ed55fb5e5282a4ea223a6c09 Mon Sep 17 00:00:00 2001 From: Duncan Crawbuck Date: Thu, 4 Jun 2026 14:27:48 -0700 Subject: [PATCH 2/9] Fix docs markdown routes and image URLs --- content/shared/vibe-coding.mdx | 26 ++++----- plugins/remark-image-paths.ts | 39 +------------ plugins/remark-markdown-image-map.test.ts | 39 +++++++++++++ plugins/remark-markdown-image-map.ts | 59 ++++++++++++++++++++ source.config.ts | 68 ++++++++++++++++------- src/lib/docs-asset-path.ts | 40 +++++++++++++ src/lib/markdown-route.ts | 30 ++++++++++ src/lib/source.ts | 65 +++++++++++++++++++++- src/routeTree.gen.ts | 42 ++++++++++++++ src/routes/llms[.]mdx.docs.$.ts | 32 ++++------- src/routes/llms[.]mdx.docs.index.ts | 14 +---- src/routes/{$}[.]md.ts | 27 +++++++++ src/routes/{$}[.]mdx.ts | 27 +++++++++ src/start.test.ts | 14 ++--- src/start.ts | 22 ++------ 15 files changed, 412 insertions(+), 132 deletions(-) create mode 100644 plugins/remark-markdown-image-map.test.ts create mode 100644 plugins/remark-markdown-image-map.ts create mode 100644 src/lib/docs-asset-path.ts create mode 100644 src/lib/markdown-route.ts create mode 100644 src/routes/{$}[.]md.ts create mode 100644 src/routes/{$}[.]mdx.ts diff --git a/content/shared/vibe-coding.mdx b/content/shared/vibe-coding.mdx index 05ec04f..de507bc 100644 --- a/content/shared/vibe-coding.mdx +++ b/content/shared/vibe-coding.mdx @@ -13,12 +13,11 @@ We've built a few tools to help you Vibe Code using the knowledge of the Superwa - [Editor AI](#editor-ai): Build and refine a paywall directly inside the visual editor using AI Chat or an external MCP-compatible agent. And right here in the Superwall Docs: + - [Superwall AI](#superwall-ai) - [Docs Links](#docs-links) - [LLMs.txt](#llmstxt) - - ## Superwall Agents [Superwall Agents](/agents) is the dedicated Superwall AI workspace. Use it when you want to analyze experiment results, use selected Superwall organization context, work with files available to the active hosted machine, create charts and reports, suggest new experiments, schedule recurring prompts, or connect webhook-driven workflows. @@ -58,7 +57,6 @@ Use Editor AI when the task is about changing the design, layout, products, vari Superwall AI is available in the bottom right 💬 and is a great place to start if you have a question or issue. - ## Docs Links At the top of each page of the Superwall Docs (including this one!): @@ -70,24 +68,26 @@ Also in the **Open** dropdown menu, you can access these options: - **View as Markdown**: to view the page in Markdown format - **Open in ChatGPT**, **Open in Claude**: to open the page in the respective AI tool and add the page as context for your conversation +You can also add `.md` to the end of any docs page URL to open that page as Markdown. For example, `https://superwall.com/docs/ios/quickstart/install.md`. ## LLMs.txt + The Superwall Docs website has `llms.txt` and `llms-full.txt` files, in total and for each SDK, that you can use to add context to your LLMs. `llms.txt` is a summary of the docs with links to each page. `llms-full.txt` is the full text of all of the docs. -| SDK | Summary | Full Text | -| -------- | ------- | --------- | -| All | [`llms.txt`](https://superwall.com/docs/llms.txt) | [`llms-full.txt`](https://superwall.com/docs/llms-full.txt) | -| Dashboard | [`llms-dashboard.txt`](https://superwall.com/docs/llms-dashboard.txt) | [`llms-full-dashboard.txt`](https://superwall.com/docs/llms-full-dashboard.txt) | -| iOS | [`llms-ios.txt`](https://superwall.com/docs/llms-ios.txt) | [`llms-full-ios.txt`](https://superwall.com/docs/llms-full-ios.txt) | -| Android | [`llms-android.txt`](https://superwall.com/docs/llms-android.txt) | [`llms-full-android.txt`](https://superwall.com/docs/llms-full-android.txt) | -| Flutter | [`llms-flutter.txt`](https://superwall.com/docs/llms-flutter.txt) | [`llms-full-flutter.txt`](https://superwall.com/docs/llms-full-flutter.txt) | -| Expo | [`llms-expo.txt`](https://superwall.com/docs/llms-expo.txt) | [`llms-full-expo.txt`](https://superwall.com/docs/llms-full-expo.txt) | +| SDK | Summary | Full Text | +| ------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| All | [`llms.txt`](https://superwall.com/docs/llms.txt) | [`llms-full.txt`](https://superwall.com/docs/llms-full.txt) | +| Dashboard | [`llms-dashboard.txt`](https://superwall.com/docs/llms-dashboard.txt) | [`llms-full-dashboard.txt`](https://superwall.com/docs/llms-full-dashboard.txt) | +| iOS | [`llms-ios.txt`](https://superwall.com/docs/llms-ios.txt) | [`llms-full-ios.txt`](https://superwall.com/docs/llms-full-ios.txt) | +| Android | [`llms-android.txt`](https://superwall.com/docs/llms-android.txt) | [`llms-full-android.txt`](https://superwall.com/docs/llms-full-android.txt) | +| Flutter | [`llms-flutter.txt`](https://superwall.com/docs/llms-flutter.txt) | [`llms-full-flutter.txt`](https://superwall.com/docs/llms-full-flutter.txt) | +| Expo | [`llms-expo.txt`](https://superwall.com/docs/llms-expo.txt) | [`llms-full-expo.txt`](https://superwall.com/docs/llms-full-expo.txt) | | React Native (Deprecated) | [`llms-react-native.txt`](https://superwall.com/docs/llms-react-native.txt) | [`llms-full-react-native.txt`](https://superwall.com/docs/llms-full-react-native.txt) | -| Integrations | [`llms-integrations.txt`](https://superwall.com/docs/llms-integrations.txt) | [`llms-full-integrations.txt`](https://superwall.com/docs/llms-full-integrations.txt) | -| Web Checkout | [`llms-web-checkout.txt`](https://superwall.com/docs/llms-web-checkout.txt) | [`llms-full-web-checkout.txt`](https://superwall.com/docs/llms-full-web-checkout.txt) | +| Integrations | [`llms-integrations.txt`](https://superwall.com/docs/llms-integrations.txt) | [`llms-full-integrations.txt`](https://superwall.com/docs/llms-full-integrations.txt) | +| Web Checkout | [`llms-web-checkout.txt`](https://superwall.com/docs/llms-web-checkout.txt) | [`llms-full-web-checkout.txt`](https://superwall.com/docs/llms-full-web-checkout.txt) | To minimize token use, we recommend using the files specific to your SDK. diff --git a/plugins/remark-image-paths.ts b/plugins/remark-image-paths.ts index c95743e..0d5d963 100644 --- a/plugins/remark-image-paths.ts +++ b/plugins/remark-image-paths.ts @@ -1,4 +1,7 @@ import { visit } from "unist-util-visit"; +import { normalizeDocsAssetPath } from "../src/lib/docs-asset-path"; + +export { normalizeDocsAssetPath }; /* Fixes the image paths @@ -10,42 +13,6 @@ output: ![](/docs/images/3pa_cp_2.jpeg) */ -const DOCS_PREFIX = "/docs"; -const SPECIAL_URL_PATTERN = /^[a-zA-Z][a-zA-Z\d+\-.]*:/; - -export function normalizeDocsAssetPath(url: string): string { - const trimmed = url.trim(); - if (!trimmed) return trimmed; - - // Keep external/special schemes and fragment-only links untouched. - if (trimmed.startsWith("#") || trimmed.startsWith("//") || SPECIAL_URL_PATTERN.test(trimmed)) { - return trimmed; - } - - const [, pathPart = "", suffix = ""] = trimmed.match(/^([^?#]*)([?#].*)?$/) ?? []; - const segments = pathPart - .replace(/^\/+/, "") - .split("/") - .filter(Boolean); - - const normalizedSegments: string[] = []; - for (const segment of segments) { - if (segment === "." || segment === "") continue; - if (segment === "..") { - normalizedSegments.pop(); - continue; - } - normalizedSegments.push(segment); - } - - const normalizedPath = `/${normalizedSegments.join("/")}`; - if (normalizedPath === DOCS_PREFIX || normalizedPath.startsWith(`${DOCS_PREFIX}/`)) { - return `${normalizedPath}${suffix}`; - } - - return `${DOCS_PREFIX}${normalizedPath === "/" ? "" : normalizedPath}${suffix}`; -} - export default function remarkImagePaths() { return (tree: any) => { visit(tree, "image", (node) => { diff --git a/plugins/remark-markdown-image-map.test.ts b/plugins/remark-markdown-image-map.test.ts new file mode 100644 index 0000000..67a0825 --- /dev/null +++ b/plugins/remark-markdown-image-map.test.ts @@ -0,0 +1,39 @@ +import assert from "node:assert/strict"; +import { describe, test } from "node:test"; +import { unified } from "unified"; +import remarkMdx from "remark-mdx"; +import remarkParse from "remark-parse"; +import remarkMarkdownImageMap from "./remark-markdown-image-map"; + +async function collectMarkdownImageMap(markdown: string) { + const processor = unified().use(remarkParse).use(remarkMdx).use(remarkMarkdownImageMap); + const file = { data: {} }; + const tree = processor.parse(markdown); + + await processor.run(tree, file as any); + return file.data as { + markdownImageMap?: Record; + "mdx-export"?: Array<{ name: string; value: unknown }>; + }; +} + +describe("remarkMarkdownImageMap", () => { + test("exports normalized image URLs keyed by Fumadocs placeholders", async () => { + const data = await collectMarkdownImageMap(` +![](/images/first.png) + +![Alt text](../images/second.png?size=large#preview) +`); + + assert.deepEqual(data.markdownImageMap, { + __img0: "/docs/images/first.png", + __img1: "/docs/images/second.png?size=large#preview", + }); + assert.deepEqual(data["mdx-export"], [ + { + name: "markdownImageMap", + value: data.markdownImageMap, + }, + ]); + }); +}); diff --git a/plugins/remark-markdown-image-map.ts b/plugins/remark-markdown-image-map.ts new file mode 100644 index 0000000..5f4fec7 --- /dev/null +++ b/plugins/remark-markdown-image-map.ts @@ -0,0 +1,59 @@ +import { visit } from "unist-util-visit"; +import { normalizeDocsAssetPath } from "../src/lib/docs-asset-path"; + +const imagePlaceholderPattern = /^__img\d+$/; + +function getAttributeValue(node: any, name: string): string | undefined { + for (const attribute of node.attributes ?? []) { + if (attribute.name !== name) continue; + + const value = attribute.value; + if (typeof value === "string") return value; + if (typeof value?.value === "string") return value.value; + } +} + +function getImagePlaceholder(node: any): string | undefined { + const src = getAttributeValue(node, "src"); + return src && imagePlaceholderPattern.test(src) ? src : undefined; +} + +function getImageUrl(node: any): string | undefined { + if (typeof node.url === "string") return normalizeDocsAssetPath(node.url); + + const src = getAttributeValue(node, "src"); + if (!src || imagePlaceholderPattern.test(src)) return undefined; + + return normalizeDocsAssetPath(src); +} + +export default function remarkMarkdownImageMap() { + return (tree: any, file: any) => { + const imageMap: Record = {}; + let imageIndex = 0; + + visit(tree, "image", (node: any) => { + if (typeof node.url !== "string") return; + + imageMap[`__img${imageIndex}`] = normalizeDocsAssetPath(node.url); + imageIndex += 1; + }); + + visit(tree, ["mdxJsxFlowElement", "mdxJsxTextElement"], (node: any) => { + if (node.name !== "img") return; + + const placeholder = getImagePlaceholder(node); + const url = getImageUrl(node); + if (placeholder && url) imageMap[placeholder] = url; + }); + + if (Object.keys(imageMap).length > 0) { + file.data.markdownImageMap = imageMap; + file.data["mdx-export"] ??= []; + file.data["mdx-export"].push({ + name: "markdownImageMap", + value: imageMap, + }); + } + }; +} diff --git a/source.config.ts b/source.config.ts index f221ede..12996ba 100644 --- a/source.config.ts +++ b/source.config.ts @@ -8,39 +8,66 @@ import remarkFollowExport from "./plugins/remark-follow-export"; import remarkDirective from "remark-directive"; import { remarkInclude } from "fumadocs-mdx/config"; import remarkSdkFilter from "./plugins/remark-sdk-filter"; +import remarkMarkdownImageMap from "./plugins/remark-markdown-image-map"; +import type { LLMsOptions } from "fumadocs-core/mdx-plugins"; const isDevelopment = process.env.NODE_ENV === "development"; +const markdownHeadingHandler: NonNullable["heading"] = ( + node, + _parent, + state, + info, +) => { + const depth = Math.min(Math.max(node.depth, 1), 6); + return `${"#".repeat(depth)} ${state.containerPhrasing(node, info)}`; +}; + +const processedMarkdownOptions: LLMsOptions = { + handlers: { + heading: markdownHeadingHandler, + }, + mdxAsPlaceholder: [ + "Accordion", + "AccordionGroup", + "Callout", + "Card", + "CardGroup", + "CodeGroup", + "Frame", + "Info", + "Mermaid", + "Note", + "Step", + "Steps", + "Tab", + "Tabs", + "Tip", + "Warning", + ], +}; + +const markdownImageMapPassthroughPlugin = { + "index-file": { + serverOptions(options: any) { + options.doc ??= {}; + options.doc.passthroughs ??= []; + options.doc.passthroughs.push("markdownImageMap"); + }, + }, +}; export const docs = defineDocs({ dir: "content/docs", docs: { async: isDevelopment, postprocess: { - includeProcessedMarkdown: { - mdxAsPlaceholder: [ - "Accordion", - "AccordionGroup", - "Callout", - "Card", - "CardGroup", - "CodeGroup", - "Frame", - "Info", - "Mermaid", - "Note", - "Step", - "Steps", - "Tab", - "Tabs", - "Tip", - "Warning", - ], - }, + includeProcessedMarkdown: processedMarkdownOptions, }, }, }); export default defineConfig({ + plugins: [markdownImageMapPassthroughPlugin], mdxOptions: { // Shiki highlighting is one of the most expensive parts of the MDX pipeline. // Keep it in builds, but skip it in local dev so first-page SSR doesn't have @@ -48,6 +75,7 @@ export default defineConfig({ rehypeCodeOptions: isDevelopment ? false : undefined, remarkPlugins: (existing) => [ remarkImagePaths, + remarkMarkdownImageMap, remarkLinkPaths, remarkFollowExport, ...existing, diff --git a/src/lib/docs-asset-path.ts b/src/lib/docs-asset-path.ts new file mode 100644 index 0000000..76a9fbb --- /dev/null +++ b/src/lib/docs-asset-path.ts @@ -0,0 +1,40 @@ +const DOCS_PREFIX = "/docs"; +const SPECIAL_URL_PATTERN = /^[a-zA-Z][a-zA-Z\d+\-.]*:/; + +export function normalizeDocsAssetPath(url: string): string { + const trimmed = url.trim(); + if (!trimmed) return trimmed; + + if (trimmed.startsWith("#") || trimmed.startsWith("//") || SPECIAL_URL_PATTERN.test(trimmed)) { + return trimmed; + } + + const [, pathPart = "", suffix = ""] = trimmed.match(/^([^?#]*)([?#].*)?$/) ?? []; + const segments = pathPart.replace(/^\/+/, "").split("/").filter(Boolean); + + const normalizedSegments: string[] = []; + for (const segment of segments) { + if (segment === "." || segment === "") continue; + if (segment === "..") { + normalizedSegments.pop(); + continue; + } + normalizedSegments.push(segment); + } + + const normalizedPath = `/${normalizedSegments.join("/")}`; + if (normalizedPath === DOCS_PREFIX || normalizedPath.startsWith(`${DOCS_PREFIX}/`)) { + return `${normalizedPath}${suffix}`; + } + + return `${DOCS_PREFIX}${normalizedPath === "/" ? "" : normalizedPath}${suffix}`; +} + +export function resolveDocsAssetUrl(url: string, baseUrl?: string) { + const normalized = normalizeDocsAssetPath(url); + if (!baseUrl || normalized.startsWith("#") || SPECIAL_URL_PATTERN.test(normalized)) { + return normalized; + } + + return new URL(normalized, baseUrl).toString(); +} diff --git a/src/lib/markdown-route.ts b/src/lib/markdown-route.ts new file mode 100644 index 0000000..0056837 --- /dev/null +++ b/src/lib/markdown-route.ts @@ -0,0 +1,30 @@ +import { buildHtmlPathFromMarkdownRoute } from "./markdown-alternate"; +import { buildLLMSummaryText } from "./llms"; +import { getPageMarkdownText, source } from "./source"; + +export function slugsFromMarkdownSplat(splat?: string) { + return (splat ?? "").replace(/\/+$/, "").split("/").filter(Boolean); +} + +export async function getMarkdownForPageSlugs(slugs: string[], request: Request) { + const page = source.getPage(slugs); + if (!page) return undefined; + + return getPageMarkdownText(page, { baseUrl: request.url }); +} + +export async function getMarkdownRouteBody(slugs: string[], request: Request) { + if (slugs.length === 0) return buildLLMSummaryText(); + return getMarkdownForPageSlugs(slugs, request); +} + +export function buildMarkdownRouteResponse(body: BodyInit | null, slugs: string[]) { + return new Response(body, { + headers: { + "Content-Type": "text/markdown; charset=utf-8", + "Access-Control-Allow-Origin": "*", + Vary: "Accept", + Link: `<${buildHtmlPathFromMarkdownRoute(slugs)}>; rel="canonical"; type="text/html"`, + }, + }); +} diff --git a/src/lib/source.ts b/src/lib/source.ts index 4e11fe6..5e1cf08 100644 --- a/src/lib/source.ts +++ b/src/lib/source.ts @@ -4,6 +4,7 @@ import { type PlaceholderData, } from "fumadocs-core/mdx-plugins/remark-llms.runtime"; import { docs } from "fumadocs-mdx:collections/server"; +import { resolveDocsAssetUrl } from "./docs-asset-path"; import { createElement, type SVGProps } from "react"; import { AlertTriangle, @@ -150,6 +151,10 @@ function joinMarkdownBlocks(blocks: Array) { .join("\n\n"); } +function normalizeMarkdownWhitespace(markdown: string) { + return markdown.replace(/\n{3,}/g, "\n\n").trim(); +} + function renderMarkdownBlock(blocks: Array) { const block = joinMarkdownBlocks(blocks); return block ? `${block}\n\n` : ""; @@ -157,7 +162,10 @@ function renderMarkdownBlock(blocks: Array) { function renderCallout(label: string, data: PlaceholderData) { const title = getStringAttribute(data.attributes, "title") ?? label; - return renderMarkdownBlock([`> **${title}**`, data.children]); + const children = data.children.trim(); + if (!children) return renderMarkdownBlock([`> **${title}**`]); + + return renderMarkdownBlock([`> **${title}:** ${children.replace(/\n/g, "\n> ")}`]); } const markdownPlaceholderRenderers: Record string> = { @@ -196,16 +204,67 @@ const markdownPlaceholderRenderers: Record st Warning: (data) => renderCallout("Warning", data), }; +type MarkdownRenderOptions = { + baseUrl?: string; +}; + +const htmlImagePlaceholderPattern = /]*?)\bsrc=["']__img(\d+)["']([^>]*)\/?>/g; +const markdownImagePlaceholderPattern = /(!\[[^\]]*]\(\s*)?(\s*\))/g; +const htmlAltAttributePattern = /\balt=["']([^"']*)["']/i; + +function restoreImagePlaceholders(markdown: string, imageImports: Map) { + const withHtmlImages = markdown.replace( + htmlImagePlaceholderPattern, + (match, before: string, index: string, after: string) => { + const url = imageImports.get(`__img${index}`); + if (!url) return match; + + const alt = `${before} ${after}`.match(htmlAltAttributePattern)?.[1] ?? ""; + return `\n![${alt}](${url})\n`; + }, + ); + + return withHtmlImages.replace( + markdownImagePlaceholderPattern, + (match, prefix: string, index: string, suffix: string) => { + const url = imageImports.get(`__img${index}`); + return url ? `${prefix}${url}${suffix}` : match; + }, + ); +} + +type PageMarkdownData = { + load?: () => Promise; + markdownImageMap?: Record; + _exports?: { markdownImageMap?: Record }; +}; + +async function getPageImageImports(page: InferPageType, baseUrl?: string) { + const pageData = page.data as PageMarkdownData; + const data = pageData.load ? await pageData.load() : pageData; + const imageMap = data.markdownImageMap ?? data._exports?.markdownImageMap ?? {}; + + return new Map( + Object.entries(imageMap) + .filter((entry): entry is [string, string] => typeof entry[1] === "string") + .map(([name, url]) => [name, resolveDocsAssetUrl(url, baseUrl)]), + ); +} + export async function getPageMarkdownText( page: InferPageType, - _type: "raw" | "processed" = "processed", + options: MarkdownRenderOptions = {}, ) { + const imageImports = await getPageImageImports(page, options.baseUrl); const processed = await renderPlaceholder( await page.data.getText("processed"), markdownPlaceholderRenderers, ); + const withImages = restoreImagePlaceholders(processed, imageImports); - return joinMarkdownBlocks([`# ${page.data.title}`, page.data.description, processed]); + return normalizeMarkdownWhitespace( + joinMarkdownBlocks([`# ${page.data.title}`, page.data.description, withImages]), + ); } export async function getLLMText(page: InferPageType) { diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index a611e4e..63b4924 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -9,6 +9,8 @@ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' +import { Route as Char123Char125DotmdxRouteImport } from './routes/{$}[.]mdx' +import { Route as Char123Char125DotmdRouteImport } from './routes/{$}[.]md' import { Route as SitemapDotxmlRouteImport } from './routes/sitemap[.]xml' import { Route as RobotsDottxtRouteImport } from './routes/robots[.]txt' import { Route as LlmsDottxtRouteImport } from './routes/llms[.]txt' @@ -26,6 +28,16 @@ import { Route as LlmsDotmdxDocsIndexRouteImport } from './routes/llms[.]mdx.doc import { Route as LlmsDotmdxDocsSplatRouteImport } from './routes/llms[.]mdx.docs.$' import { Route as ApiRawSplatRouteImport } from './routes/api/raw/$' +const Char123Char125DotmdxRoute = Char123Char125DotmdxRouteImport.update({ + id: '/{$}.mdx', + path: '/{$}.mdx', + getParentRoute: () => rootRouteImport, +} as any) +const Char123Char125DotmdRoute = Char123Char125DotmdRouteImport.update({ + id: '/{$}.md', + path: '/{$}.md', + getParentRoute: () => rootRouteImport, +} as any) const SitemapDotxmlRoute = SitemapDotxmlRouteImport.update({ id: '/sitemap.xml', path: '/sitemap.xml', @@ -118,6 +130,8 @@ export interface FileRoutesByFullPath { '/llms.txt': typeof LlmsDottxtRoute '/robots.txt': typeof RobotsDottxtRoute '/sitemap.xml': typeof SitemapDotxmlRoute + '/{$}.md': typeof Char123Char125DotmdRoute + '/{$}.mdx': typeof Char123Char125DotmdxRoute '/api/feedback': typeof ApiFeedbackRoute '/api/search': typeof ApiSearchRoute '/llms.mdx/docs': typeof LlmsDotmdxDocsRouteWithChildren @@ -136,6 +150,8 @@ export interface FileRoutesByTo { '/llms.txt': typeof LlmsDottxtRoute '/robots.txt': typeof RobotsDottxtRoute '/sitemap.xml': typeof SitemapDotxmlRoute + '/{$}.md': typeof Char123Char125DotmdRoute + '/{$}.mdx': typeof Char123Char125DotmdxRoute '/api/feedback': typeof ApiFeedbackRoute '/api/search': typeof ApiSearchRoute '/sdk/$': typeof SdkSplatRoute @@ -154,6 +170,8 @@ export interface FileRoutesById { '/llms.txt': typeof LlmsDottxtRoute '/robots.txt': typeof RobotsDottxtRoute '/sitemap.xml': typeof SitemapDotxmlRoute + '/{$}.md': typeof Char123Char125DotmdRoute + '/{$}.mdx': typeof Char123Char125DotmdxRoute '/api/feedback': typeof ApiFeedbackRoute '/api/search': typeof ApiSearchRoute '/llms.mdx/docs': typeof LlmsDotmdxDocsRouteWithChildren @@ -174,6 +192,8 @@ export interface FileRouteTypes { | '/llms.txt' | '/robots.txt' | '/sitemap.xml' + | '/{$}.md' + | '/{$}.mdx' | '/api/feedback' | '/api/search' | '/llms.mdx/docs' @@ -192,6 +212,8 @@ export interface FileRouteTypes { | '/llms.txt' | '/robots.txt' | '/sitemap.xml' + | '/{$}.md' + | '/{$}.mdx' | '/api/feedback' | '/api/search' | '/sdk/$' @@ -209,6 +231,8 @@ export interface FileRouteTypes { | '/llms.txt' | '/robots.txt' | '/sitemap.xml' + | '/{$}.md' + | '/{$}.mdx' | '/api/feedback' | '/api/search' | '/llms.mdx/docs' @@ -228,6 +252,8 @@ export interface RootRouteChildren { LlmsDottxtRoute: typeof LlmsDottxtRoute RobotsDottxtRoute: typeof RobotsDottxtRoute SitemapDotxmlRoute: typeof SitemapDotxmlRoute + Char123Char125DotmdRoute: typeof Char123Char125DotmdRoute + Char123Char125DotmdxRoute: typeof Char123Char125DotmdxRoute ApiFeedbackRoute: typeof ApiFeedbackRoute ApiSearchRoute: typeof ApiSearchRoute LlmsDotmdxDocsRoute: typeof LlmsDotmdxDocsRouteWithChildren @@ -238,6 +264,20 @@ export interface RootRouteChildren { declare module '@tanstack/react-router' { interface FileRoutesByPath { + '/{$}.mdx': { + id: '/{$}.mdx' + path: '/{$}.mdx' + fullPath: '/{$}.mdx' + preLoaderRoute: typeof Char123Char125DotmdxRouteImport + parentRoute: typeof rootRouteImport + } + '/{$}.md': { + id: '/{$}.md' + path: '/{$}.md' + fullPath: '/{$}.md' + preLoaderRoute: typeof Char123Char125DotmdRouteImport + parentRoute: typeof rootRouteImport + } '/sitemap.xml': { id: '/sitemap.xml' path: '/sitemap.xml' @@ -377,6 +417,8 @@ const rootRouteChildren: RootRouteChildren = { LlmsDottxtRoute: LlmsDottxtRoute, RobotsDottxtRoute: RobotsDottxtRoute, SitemapDotxmlRoute: SitemapDotxmlRoute, + Char123Char125DotmdRoute: Char123Char125DotmdRoute, + Char123Char125DotmdxRoute: Char123Char125DotmdxRoute, ApiFeedbackRoute: ApiFeedbackRoute, ApiSearchRoute: ApiSearchRoute, LlmsDotmdxDocsRoute: LlmsDotmdxDocsRouteWithChildren, diff --git a/src/routes/llms[.]mdx.docs.$.ts b/src/routes/llms[.]mdx.docs.$.ts index 357a583..aadbf51 100644 --- a/src/routes/llms[.]mdx.docs.$.ts +++ b/src/routes/llms[.]mdx.docs.$.ts @@ -1,32 +1,20 @@ import { createFileRoute, notFound } from "@tanstack/react-router"; -import { getPageMarkdownText, source } from "@/lib/source"; -import { buildLLMSummaryText } from "@/lib/llms"; -import { buildHtmlPathFromMarkdownRoute } from "@/lib/markdown-alternate"; +import { + buildMarkdownRouteResponse, + getMarkdownRouteBody, + slugsFromMarkdownSplat, +} from "@/lib/markdown-route"; export const Route = createFileRoute("/llms.mdx/docs/$")({ server: { handlers: { - GET: async ({ params }) => { - const slugs = params._splat?.split("/") ?? []; - const body = - slugs.length === 0 ? await buildLLMSummaryText() : await getMarkdownForPage(slugs); + GET: async ({ params, request }) => { + const slugs = slugsFromMarkdownSplat(params._splat); + const body = await getMarkdownRouteBody(slugs, request); + if (!body) throw notFound(); - return new Response(body, { - headers: { - "Content-Type": "text/markdown; charset=utf-8", - "Access-Control-Allow-Origin": "*", - Vary: "Accept", - Link: `<${buildHtmlPathFromMarkdownRoute(slugs)}>; rel="canonical"; type="text/html"`, - }, - }); + return buildMarkdownRouteResponse(body, slugs); }, }, }, }); - -async function getMarkdownForPage(slugs: string[]) { - const page = source.getPage(slugs); - if (!page) throw notFound(); - - return getPageMarkdownText(page); -} diff --git a/src/routes/llms[.]mdx.docs.index.ts b/src/routes/llms[.]mdx.docs.index.ts index bf072a0..fe09780 100644 --- a/src/routes/llms[.]mdx.docs.index.ts +++ b/src/routes/llms[.]mdx.docs.index.ts @@ -1,19 +1,11 @@ import { createFileRoute } from "@tanstack/react-router"; -import { buildLLMSummaryText } from "@/lib/llms"; -import { DOCS_BASE } from "@/lib/url-base"; +import { buildMarkdownRouteResponse, getMarkdownRouteBody } from "@/lib/markdown-route"; export const Route = createFileRoute("/llms.mdx/docs/")({ server: { handlers: { - GET: async () => { - return new Response(await buildLLMSummaryText(), { - headers: { - "Content-Type": "text/markdown; charset=utf-8", - "Access-Control-Allow-Origin": "*", - Vary: "Accept", - Link: `<${DOCS_BASE}>; rel="canonical"; type="text/html"`, - }, - }); + GET: async ({ request }) => { + return buildMarkdownRouteResponse(await getMarkdownRouteBody([], request), []); }, }, }, diff --git a/src/routes/{$}[.]md.ts b/src/routes/{$}[.]md.ts new file mode 100644 index 0000000..149b00a --- /dev/null +++ b/src/routes/{$}[.]md.ts @@ -0,0 +1,27 @@ +import { createFileRoute, notFound } from "@tanstack/react-router"; +import { + buildMarkdownRouteResponse, + getMarkdownForPageSlugs, + slugsFromMarkdownSplat, +} from "@/lib/markdown-route"; + +async function getMarkdownResponse( + params: { _splat?: string }, + request: Request, + includeBody = true, +) { + const slugs = slugsFromMarkdownSplat(params._splat); + const body = await getMarkdownForPageSlugs(slugs, request); + if (!body) throw notFound(); + + return buildMarkdownRouteResponse(includeBody ? body : null, slugs); +} + +export const Route = createFileRoute("/{$}.md")({ + server: { + handlers: { + GET: async ({ params, request }) => getMarkdownResponse(params, request), + HEAD: async ({ params, request }) => getMarkdownResponse(params, request, false), + }, + }, +}); diff --git a/src/routes/{$}[.]mdx.ts b/src/routes/{$}[.]mdx.ts new file mode 100644 index 0000000..d1dde82 --- /dev/null +++ b/src/routes/{$}[.]mdx.ts @@ -0,0 +1,27 @@ +import { createFileRoute, notFound } from "@tanstack/react-router"; +import { + buildMarkdownRouteResponse, + getMarkdownForPageSlugs, + slugsFromMarkdownSplat, +} from "@/lib/markdown-route"; + +async function getMarkdownResponse( + params: { _splat?: string }, + request: Request, + includeBody = true, +) { + const slugs = slugsFromMarkdownSplat(params._splat); + const body = await getMarkdownForPageSlugs(slugs, request); + if (!body) throw notFound(); + + return buildMarkdownRouteResponse(includeBody ? body : null, slugs); +} + +export const Route = createFileRoute("/{$}.mdx")({ + server: { + handlers: { + GET: async ({ params, request }) => getMarkdownResponse(params, request), + HEAD: async ({ params, request }) => getMarkdownResponse(params, request, false), + }, + }, +}); diff --git a/src/start.test.ts b/src/start.test.ts index a2a858e..bb24c93 100644 --- a/src/start.test.ts +++ b/src/start.test.ts @@ -66,18 +66,12 @@ describe("negotiateDocsRepresentation", () => { describe("resolveLLMPath", () => { test("rewrites canonical docs paths only when markdown wins negotiation", () => { - assert.equal(resolveLLMPath(docsRequest("text/markdown")), "/docs/llms.mdx/docs/dashboard"); + assert.equal(resolveLLMPath(docsRequest("text/markdown")), "/docs/dashboard.md"); assert.equal(resolveLLMPath(docsRequest("text/html;q=1,text/markdown;q=0.5")), undefined); }); - test("rewrites direct markdown suffixes regardless of Accept", () => { - assert.equal( - resolveLLMPath(docsRequest("text/html", "/docs/dashboard.md")), - "/docs/llms.mdx/docs/dashboard", - ); - assert.equal( - resolveLLMPath(docsRequest("text/html", "/docs/dashboard.mdx")), - "/docs/llms.mdx/docs/dashboard", - ); + test("leaves direct markdown suffixes on their stable URL routes", () => { + assert.equal(resolveLLMPath(docsRequest("text/html", "/docs/dashboard.md")), undefined); + assert.equal(resolveLLMPath(docsRequest("text/html", "/docs/dashboard.mdx")), undefined); }); }); diff --git a/src/start.ts b/src/start.ts index 9792180..141847d 100644 --- a/src/start.ts +++ b/src/start.ts @@ -1,26 +1,15 @@ import { createMiddleware, createStart } from "@tanstack/react-start"; -import { getNegotiator, rewritePath } from "fumadocs-core/negotiation"; +import { getNegotiator } from "fumadocs-core/negotiation"; import { redirect } from "@tanstack/react-router"; import { buildRedirectRouteRules } from "./lib/redirect-route-rules"; import { DOCS_BASE } from "./lib/url-base"; import { appendHeaderValue, appendVaryAccept, + buildMarkdownAlternatePath, buildMarkdownAlternateLinkHeader, } from "./lib/markdown-alternate"; -const { rewrite: rewriteLLMMarkdown } = rewritePath( - `${DOCS_BASE}{/*path}.md`, - `${DOCS_BASE}/llms.mdx/docs{/*path}`, -); -const { rewrite: rewriteLLMMdx } = rewritePath( - `${DOCS_BASE}{/*path}.mdx`, - `${DOCS_BASE}/llms.mdx/docs{/*path}`, -); -const { rewrite: rewriteLLMPreferred } = rewritePath( - `${DOCS_BASE}{/*path}`, - `${DOCS_BASE}/llms.mdx/docs{/*path}`, -); const legacyRedirectRules = buildRedirectRouteRules(); const redirectBypassPrefixes = [ `${DOCS_BASE}/_serverFn`, @@ -28,6 +17,7 @@ const redirectBypassPrefixes = [ `${DOCS_BASE}/assets`, ]; const staticAssetPathPattern = /\.[a-z0-9]{2,8}$/i; +const directMarkdownPathPattern = /\.mdx?$/i; const markdownMediaTypes = ["text/markdown", "text/plain", "text/x-markdown"] as const; const negotiatedMediaTypes = ["text/html", ...markdownMediaTypes] as const; type DocsRepresentation = "text/html" | "text/markdown"; @@ -68,6 +58,7 @@ function shouldBypassLLMRewrite(pathname: string): boolean { if ( pathname === `${DOCS_BASE}/llms.mdx` || pathname.startsWith(`${DOCS_BASE}/llms.mdx/`) || + directMarkdownPathPattern.test(pathname) || redirectBypassPrefixes.some( (prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`), ) @@ -115,12 +106,9 @@ export function resolveLLMPath(request: Request): string | undefined { const url = new URL(request.url); if (shouldBypassLLMRewrite(url.pathname)) return undefined; - const directRewrite = rewriteLLMMarkdown(url.pathname) || rewriteLLMMdx(url.pathname); - if (directRewrite) return directRewrite; - if (negotiateDocsRepresentation(request) !== "text/markdown") return undefined; - return rewriteLLMPreferred(url.pathname) || undefined; + return buildMarkdownAlternatePath(url.pathname); } export function resolveLegacyRedirect(requestUrl: URL): string | undefined { From e9b8fc82351e4ddcd864cbb639559dddfe2195be Mon Sep 17 00:00:00 2001 From: Duncan Crawbuck Date: Fri, 5 Jun 2026 16:21:33 -0700 Subject: [PATCH 3/9] Add CORS header to markdown redirects --- src/start.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/start.ts b/src/start.ts index 141847d..a0d5848 100644 --- a/src/start.ts +++ b/src/start.ts @@ -158,6 +158,7 @@ const llmMiddleware = createMiddleware().server(async ({ next, request }) => { href: withRequestSearch(url, destination).toString(), statusCode: 307, headers: { + "Access-Control-Allow-Origin": "*", Vary: "Accept", }, }); From aa118fffcb7891748aea4a875ad67d4b216b9904 Mon Sep 17 00:00:00 2001 From: Duncan Crawbuck Date: Fri, 5 Jun 2026 17:23:38 -0700 Subject: [PATCH 4/9] Fix markdown negotiation edge cases --- src/lib/markdown-alternate.ts | 2 +- src/start.ts | 29 +++++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/lib/markdown-alternate.ts b/src/lib/markdown-alternate.ts index 16e96c9..366d719 100644 --- a/src/lib/markdown-alternate.ts +++ b/src/lib/markdown-alternate.ts @@ -1,6 +1,6 @@ import { DOCS_BASE } from "./url-base"; -const MARKDOWN_ROUTE_PREFIX = `${DOCS_BASE}/llms.mdx/docs`; +const MARKDOWN_ROUTE_PREFIX = `${DOCS_BASE}/llms.mdx/docs/`; function normalizeDocsPath(pathname: string): string { const normalized = pathname.replace(/\/+$/, ""); diff --git a/src/start.ts b/src/start.ts index a0d5848..6dd6599 100644 --- a/src/start.ts +++ b/src/start.ts @@ -41,6 +41,15 @@ function withRequestSearch(requestUrl: URL, destination: string): URL { return targetUrl; } +async function isKnownDocsRoutePath(pathname: string): Promise { + const normalizedPath = normalizePathname(pathname); + if (normalizedPath === DOCS_BASE || normalizedPath === `${DOCS_BASE}/sdk` || normalizedPath.startsWith(`${DOCS_BASE}/sdk/`)) return true; + + const docsPath = normalizedPath.slice(DOCS_BASE.length).replace(/^\/+/, ""); + const { source } = await import("./lib/source"); + return Boolean(source.getPage(docsPath.split("/").filter(Boolean))); +} + function shouldBypassLegacyRedirect(pathname: string): boolean { if (!pathname.startsWith(DOCS_BASE)) return true; if ( @@ -86,6 +95,16 @@ function buildNotAcceptableResponse(): Response { }); } +function buildNotFoundResponse(): Response { + return new Response("Not Found", { + status: 404, + headers: { + "Content-Type": "text/plain; charset=utf-8", + Vary: "Accept", + }, + }); +} + function withDocsNegotiationHeaders(response: Response, pathname: string): Response { const headers = new Headers(response.headers); appendVaryAccept(headers); @@ -164,14 +183,20 @@ const llmMiddleware = createMiddleware().server(async ({ next, request }) => { }); } - if (!shouldBypassLLMRewrite(url.pathname) && !representation) { + const bypassLLMRewrite = shouldBypassLLMRewrite(url.pathname); + + if (!bypassLLMRewrite && !representation) { + if (!(await isKnownDocsRoutePath(url.pathname))) { + return buildNotFoundResponse(); + } + return buildNotAcceptableResponse(); } const result = await next(); return { ...result, - response: !shouldBypassLLMRewrite(url.pathname) + response: !bypassLLMRewrite ? withDocsNegotiationHeaders(result.response, url.pathname) : result.response, }; From fd4762026c6f9f82319f91bae0246a09c8ddf14b Mon Sep 17 00:00:00 2001 From: Duncan Crawbuck Date: Tue, 9 Jun 2026 13:52:02 -0700 Subject: [PATCH 5/9] Add section llms aliases --- content/shared/vibe-coding.mdx | 16 +++++----- src/lib/llms.ts | 30 ++++++++++++++++-- src/routeTree.gen.ts | 42 ++++++++++++++++++++++++++ src/routes/$section/llms-full[.]txt.ts | 15 +++++++++ src/routes/$section/llms[.]txt.ts | 15 +++++++++ 5 files changed, 107 insertions(+), 11 deletions(-) create mode 100644 src/routes/$section/llms-full[.]txt.ts create mode 100644 src/routes/$section/llms[.]txt.ts diff --git a/content/shared/vibe-coding.mdx b/content/shared/vibe-coding.mdx index de507bc..c1db278 100644 --- a/content/shared/vibe-coding.mdx +++ b/content/shared/vibe-coding.mdx @@ -81,13 +81,13 @@ The Superwall Docs website has `llms.txt` and `llms-full.txt` files, in total an | SDK | Summary | Full Text | | ------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | All | [`llms.txt`](https://superwall.com/docs/llms.txt) | [`llms-full.txt`](https://superwall.com/docs/llms-full.txt) | -| Dashboard | [`llms-dashboard.txt`](https://superwall.com/docs/llms-dashboard.txt) | [`llms-full-dashboard.txt`](https://superwall.com/docs/llms-full-dashboard.txt) | -| iOS | [`llms-ios.txt`](https://superwall.com/docs/llms-ios.txt) | [`llms-full-ios.txt`](https://superwall.com/docs/llms-full-ios.txt) | -| Android | [`llms-android.txt`](https://superwall.com/docs/llms-android.txt) | [`llms-full-android.txt`](https://superwall.com/docs/llms-full-android.txt) | -| Flutter | [`llms-flutter.txt`](https://superwall.com/docs/llms-flutter.txt) | [`llms-full-flutter.txt`](https://superwall.com/docs/llms-full-flutter.txt) | -| Expo | [`llms-expo.txt`](https://superwall.com/docs/llms-expo.txt) | [`llms-full-expo.txt`](https://superwall.com/docs/llms-full-expo.txt) | -| React Native (Deprecated) | [`llms-react-native.txt`](https://superwall.com/docs/llms-react-native.txt) | [`llms-full-react-native.txt`](https://superwall.com/docs/llms-full-react-native.txt) | -| Integrations | [`llms-integrations.txt`](https://superwall.com/docs/llms-integrations.txt) | [`llms-full-integrations.txt`](https://superwall.com/docs/llms-full-integrations.txt) | -| Web Checkout | [`llms-web-checkout.txt`](https://superwall.com/docs/llms-web-checkout.txt) | [`llms-full-web-checkout.txt`](https://superwall.com/docs/llms-full-web-checkout.txt) | +| Dashboard | [`dashboard/llms.txt`](https://superwall.com/docs/dashboard/llms.txt) | [`dashboard/llms-full.txt`](https://superwall.com/docs/dashboard/llms-full.txt) | +| iOS | [`ios/llms.txt`](https://superwall.com/docs/ios/llms.txt) | [`ios/llms-full.txt`](https://superwall.com/docs/ios/llms-full.txt) | +| Android | [`android/llms.txt`](https://superwall.com/docs/android/llms.txt) | [`android/llms-full.txt`](https://superwall.com/docs/android/llms-full.txt) | +| Flutter | [`flutter/llms.txt`](https://superwall.com/docs/flutter/llms.txt) | [`flutter/llms-full.txt`](https://superwall.com/docs/flutter/llms-full.txt) | +| Expo | [`expo/llms.txt`](https://superwall.com/docs/expo/llms.txt) | [`expo/llms-full.txt`](https://superwall.com/docs/expo/llms-full.txt) | +| React Native (Deprecated) | [`react-native/llms.txt`](https://superwall.com/docs/react-native/llms.txt) | [`react-native/llms-full.txt`](https://superwall.com/docs/react-native/llms-full.txt) | +| Integrations | [`integrations/llms.txt`](https://superwall.com/docs/integrations/llms.txt) | [`integrations/llms-full.txt`](https://superwall.com/docs/integrations/llms-full.txt) | +| Web Checkout | [`web-checkout/llms.txt`](https://superwall.com/docs/web-checkout/llms.txt) | [`web-checkout/llms-full.txt`](https://superwall.com/docs/web-checkout/llms-full.txt) | To minimize token use, we recommend using the files specific to your SDK. diff --git a/src/lib/llms.ts b/src/lib/llms.ts index c5bd6a4..1653d1a 100644 --- a/src/lib/llms.ts +++ b/src/lib/llms.ts @@ -51,6 +51,24 @@ export function getLLMSectionConfig(section?: string) { return llmsSectionConfigs[section as LLMSection]; } +export function buildLLMSummaryPath(section: string) { + const config = getLLMSectionConfig(section); + return config ? `${config.urlPrefix}/llms.txt` : buildLegacyLLMSummaryPath(section); +} + +export function buildLLMFullPath(section: string) { + const config = getLLMSectionConfig(section); + return config ? `${config.urlPrefix}/llms-full.txt` : buildLegacyLLMFullPath(section); +} + +export function buildLegacyLLMSummaryPath(section: string) { + return `/docs/llms-${section}.txt`; +} + +export function buildLegacyLLMFullPath(section: string) { + return `/docs/llms-full-${section}.txt`; +} + export function getPagesForLLMSection(pages: TPage[], section?: string) { const config = getLLMSectionConfig(section); if (!config) return pages; @@ -104,16 +122,22 @@ export function buildLLMSummaryTextFromPages(pages: LLMPageLike[], section?: str // Top-level: list all platform-specific docs lines.push("## Platform-specific docs", ""); for (const [key, val] of Object.entries(llmsSectionConfigs)) { - lines.push(`- [${val.label} Documentation](/docs/llms-${key}.txt)`); + lines.push(`- [${val.label} Documentation](${buildLLMSummaryPath(key)}) ([full text](${buildLLMFullPath(key)}))`); } - lines.push("", "## All Pages", ""); + lines.push( + "", + "Legacy root-level aliases such as `/docs/llms-ios.txt` and `/docs/llms-full-ios.txt` remain supported.", + "", + "## All Pages", + "", + ); } else { // Section-specific: link back to top-level + list other sections lines.push("## Other sections", ""); lines.push("- [All Documentation](/docs/llms.txt)"); for (const [key, val] of Object.entries(llmsSectionConfigs)) { if (key !== section) { - lines.push(`- [${val.label} Documentation](/docs/llms-${key}.txt)`); + lines.push(`- [${val.label} Documentation](${buildLLMSummaryPath(key)})`); } } lines.push(""); diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 63b4924..769dfc0 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -24,6 +24,8 @@ import { Route as SdkSplatRouteImport } from './routes/sdk/$' import { Route as LlmsDotmdxDocsRouteImport } from './routes/llms[.]mdx.docs' import { Route as ApiSearchRouteImport } from './routes/api/search' import { Route as ApiFeedbackRouteImport } from './routes/api/feedback' +import { Route as SectionLlmsDottxtRouteImport } from './routes/$section/llms[.]txt' +import { Route as SectionLlmsFullDottxtRouteImport } from './routes/$section/llms-full[.]txt' import { Route as LlmsDotmdxDocsIndexRouteImport } from './routes/llms[.]mdx.docs.index' import { Route as LlmsDotmdxDocsSplatRouteImport } from './routes/llms[.]mdx.docs.$' import { Route as ApiRawSplatRouteImport } from './routes/api/raw/$' @@ -105,6 +107,16 @@ const ApiFeedbackRoute = ApiFeedbackRouteImport.update({ path: '/api/feedback', getParentRoute: () => rootRouteImport, } as any) +const SectionLlmsDottxtRoute = SectionLlmsDottxtRouteImport.update({ + id: '/$section/llms.txt', + path: '/$section/llms.txt', + getParentRoute: () => rootRouteImport, +} as any) +const SectionLlmsFullDottxtRoute = SectionLlmsFullDottxtRouteImport.update({ + id: '/$section/llms-full.txt', + path: '/$section/llms-full.txt', + getParentRoute: () => rootRouteImport, +} as any) const LlmsDotmdxDocsIndexRoute = LlmsDotmdxDocsIndexRouteImport.update({ id: '/', path: '/', @@ -132,6 +144,8 @@ export interface FileRoutesByFullPath { '/sitemap.xml': typeof SitemapDotxmlRoute '/{$}.md': typeof Char123Char125DotmdRoute '/{$}.mdx': typeof Char123Char125DotmdxRoute + '/$section/llms-full.txt': typeof SectionLlmsFullDottxtRoute + '/$section/llms.txt': typeof SectionLlmsDottxtRoute '/api/feedback': typeof ApiFeedbackRoute '/api/search': typeof ApiSearchRoute '/llms.mdx/docs': typeof LlmsDotmdxDocsRouteWithChildren @@ -152,6 +166,8 @@ export interface FileRoutesByTo { '/sitemap.xml': typeof SitemapDotxmlRoute '/{$}.md': typeof Char123Char125DotmdRoute '/{$}.mdx': typeof Char123Char125DotmdxRoute + '/$section/llms-full.txt': typeof SectionLlmsFullDottxtRoute + '/$section/llms.txt': typeof SectionLlmsDottxtRoute '/api/feedback': typeof ApiFeedbackRoute '/api/search': typeof ApiSearchRoute '/sdk/$': typeof SdkSplatRoute @@ -172,6 +188,8 @@ export interface FileRoutesById { '/sitemap.xml': typeof SitemapDotxmlRoute '/{$}.md': typeof Char123Char125DotmdRoute '/{$}.mdx': typeof Char123Char125DotmdxRoute + '/$section/llms-full.txt': typeof SectionLlmsFullDottxtRoute + '/$section/llms.txt': typeof SectionLlmsDottxtRoute '/api/feedback': typeof ApiFeedbackRoute '/api/search': typeof ApiSearchRoute '/llms.mdx/docs': typeof LlmsDotmdxDocsRouteWithChildren @@ -194,6 +212,8 @@ export interface FileRouteTypes { | '/sitemap.xml' | '/{$}.md' | '/{$}.mdx' + | '/$section/llms-full.txt' + | '/$section/llms.txt' | '/api/feedback' | '/api/search' | '/llms.mdx/docs' @@ -214,6 +234,8 @@ export interface FileRouteTypes { | '/sitemap.xml' | '/{$}.md' | '/{$}.mdx' + | '/$section/llms-full.txt' + | '/$section/llms.txt' | '/api/feedback' | '/api/search' | '/sdk/$' @@ -233,6 +255,8 @@ export interface FileRouteTypes { | '/sitemap.xml' | '/{$}.md' | '/{$}.mdx' + | '/$section/llms-full.txt' + | '/$section/llms.txt' | '/api/feedback' | '/api/search' | '/llms.mdx/docs' @@ -254,6 +278,8 @@ export interface RootRouteChildren { SitemapDotxmlRoute: typeof SitemapDotxmlRoute Char123Char125DotmdRoute: typeof Char123Char125DotmdRoute Char123Char125DotmdxRoute: typeof Char123Char125DotmdxRoute + SectionLlmsFullDottxtRoute: typeof SectionLlmsFullDottxtRoute + SectionLlmsDottxtRoute: typeof SectionLlmsDottxtRoute ApiFeedbackRoute: typeof ApiFeedbackRoute ApiSearchRoute: typeof ApiSearchRoute LlmsDotmdxDocsRoute: typeof LlmsDotmdxDocsRouteWithChildren @@ -369,6 +395,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiFeedbackRouteImport parentRoute: typeof rootRouteImport } + '/$section/llms.txt': { + id: '/$section/llms.txt' + path: '/$section/llms.txt' + fullPath: '/$section/llms.txt' + preLoaderRoute: typeof SectionLlmsDottxtRouteImport + parentRoute: typeof rootRouteImport + } + '/$section/llms-full.txt': { + id: '/$section/llms-full.txt' + path: '/$section/llms-full.txt' + fullPath: '/$section/llms-full.txt' + preLoaderRoute: typeof SectionLlmsFullDottxtRouteImport + parentRoute: typeof rootRouteImport + } '/llms.mdx/docs/': { id: '/llms.mdx/docs/' path: '/' @@ -419,6 +459,8 @@ const rootRouteChildren: RootRouteChildren = { SitemapDotxmlRoute: SitemapDotxmlRoute, Char123Char125DotmdRoute: Char123Char125DotmdRoute, Char123Char125DotmdxRoute: Char123Char125DotmdxRoute, + SectionLlmsFullDottxtRoute: SectionLlmsFullDottxtRoute, + SectionLlmsDottxtRoute: SectionLlmsDottxtRoute, ApiFeedbackRoute: ApiFeedbackRoute, ApiSearchRoute: ApiSearchRoute, LlmsDotmdxDocsRoute: LlmsDotmdxDocsRouteWithChildren, diff --git a/src/routes/$section/llms-full[.]txt.ts b/src/routes/$section/llms-full[.]txt.ts new file mode 100644 index 0000000..3034c25 --- /dev/null +++ b/src/routes/$section/llms-full[.]txt.ts @@ -0,0 +1,15 @@ +import { buildLLMFullText, buildLLMResponse, getLLMSectionConfig } from "@/lib/llms"; +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/$section/llms-full.txt")({ + server: { + handlers: { + GET: async ({ params }) => { + if (!getLLMSectionConfig(params.section)) { + return new Response("LLMS section not found", { status: 404 }); + } + return buildLLMResponse(await buildLLMFullText(params.section)); + }, + }, + }, +}); diff --git a/src/routes/$section/llms[.]txt.ts b/src/routes/$section/llms[.]txt.ts new file mode 100644 index 0000000..e837927 --- /dev/null +++ b/src/routes/$section/llms[.]txt.ts @@ -0,0 +1,15 @@ +import { buildLLMResponse, buildLLMSummaryText, getLLMSectionConfig } from "@/lib/llms"; +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/$section/llms.txt")({ + server: { + handlers: { + GET: async ({ params }) => { + if (!getLLMSectionConfig(params.section)) { + return new Response("LLMS section not found", { status: 404 }); + } + return buildLLMResponse(await buildLLMSummaryText(params.section)); + }, + }, + }, +}); From f4cc144f998b174543d639eb6152228f2401e030 Mon Sep 17 00:00:00 2001 From: Duncan Crawbuck Date: Wed, 10 Jun 2026 16:23:45 -0700 Subject: [PATCH 6/9] Use explicit llms section routes --- src/lib/llms.ts | 21 + src/routeTree.gen.ts | 449 +++++++++++++++++++-- src/routes/$section/llms-full[.]txt.ts | 15 - src/routes/$section/llms[.]txt.ts | 15 - src/routes/agents/llms-full[.]txt.ts | 10 + src/routes/agents/llms[.]txt.ts | 10 + src/routes/android/llms-full[.]txt.ts | 10 + src/routes/android/llms[.]txt.ts | 10 + src/routes/dashboard/llms-full[.]txt.ts | 10 + src/routes/dashboard/llms[.]txt.ts | 10 + src/routes/expo/llms-full[.]txt.ts | 10 + src/routes/expo/llms[.]txt.ts | 10 + src/routes/flutter/llms-full[.]txt.ts | 10 + src/routes/flutter/llms[.]txt.ts | 10 + src/routes/integrations/llms-full[.]txt.ts | 10 + src/routes/integrations/llms[.]txt.ts | 10 + src/routes/ios/llms-full[.]txt.ts | 10 + src/routes/ios/llms[.]txt.ts | 10 + src/routes/react-native/llms-full[.]txt.ts | 10 + src/routes/react-native/llms[.]txt.ts | 10 + src/routes/unity/llms-full[.]txt.ts | 10 + src/routes/unity/llms[.]txt.ts | 10 + src/routes/web-checkout/llms-full[.]txt.ts | 10 + src/routes/web-checkout/llms[.]txt.ts | 10 + 24 files changed, 636 insertions(+), 64 deletions(-) delete mode 100644 src/routes/$section/llms-full[.]txt.ts delete mode 100644 src/routes/$section/llms[.]txt.ts create mode 100644 src/routes/agents/llms-full[.]txt.ts create mode 100644 src/routes/agents/llms[.]txt.ts create mode 100644 src/routes/android/llms-full[.]txt.ts create mode 100644 src/routes/android/llms[.]txt.ts create mode 100644 src/routes/dashboard/llms-full[.]txt.ts create mode 100644 src/routes/dashboard/llms[.]txt.ts create mode 100644 src/routes/expo/llms-full[.]txt.ts create mode 100644 src/routes/expo/llms[.]txt.ts create mode 100644 src/routes/flutter/llms-full[.]txt.ts create mode 100644 src/routes/flutter/llms[.]txt.ts create mode 100644 src/routes/integrations/llms-full[.]txt.ts create mode 100644 src/routes/integrations/llms[.]txt.ts create mode 100644 src/routes/ios/llms-full[.]txt.ts create mode 100644 src/routes/ios/llms[.]txt.ts create mode 100644 src/routes/react-native/llms-full[.]txt.ts create mode 100644 src/routes/react-native/llms[.]txt.ts create mode 100644 src/routes/unity/llms-full[.]txt.ts create mode 100644 src/routes/unity/llms[.]txt.ts create mode 100644 src/routes/web-checkout/llms-full[.]txt.ts create mode 100644 src/routes/web-checkout/llms[.]txt.ts diff --git a/src/lib/llms.ts b/src/lib/llms.ts index 1653d1a..7db001f 100644 --- a/src/lib/llms.ts +++ b/src/lib/llms.ts @@ -3,6 +3,10 @@ export const llmsSectionConfigs = { label: "Dashboard", urlPrefix: "/docs/dashboard", }, + agents: { + label: "Agents", + urlPrefix: "/docs/agents", + }, ios: { label: "iOS", urlPrefix: "/docs/ios", @@ -87,6 +91,7 @@ function getExamplePathForSection(section: string): string { unity: "/docs/unity/guides/game-controller-input.md", "react-native": "/docs/react-native/guides/advanced/game-controller-support.md", dashboard: "/docs/dashboard/dashboard-settings/overview-settings-apple-search-ads.md", + agents: "/docs/agents/create-an-agent.md", integrations: "/docs/integrations/amplitude.md", "web-checkout": "/docs/web-checkout/overview.md", }; @@ -157,6 +162,14 @@ export async function buildLLMSummaryText(section?: string) { return buildLLMSummaryTextFromPages(source.getPages(), section); } +export async function buildLLMSummaryResponseForSection(section: string) { + if (!getLLMSectionConfig(section)) { + return new Response("LLMS section not found", { status: 404 }); + } + + return buildLLMResponse(await buildLLMSummaryText(section)); +} + export async function buildLLMFullText(section?: string) { const config = getLLMSectionConfig(section); const title = config ? `${config.label} Documentation` : "Superwall Documentation"; @@ -167,6 +180,14 @@ export async function buildLLMFullText(section?: string) { return `# ${title}\n\n${scanned.join("\n\n")}`; } +export async function buildLLMFullResponseForSection(section: string) { + if (!getLLMSectionConfig(section)) { + return new Response("LLMS section not found", { status: 404 }); + } + + return buildLLMResponse(await buildLLMFullText(section)); +} + export function buildLLMResponse(body: string) { return new Response(body, { headers: { diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 769dfc0..a064b4f 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -20,12 +20,30 @@ import { Route as LlmsFullChar123sectionChar125DottxtRouteImport } from './route import { Route as SplatRouteImport } from './routes/$' import { Route as IndexRouteImport } from './routes/index' import { Route as SdkIndexRouteImport } from './routes/sdk/index' +import { Route as WebCheckoutLlmsDottxtRouteImport } from './routes/web-checkout/llms[.]txt' +import { Route as WebCheckoutLlmsFullDottxtRouteImport } from './routes/web-checkout/llms-full[.]txt' +import { Route as UnityLlmsDottxtRouteImport } from './routes/unity/llms[.]txt' +import { Route as UnityLlmsFullDottxtRouteImport } from './routes/unity/llms-full[.]txt' import { Route as SdkSplatRouteImport } from './routes/sdk/$' +import { Route as ReactNativeLlmsDottxtRouteImport } from './routes/react-native/llms[.]txt' +import { Route as ReactNativeLlmsFullDottxtRouteImport } from './routes/react-native/llms-full[.]txt' import { Route as LlmsDotmdxDocsRouteImport } from './routes/llms[.]mdx.docs' +import { Route as IosLlmsDottxtRouteImport } from './routes/ios/llms[.]txt' +import { Route as IosLlmsFullDottxtRouteImport } from './routes/ios/llms-full[.]txt' +import { Route as IntegrationsLlmsDottxtRouteImport } from './routes/integrations/llms[.]txt' +import { Route as IntegrationsLlmsFullDottxtRouteImport } from './routes/integrations/llms-full[.]txt' +import { Route as FlutterLlmsDottxtRouteImport } from './routes/flutter/llms[.]txt' +import { Route as FlutterLlmsFullDottxtRouteImport } from './routes/flutter/llms-full[.]txt' +import { Route as ExpoLlmsDottxtRouteImport } from './routes/expo/llms[.]txt' +import { Route as ExpoLlmsFullDottxtRouteImport } from './routes/expo/llms-full[.]txt' +import { Route as DashboardLlmsDottxtRouteImport } from './routes/dashboard/llms[.]txt' +import { Route as DashboardLlmsFullDottxtRouteImport } from './routes/dashboard/llms-full[.]txt' import { Route as ApiSearchRouteImport } from './routes/api/search' import { Route as ApiFeedbackRouteImport } from './routes/api/feedback' -import { Route as SectionLlmsDottxtRouteImport } from './routes/$section/llms[.]txt' -import { Route as SectionLlmsFullDottxtRouteImport } from './routes/$section/llms-full[.]txt' +import { Route as AndroidLlmsDottxtRouteImport } from './routes/android/llms[.]txt' +import { Route as AndroidLlmsFullDottxtRouteImport } from './routes/android/llms-full[.]txt' +import { Route as AgentsLlmsDottxtRouteImport } from './routes/agents/llms[.]txt' +import { Route as AgentsLlmsFullDottxtRouteImport } from './routes/agents/llms-full[.]txt' import { Route as LlmsDotmdxDocsIndexRouteImport } from './routes/llms[.]mdx.docs.index' import { Route as LlmsDotmdxDocsSplatRouteImport } from './routes/llms[.]mdx.docs.$' import { Route as ApiRawSplatRouteImport } from './routes/api/raw/$' @@ -87,16 +105,99 @@ const SdkIndexRoute = SdkIndexRouteImport.update({ path: '/sdk/', getParentRoute: () => rootRouteImport, } as any) +const WebCheckoutLlmsDottxtRoute = WebCheckoutLlmsDottxtRouteImport.update({ + id: '/web-checkout/llms.txt', + path: '/web-checkout/llms.txt', + getParentRoute: () => rootRouteImport, +} as any) +const WebCheckoutLlmsFullDottxtRoute = + WebCheckoutLlmsFullDottxtRouteImport.update({ + id: '/web-checkout/llms-full.txt', + path: '/web-checkout/llms-full.txt', + getParentRoute: () => rootRouteImport, + } as any) +const UnityLlmsDottxtRoute = UnityLlmsDottxtRouteImport.update({ + id: '/unity/llms.txt', + path: '/unity/llms.txt', + getParentRoute: () => rootRouteImport, +} as any) +const UnityLlmsFullDottxtRoute = UnityLlmsFullDottxtRouteImport.update({ + id: '/unity/llms-full.txt', + path: '/unity/llms-full.txt', + getParentRoute: () => rootRouteImport, +} as any) const SdkSplatRoute = SdkSplatRouteImport.update({ id: '/sdk/$', path: '/sdk/$', getParentRoute: () => rootRouteImport, } as any) +const ReactNativeLlmsDottxtRoute = ReactNativeLlmsDottxtRouteImport.update({ + id: '/react-native/llms.txt', + path: '/react-native/llms.txt', + getParentRoute: () => rootRouteImport, +} as any) +const ReactNativeLlmsFullDottxtRoute = + ReactNativeLlmsFullDottxtRouteImport.update({ + id: '/react-native/llms-full.txt', + path: '/react-native/llms-full.txt', + getParentRoute: () => rootRouteImport, + } as any) const LlmsDotmdxDocsRoute = LlmsDotmdxDocsRouteImport.update({ id: '/llms.mdx/docs', path: '/llms.mdx/docs', getParentRoute: () => rootRouteImport, } as any) +const IosLlmsDottxtRoute = IosLlmsDottxtRouteImport.update({ + id: '/ios/llms.txt', + path: '/ios/llms.txt', + getParentRoute: () => rootRouteImport, +} as any) +const IosLlmsFullDottxtRoute = IosLlmsFullDottxtRouteImport.update({ + id: '/ios/llms-full.txt', + path: '/ios/llms-full.txt', + getParentRoute: () => rootRouteImport, +} as any) +const IntegrationsLlmsDottxtRoute = IntegrationsLlmsDottxtRouteImport.update({ + id: '/integrations/llms.txt', + path: '/integrations/llms.txt', + getParentRoute: () => rootRouteImport, +} as any) +const IntegrationsLlmsFullDottxtRoute = + IntegrationsLlmsFullDottxtRouteImport.update({ + id: '/integrations/llms-full.txt', + path: '/integrations/llms-full.txt', + getParentRoute: () => rootRouteImport, + } as any) +const FlutterLlmsDottxtRoute = FlutterLlmsDottxtRouteImport.update({ + id: '/flutter/llms.txt', + path: '/flutter/llms.txt', + getParentRoute: () => rootRouteImport, +} as any) +const FlutterLlmsFullDottxtRoute = FlutterLlmsFullDottxtRouteImport.update({ + id: '/flutter/llms-full.txt', + path: '/flutter/llms-full.txt', + getParentRoute: () => rootRouteImport, +} as any) +const ExpoLlmsDottxtRoute = ExpoLlmsDottxtRouteImport.update({ + id: '/expo/llms.txt', + path: '/expo/llms.txt', + getParentRoute: () => rootRouteImport, +} as any) +const ExpoLlmsFullDottxtRoute = ExpoLlmsFullDottxtRouteImport.update({ + id: '/expo/llms-full.txt', + path: '/expo/llms-full.txt', + getParentRoute: () => rootRouteImport, +} as any) +const DashboardLlmsDottxtRoute = DashboardLlmsDottxtRouteImport.update({ + id: '/dashboard/llms.txt', + path: '/dashboard/llms.txt', + getParentRoute: () => rootRouteImport, +} as any) +const DashboardLlmsFullDottxtRoute = DashboardLlmsFullDottxtRouteImport.update({ + id: '/dashboard/llms-full.txt', + path: '/dashboard/llms-full.txt', + getParentRoute: () => rootRouteImport, +} as any) const ApiSearchRoute = ApiSearchRouteImport.update({ id: '/api/search', path: '/api/search', @@ -107,14 +208,24 @@ const ApiFeedbackRoute = ApiFeedbackRouteImport.update({ path: '/api/feedback', getParentRoute: () => rootRouteImport, } as any) -const SectionLlmsDottxtRoute = SectionLlmsDottxtRouteImport.update({ - id: '/$section/llms.txt', - path: '/$section/llms.txt', +const AndroidLlmsDottxtRoute = AndroidLlmsDottxtRouteImport.update({ + id: '/android/llms.txt', + path: '/android/llms.txt', + getParentRoute: () => rootRouteImport, +} as any) +const AndroidLlmsFullDottxtRoute = AndroidLlmsFullDottxtRouteImport.update({ + id: '/android/llms-full.txt', + path: '/android/llms-full.txt', + getParentRoute: () => rootRouteImport, +} as any) +const AgentsLlmsDottxtRoute = AgentsLlmsDottxtRouteImport.update({ + id: '/agents/llms.txt', + path: '/agents/llms.txt', getParentRoute: () => rootRouteImport, } as any) -const SectionLlmsFullDottxtRoute = SectionLlmsFullDottxtRouteImport.update({ - id: '/$section/llms-full.txt', - path: '/$section/llms-full.txt', +const AgentsLlmsFullDottxtRoute = AgentsLlmsFullDottxtRouteImport.update({ + id: '/agents/llms-full.txt', + path: '/agents/llms-full.txt', getParentRoute: () => rootRouteImport, } as any) const LlmsDotmdxDocsIndexRoute = LlmsDotmdxDocsIndexRouteImport.update({ @@ -144,12 +255,30 @@ export interface FileRoutesByFullPath { '/sitemap.xml': typeof SitemapDotxmlRoute '/{$}.md': typeof Char123Char125DotmdRoute '/{$}.mdx': typeof Char123Char125DotmdxRoute - '/$section/llms-full.txt': typeof SectionLlmsFullDottxtRoute - '/$section/llms.txt': typeof SectionLlmsDottxtRoute + '/agents/llms-full.txt': typeof AgentsLlmsFullDottxtRoute + '/agents/llms.txt': typeof AgentsLlmsDottxtRoute + '/android/llms-full.txt': typeof AndroidLlmsFullDottxtRoute + '/android/llms.txt': typeof AndroidLlmsDottxtRoute '/api/feedback': typeof ApiFeedbackRoute '/api/search': typeof ApiSearchRoute + '/dashboard/llms-full.txt': typeof DashboardLlmsFullDottxtRoute + '/dashboard/llms.txt': typeof DashboardLlmsDottxtRoute + '/expo/llms-full.txt': typeof ExpoLlmsFullDottxtRoute + '/expo/llms.txt': typeof ExpoLlmsDottxtRoute + '/flutter/llms-full.txt': typeof FlutterLlmsFullDottxtRoute + '/flutter/llms.txt': typeof FlutterLlmsDottxtRoute + '/integrations/llms-full.txt': typeof IntegrationsLlmsFullDottxtRoute + '/integrations/llms.txt': typeof IntegrationsLlmsDottxtRoute + '/ios/llms-full.txt': typeof IosLlmsFullDottxtRoute + '/ios/llms.txt': typeof IosLlmsDottxtRoute '/llms.mdx/docs': typeof LlmsDotmdxDocsRouteWithChildren + '/react-native/llms-full.txt': typeof ReactNativeLlmsFullDottxtRoute + '/react-native/llms.txt': typeof ReactNativeLlmsDottxtRoute '/sdk/$': typeof SdkSplatRoute + '/unity/llms-full.txt': typeof UnityLlmsFullDottxtRoute + '/unity/llms.txt': typeof UnityLlmsDottxtRoute + '/web-checkout/llms-full.txt': typeof WebCheckoutLlmsFullDottxtRoute + '/web-checkout/llms.txt': typeof WebCheckoutLlmsDottxtRoute '/sdk/': typeof SdkIndexRoute '/api/raw/$': typeof ApiRawSplatRoute '/llms.mdx/docs/$': typeof LlmsDotmdxDocsSplatRoute @@ -166,11 +295,29 @@ export interface FileRoutesByTo { '/sitemap.xml': typeof SitemapDotxmlRoute '/{$}.md': typeof Char123Char125DotmdRoute '/{$}.mdx': typeof Char123Char125DotmdxRoute - '/$section/llms-full.txt': typeof SectionLlmsFullDottxtRoute - '/$section/llms.txt': typeof SectionLlmsDottxtRoute + '/agents/llms-full.txt': typeof AgentsLlmsFullDottxtRoute + '/agents/llms.txt': typeof AgentsLlmsDottxtRoute + '/android/llms-full.txt': typeof AndroidLlmsFullDottxtRoute + '/android/llms.txt': typeof AndroidLlmsDottxtRoute '/api/feedback': typeof ApiFeedbackRoute '/api/search': typeof ApiSearchRoute + '/dashboard/llms-full.txt': typeof DashboardLlmsFullDottxtRoute + '/dashboard/llms.txt': typeof DashboardLlmsDottxtRoute + '/expo/llms-full.txt': typeof ExpoLlmsFullDottxtRoute + '/expo/llms.txt': typeof ExpoLlmsDottxtRoute + '/flutter/llms-full.txt': typeof FlutterLlmsFullDottxtRoute + '/flutter/llms.txt': typeof FlutterLlmsDottxtRoute + '/integrations/llms-full.txt': typeof IntegrationsLlmsFullDottxtRoute + '/integrations/llms.txt': typeof IntegrationsLlmsDottxtRoute + '/ios/llms-full.txt': typeof IosLlmsFullDottxtRoute + '/ios/llms.txt': typeof IosLlmsDottxtRoute + '/react-native/llms-full.txt': typeof ReactNativeLlmsFullDottxtRoute + '/react-native/llms.txt': typeof ReactNativeLlmsDottxtRoute '/sdk/$': typeof SdkSplatRoute + '/unity/llms-full.txt': typeof UnityLlmsFullDottxtRoute + '/unity/llms.txt': typeof UnityLlmsDottxtRoute + '/web-checkout/llms-full.txt': typeof WebCheckoutLlmsFullDottxtRoute + '/web-checkout/llms.txt': typeof WebCheckoutLlmsDottxtRoute '/sdk': typeof SdkIndexRoute '/api/raw/$': typeof ApiRawSplatRoute '/llms.mdx/docs/$': typeof LlmsDotmdxDocsSplatRoute @@ -188,12 +335,30 @@ export interface FileRoutesById { '/sitemap.xml': typeof SitemapDotxmlRoute '/{$}.md': typeof Char123Char125DotmdRoute '/{$}.mdx': typeof Char123Char125DotmdxRoute - '/$section/llms-full.txt': typeof SectionLlmsFullDottxtRoute - '/$section/llms.txt': typeof SectionLlmsDottxtRoute + '/agents/llms-full.txt': typeof AgentsLlmsFullDottxtRoute + '/agents/llms.txt': typeof AgentsLlmsDottxtRoute + '/android/llms-full.txt': typeof AndroidLlmsFullDottxtRoute + '/android/llms.txt': typeof AndroidLlmsDottxtRoute '/api/feedback': typeof ApiFeedbackRoute '/api/search': typeof ApiSearchRoute + '/dashboard/llms-full.txt': typeof DashboardLlmsFullDottxtRoute + '/dashboard/llms.txt': typeof DashboardLlmsDottxtRoute + '/expo/llms-full.txt': typeof ExpoLlmsFullDottxtRoute + '/expo/llms.txt': typeof ExpoLlmsDottxtRoute + '/flutter/llms-full.txt': typeof FlutterLlmsFullDottxtRoute + '/flutter/llms.txt': typeof FlutterLlmsDottxtRoute + '/integrations/llms-full.txt': typeof IntegrationsLlmsFullDottxtRoute + '/integrations/llms.txt': typeof IntegrationsLlmsDottxtRoute + '/ios/llms-full.txt': typeof IosLlmsFullDottxtRoute + '/ios/llms.txt': typeof IosLlmsDottxtRoute '/llms.mdx/docs': typeof LlmsDotmdxDocsRouteWithChildren + '/react-native/llms-full.txt': typeof ReactNativeLlmsFullDottxtRoute + '/react-native/llms.txt': typeof ReactNativeLlmsDottxtRoute '/sdk/$': typeof SdkSplatRoute + '/unity/llms-full.txt': typeof UnityLlmsFullDottxtRoute + '/unity/llms.txt': typeof UnityLlmsDottxtRoute + '/web-checkout/llms-full.txt': typeof WebCheckoutLlmsFullDottxtRoute + '/web-checkout/llms.txt': typeof WebCheckoutLlmsDottxtRoute '/sdk/': typeof SdkIndexRoute '/api/raw/$': typeof ApiRawSplatRoute '/llms.mdx/docs/$': typeof LlmsDotmdxDocsSplatRoute @@ -212,12 +377,30 @@ export interface FileRouteTypes { | '/sitemap.xml' | '/{$}.md' | '/{$}.mdx' - | '/$section/llms-full.txt' - | '/$section/llms.txt' + | '/agents/llms-full.txt' + | '/agents/llms.txt' + | '/android/llms-full.txt' + | '/android/llms.txt' | '/api/feedback' | '/api/search' + | '/dashboard/llms-full.txt' + | '/dashboard/llms.txt' + | '/expo/llms-full.txt' + | '/expo/llms.txt' + | '/flutter/llms-full.txt' + | '/flutter/llms.txt' + | '/integrations/llms-full.txt' + | '/integrations/llms.txt' + | '/ios/llms-full.txt' + | '/ios/llms.txt' | '/llms.mdx/docs' + | '/react-native/llms-full.txt' + | '/react-native/llms.txt' | '/sdk/$' + | '/unity/llms-full.txt' + | '/unity/llms.txt' + | '/web-checkout/llms-full.txt' + | '/web-checkout/llms.txt' | '/sdk/' | '/api/raw/$' | '/llms.mdx/docs/$' @@ -234,11 +417,29 @@ export interface FileRouteTypes { | '/sitemap.xml' | '/{$}.md' | '/{$}.mdx' - | '/$section/llms-full.txt' - | '/$section/llms.txt' + | '/agents/llms-full.txt' + | '/agents/llms.txt' + | '/android/llms-full.txt' + | '/android/llms.txt' | '/api/feedback' | '/api/search' + | '/dashboard/llms-full.txt' + | '/dashboard/llms.txt' + | '/expo/llms-full.txt' + | '/expo/llms.txt' + | '/flutter/llms-full.txt' + | '/flutter/llms.txt' + | '/integrations/llms-full.txt' + | '/integrations/llms.txt' + | '/ios/llms-full.txt' + | '/ios/llms.txt' + | '/react-native/llms-full.txt' + | '/react-native/llms.txt' | '/sdk/$' + | '/unity/llms-full.txt' + | '/unity/llms.txt' + | '/web-checkout/llms-full.txt' + | '/web-checkout/llms.txt' | '/sdk' | '/api/raw/$' | '/llms.mdx/docs/$' @@ -255,12 +456,30 @@ export interface FileRouteTypes { | '/sitemap.xml' | '/{$}.md' | '/{$}.mdx' - | '/$section/llms-full.txt' - | '/$section/llms.txt' + | '/agents/llms-full.txt' + | '/agents/llms.txt' + | '/android/llms-full.txt' + | '/android/llms.txt' | '/api/feedback' | '/api/search' + | '/dashboard/llms-full.txt' + | '/dashboard/llms.txt' + | '/expo/llms-full.txt' + | '/expo/llms.txt' + | '/flutter/llms-full.txt' + | '/flutter/llms.txt' + | '/integrations/llms-full.txt' + | '/integrations/llms.txt' + | '/ios/llms-full.txt' + | '/ios/llms.txt' | '/llms.mdx/docs' + | '/react-native/llms-full.txt' + | '/react-native/llms.txt' | '/sdk/$' + | '/unity/llms-full.txt' + | '/unity/llms.txt' + | '/web-checkout/llms-full.txt' + | '/web-checkout/llms.txt' | '/sdk/' | '/api/raw/$' | '/llms.mdx/docs/$' @@ -278,12 +497,30 @@ export interface RootRouteChildren { SitemapDotxmlRoute: typeof SitemapDotxmlRoute Char123Char125DotmdRoute: typeof Char123Char125DotmdRoute Char123Char125DotmdxRoute: typeof Char123Char125DotmdxRoute - SectionLlmsFullDottxtRoute: typeof SectionLlmsFullDottxtRoute - SectionLlmsDottxtRoute: typeof SectionLlmsDottxtRoute + AgentsLlmsFullDottxtRoute: typeof AgentsLlmsFullDottxtRoute + AgentsLlmsDottxtRoute: typeof AgentsLlmsDottxtRoute + AndroidLlmsFullDottxtRoute: typeof AndroidLlmsFullDottxtRoute + AndroidLlmsDottxtRoute: typeof AndroidLlmsDottxtRoute ApiFeedbackRoute: typeof ApiFeedbackRoute ApiSearchRoute: typeof ApiSearchRoute + DashboardLlmsFullDottxtRoute: typeof DashboardLlmsFullDottxtRoute + DashboardLlmsDottxtRoute: typeof DashboardLlmsDottxtRoute + ExpoLlmsFullDottxtRoute: typeof ExpoLlmsFullDottxtRoute + ExpoLlmsDottxtRoute: typeof ExpoLlmsDottxtRoute + FlutterLlmsFullDottxtRoute: typeof FlutterLlmsFullDottxtRoute + FlutterLlmsDottxtRoute: typeof FlutterLlmsDottxtRoute + IntegrationsLlmsFullDottxtRoute: typeof IntegrationsLlmsFullDottxtRoute + IntegrationsLlmsDottxtRoute: typeof IntegrationsLlmsDottxtRoute + IosLlmsFullDottxtRoute: typeof IosLlmsFullDottxtRoute + IosLlmsDottxtRoute: typeof IosLlmsDottxtRoute LlmsDotmdxDocsRoute: typeof LlmsDotmdxDocsRouteWithChildren + ReactNativeLlmsFullDottxtRoute: typeof ReactNativeLlmsFullDottxtRoute + ReactNativeLlmsDottxtRoute: typeof ReactNativeLlmsDottxtRoute SdkSplatRoute: typeof SdkSplatRoute + UnityLlmsFullDottxtRoute: typeof UnityLlmsFullDottxtRoute + UnityLlmsDottxtRoute: typeof UnityLlmsDottxtRoute + WebCheckoutLlmsFullDottxtRoute: typeof WebCheckoutLlmsFullDottxtRoute + WebCheckoutLlmsDottxtRoute: typeof WebCheckoutLlmsDottxtRoute SdkIndexRoute: typeof SdkIndexRoute ApiRawSplatRoute: typeof ApiRawSplatRoute } @@ -367,6 +604,34 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SdkIndexRouteImport parentRoute: typeof rootRouteImport } + '/web-checkout/llms.txt': { + id: '/web-checkout/llms.txt' + path: '/web-checkout/llms.txt' + fullPath: '/web-checkout/llms.txt' + preLoaderRoute: typeof WebCheckoutLlmsDottxtRouteImport + parentRoute: typeof rootRouteImport + } + '/web-checkout/llms-full.txt': { + id: '/web-checkout/llms-full.txt' + path: '/web-checkout/llms-full.txt' + fullPath: '/web-checkout/llms-full.txt' + preLoaderRoute: typeof WebCheckoutLlmsFullDottxtRouteImport + parentRoute: typeof rootRouteImport + } + '/unity/llms.txt': { + id: '/unity/llms.txt' + path: '/unity/llms.txt' + fullPath: '/unity/llms.txt' + preLoaderRoute: typeof UnityLlmsDottxtRouteImport + parentRoute: typeof rootRouteImport + } + '/unity/llms-full.txt': { + id: '/unity/llms-full.txt' + path: '/unity/llms-full.txt' + fullPath: '/unity/llms-full.txt' + preLoaderRoute: typeof UnityLlmsFullDottxtRouteImport + parentRoute: typeof rootRouteImport + } '/sdk/$': { id: '/sdk/$' path: '/sdk/$' @@ -374,6 +639,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SdkSplatRouteImport parentRoute: typeof rootRouteImport } + '/react-native/llms.txt': { + id: '/react-native/llms.txt' + path: '/react-native/llms.txt' + fullPath: '/react-native/llms.txt' + preLoaderRoute: typeof ReactNativeLlmsDottxtRouteImport + parentRoute: typeof rootRouteImport + } + '/react-native/llms-full.txt': { + id: '/react-native/llms-full.txt' + path: '/react-native/llms-full.txt' + fullPath: '/react-native/llms-full.txt' + preLoaderRoute: typeof ReactNativeLlmsFullDottxtRouteImport + parentRoute: typeof rootRouteImport + } '/llms.mdx/docs': { id: '/llms.mdx/docs' path: '/llms.mdx/docs' @@ -381,6 +660,76 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LlmsDotmdxDocsRouteImport parentRoute: typeof rootRouteImport } + '/ios/llms.txt': { + id: '/ios/llms.txt' + path: '/ios/llms.txt' + fullPath: '/ios/llms.txt' + preLoaderRoute: typeof IosLlmsDottxtRouteImport + parentRoute: typeof rootRouteImport + } + '/ios/llms-full.txt': { + id: '/ios/llms-full.txt' + path: '/ios/llms-full.txt' + fullPath: '/ios/llms-full.txt' + preLoaderRoute: typeof IosLlmsFullDottxtRouteImport + parentRoute: typeof rootRouteImport + } + '/integrations/llms.txt': { + id: '/integrations/llms.txt' + path: '/integrations/llms.txt' + fullPath: '/integrations/llms.txt' + preLoaderRoute: typeof IntegrationsLlmsDottxtRouteImport + parentRoute: typeof rootRouteImport + } + '/integrations/llms-full.txt': { + id: '/integrations/llms-full.txt' + path: '/integrations/llms-full.txt' + fullPath: '/integrations/llms-full.txt' + preLoaderRoute: typeof IntegrationsLlmsFullDottxtRouteImport + parentRoute: typeof rootRouteImport + } + '/flutter/llms.txt': { + id: '/flutter/llms.txt' + path: '/flutter/llms.txt' + fullPath: '/flutter/llms.txt' + preLoaderRoute: typeof FlutterLlmsDottxtRouteImport + parentRoute: typeof rootRouteImport + } + '/flutter/llms-full.txt': { + id: '/flutter/llms-full.txt' + path: '/flutter/llms-full.txt' + fullPath: '/flutter/llms-full.txt' + preLoaderRoute: typeof FlutterLlmsFullDottxtRouteImport + parentRoute: typeof rootRouteImport + } + '/expo/llms.txt': { + id: '/expo/llms.txt' + path: '/expo/llms.txt' + fullPath: '/expo/llms.txt' + preLoaderRoute: typeof ExpoLlmsDottxtRouteImport + parentRoute: typeof rootRouteImport + } + '/expo/llms-full.txt': { + id: '/expo/llms-full.txt' + path: '/expo/llms-full.txt' + fullPath: '/expo/llms-full.txt' + preLoaderRoute: typeof ExpoLlmsFullDottxtRouteImport + parentRoute: typeof rootRouteImport + } + '/dashboard/llms.txt': { + id: '/dashboard/llms.txt' + path: '/dashboard/llms.txt' + fullPath: '/dashboard/llms.txt' + preLoaderRoute: typeof DashboardLlmsDottxtRouteImport + parentRoute: typeof rootRouteImport + } + '/dashboard/llms-full.txt': { + id: '/dashboard/llms-full.txt' + path: '/dashboard/llms-full.txt' + fullPath: '/dashboard/llms-full.txt' + preLoaderRoute: typeof DashboardLlmsFullDottxtRouteImport + parentRoute: typeof rootRouteImport + } '/api/search': { id: '/api/search' path: '/api/search' @@ -395,18 +744,32 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiFeedbackRouteImport parentRoute: typeof rootRouteImport } - '/$section/llms.txt': { - id: '/$section/llms.txt' - path: '/$section/llms.txt' - fullPath: '/$section/llms.txt' - preLoaderRoute: typeof SectionLlmsDottxtRouteImport + '/android/llms.txt': { + id: '/android/llms.txt' + path: '/android/llms.txt' + fullPath: '/android/llms.txt' + preLoaderRoute: typeof AndroidLlmsDottxtRouteImport + parentRoute: typeof rootRouteImport + } + '/android/llms-full.txt': { + id: '/android/llms-full.txt' + path: '/android/llms-full.txt' + fullPath: '/android/llms-full.txt' + preLoaderRoute: typeof AndroidLlmsFullDottxtRouteImport + parentRoute: typeof rootRouteImport + } + '/agents/llms.txt': { + id: '/agents/llms.txt' + path: '/agents/llms.txt' + fullPath: '/agents/llms.txt' + preLoaderRoute: typeof AgentsLlmsDottxtRouteImport parentRoute: typeof rootRouteImport } - '/$section/llms-full.txt': { - id: '/$section/llms-full.txt' - path: '/$section/llms-full.txt' - fullPath: '/$section/llms-full.txt' - preLoaderRoute: typeof SectionLlmsFullDottxtRouteImport + '/agents/llms-full.txt': { + id: '/agents/llms-full.txt' + path: '/agents/llms-full.txt' + fullPath: '/agents/llms-full.txt' + preLoaderRoute: typeof AgentsLlmsFullDottxtRouteImport parentRoute: typeof rootRouteImport } '/llms.mdx/docs/': { @@ -459,12 +822,30 @@ const rootRouteChildren: RootRouteChildren = { SitemapDotxmlRoute: SitemapDotxmlRoute, Char123Char125DotmdRoute: Char123Char125DotmdRoute, Char123Char125DotmdxRoute: Char123Char125DotmdxRoute, - SectionLlmsFullDottxtRoute: SectionLlmsFullDottxtRoute, - SectionLlmsDottxtRoute: SectionLlmsDottxtRoute, + AgentsLlmsFullDottxtRoute: AgentsLlmsFullDottxtRoute, + AgentsLlmsDottxtRoute: AgentsLlmsDottxtRoute, + AndroidLlmsFullDottxtRoute: AndroidLlmsFullDottxtRoute, + AndroidLlmsDottxtRoute: AndroidLlmsDottxtRoute, ApiFeedbackRoute: ApiFeedbackRoute, ApiSearchRoute: ApiSearchRoute, + DashboardLlmsFullDottxtRoute: DashboardLlmsFullDottxtRoute, + DashboardLlmsDottxtRoute: DashboardLlmsDottxtRoute, + ExpoLlmsFullDottxtRoute: ExpoLlmsFullDottxtRoute, + ExpoLlmsDottxtRoute: ExpoLlmsDottxtRoute, + FlutterLlmsFullDottxtRoute: FlutterLlmsFullDottxtRoute, + FlutterLlmsDottxtRoute: FlutterLlmsDottxtRoute, + IntegrationsLlmsFullDottxtRoute: IntegrationsLlmsFullDottxtRoute, + IntegrationsLlmsDottxtRoute: IntegrationsLlmsDottxtRoute, + IosLlmsFullDottxtRoute: IosLlmsFullDottxtRoute, + IosLlmsDottxtRoute: IosLlmsDottxtRoute, LlmsDotmdxDocsRoute: LlmsDotmdxDocsRouteWithChildren, + ReactNativeLlmsFullDottxtRoute: ReactNativeLlmsFullDottxtRoute, + ReactNativeLlmsDottxtRoute: ReactNativeLlmsDottxtRoute, SdkSplatRoute: SdkSplatRoute, + UnityLlmsFullDottxtRoute: UnityLlmsFullDottxtRoute, + UnityLlmsDottxtRoute: UnityLlmsDottxtRoute, + WebCheckoutLlmsFullDottxtRoute: WebCheckoutLlmsFullDottxtRoute, + WebCheckoutLlmsDottxtRoute: WebCheckoutLlmsDottxtRoute, SdkIndexRoute: SdkIndexRoute, ApiRawSplatRoute: ApiRawSplatRoute, } diff --git a/src/routes/$section/llms-full[.]txt.ts b/src/routes/$section/llms-full[.]txt.ts deleted file mode 100644 index 3034c25..0000000 --- a/src/routes/$section/llms-full[.]txt.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { buildLLMFullText, buildLLMResponse, getLLMSectionConfig } from "@/lib/llms"; -import { createFileRoute } from "@tanstack/react-router"; - -export const Route = createFileRoute("/$section/llms-full.txt")({ - server: { - handlers: { - GET: async ({ params }) => { - if (!getLLMSectionConfig(params.section)) { - return new Response("LLMS section not found", { status: 404 }); - } - return buildLLMResponse(await buildLLMFullText(params.section)); - }, - }, - }, -}); diff --git a/src/routes/$section/llms[.]txt.ts b/src/routes/$section/llms[.]txt.ts deleted file mode 100644 index e837927..0000000 --- a/src/routes/$section/llms[.]txt.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { buildLLMResponse, buildLLMSummaryText, getLLMSectionConfig } from "@/lib/llms"; -import { createFileRoute } from "@tanstack/react-router"; - -export const Route = createFileRoute("/$section/llms.txt")({ - server: { - handlers: { - GET: async ({ params }) => { - if (!getLLMSectionConfig(params.section)) { - return new Response("LLMS section not found", { status: 404 }); - } - return buildLLMResponse(await buildLLMSummaryText(params.section)); - }, - }, - }, -}); diff --git a/src/routes/agents/llms-full[.]txt.ts b/src/routes/agents/llms-full[.]txt.ts new file mode 100644 index 0000000..d3eda04 --- /dev/null +++ b/src/routes/agents/llms-full[.]txt.ts @@ -0,0 +1,10 @@ +import { buildLLMFullResponseForSection } from "@/lib/llms"; +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/agents/llms-full.txt")({ + server: { + handlers: { + GET: () => buildLLMFullResponseForSection("agents"), + }, + }, +}); diff --git a/src/routes/agents/llms[.]txt.ts b/src/routes/agents/llms[.]txt.ts new file mode 100644 index 0000000..dc34f91 --- /dev/null +++ b/src/routes/agents/llms[.]txt.ts @@ -0,0 +1,10 @@ +import { buildLLMSummaryResponseForSection } from "@/lib/llms"; +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/agents/llms.txt")({ + server: { + handlers: { + GET: () => buildLLMSummaryResponseForSection("agents"), + }, + }, +}); diff --git a/src/routes/android/llms-full[.]txt.ts b/src/routes/android/llms-full[.]txt.ts new file mode 100644 index 0000000..5ecbba7 --- /dev/null +++ b/src/routes/android/llms-full[.]txt.ts @@ -0,0 +1,10 @@ +import { buildLLMFullResponseForSection } from "@/lib/llms"; +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/android/llms-full.txt")({ + server: { + handlers: { + GET: () => buildLLMFullResponseForSection("android"), + }, + }, +}); diff --git a/src/routes/android/llms[.]txt.ts b/src/routes/android/llms[.]txt.ts new file mode 100644 index 0000000..190f875 --- /dev/null +++ b/src/routes/android/llms[.]txt.ts @@ -0,0 +1,10 @@ +import { buildLLMSummaryResponseForSection } from "@/lib/llms"; +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/android/llms.txt")({ + server: { + handlers: { + GET: () => buildLLMSummaryResponseForSection("android"), + }, + }, +}); diff --git a/src/routes/dashboard/llms-full[.]txt.ts b/src/routes/dashboard/llms-full[.]txt.ts new file mode 100644 index 0000000..c9c8f4d --- /dev/null +++ b/src/routes/dashboard/llms-full[.]txt.ts @@ -0,0 +1,10 @@ +import { buildLLMFullResponseForSection } from "@/lib/llms"; +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/dashboard/llms-full.txt")({ + server: { + handlers: { + GET: () => buildLLMFullResponseForSection("dashboard"), + }, + }, +}); diff --git a/src/routes/dashboard/llms[.]txt.ts b/src/routes/dashboard/llms[.]txt.ts new file mode 100644 index 0000000..c46cec2 --- /dev/null +++ b/src/routes/dashboard/llms[.]txt.ts @@ -0,0 +1,10 @@ +import { buildLLMSummaryResponseForSection } from "@/lib/llms"; +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/dashboard/llms.txt")({ + server: { + handlers: { + GET: () => buildLLMSummaryResponseForSection("dashboard"), + }, + }, +}); diff --git a/src/routes/expo/llms-full[.]txt.ts b/src/routes/expo/llms-full[.]txt.ts new file mode 100644 index 0000000..6141482 --- /dev/null +++ b/src/routes/expo/llms-full[.]txt.ts @@ -0,0 +1,10 @@ +import { buildLLMFullResponseForSection } from "@/lib/llms"; +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/expo/llms-full.txt")({ + server: { + handlers: { + GET: () => buildLLMFullResponseForSection("expo"), + }, + }, +}); diff --git a/src/routes/expo/llms[.]txt.ts b/src/routes/expo/llms[.]txt.ts new file mode 100644 index 0000000..708023d --- /dev/null +++ b/src/routes/expo/llms[.]txt.ts @@ -0,0 +1,10 @@ +import { buildLLMSummaryResponseForSection } from "@/lib/llms"; +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/expo/llms.txt")({ + server: { + handlers: { + GET: () => buildLLMSummaryResponseForSection("expo"), + }, + }, +}); diff --git a/src/routes/flutter/llms-full[.]txt.ts b/src/routes/flutter/llms-full[.]txt.ts new file mode 100644 index 0000000..a1278da --- /dev/null +++ b/src/routes/flutter/llms-full[.]txt.ts @@ -0,0 +1,10 @@ +import { buildLLMFullResponseForSection } from "@/lib/llms"; +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/flutter/llms-full.txt")({ + server: { + handlers: { + GET: () => buildLLMFullResponseForSection("flutter"), + }, + }, +}); diff --git a/src/routes/flutter/llms[.]txt.ts b/src/routes/flutter/llms[.]txt.ts new file mode 100644 index 0000000..af17df5 --- /dev/null +++ b/src/routes/flutter/llms[.]txt.ts @@ -0,0 +1,10 @@ +import { buildLLMSummaryResponseForSection } from "@/lib/llms"; +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/flutter/llms.txt")({ + server: { + handlers: { + GET: () => buildLLMSummaryResponseForSection("flutter"), + }, + }, +}); diff --git a/src/routes/integrations/llms-full[.]txt.ts b/src/routes/integrations/llms-full[.]txt.ts new file mode 100644 index 0000000..0f7afea --- /dev/null +++ b/src/routes/integrations/llms-full[.]txt.ts @@ -0,0 +1,10 @@ +import { buildLLMFullResponseForSection } from "@/lib/llms"; +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/integrations/llms-full.txt")({ + server: { + handlers: { + GET: () => buildLLMFullResponseForSection("integrations"), + }, + }, +}); diff --git a/src/routes/integrations/llms[.]txt.ts b/src/routes/integrations/llms[.]txt.ts new file mode 100644 index 0000000..de2db6c --- /dev/null +++ b/src/routes/integrations/llms[.]txt.ts @@ -0,0 +1,10 @@ +import { buildLLMSummaryResponseForSection } from "@/lib/llms"; +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/integrations/llms.txt")({ + server: { + handlers: { + GET: () => buildLLMSummaryResponseForSection("integrations"), + }, + }, +}); diff --git a/src/routes/ios/llms-full[.]txt.ts b/src/routes/ios/llms-full[.]txt.ts new file mode 100644 index 0000000..2280d9a --- /dev/null +++ b/src/routes/ios/llms-full[.]txt.ts @@ -0,0 +1,10 @@ +import { buildLLMFullResponseForSection } from "@/lib/llms"; +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/ios/llms-full.txt")({ + server: { + handlers: { + GET: () => buildLLMFullResponseForSection("ios"), + }, + }, +}); diff --git a/src/routes/ios/llms[.]txt.ts b/src/routes/ios/llms[.]txt.ts new file mode 100644 index 0000000..207a84c --- /dev/null +++ b/src/routes/ios/llms[.]txt.ts @@ -0,0 +1,10 @@ +import { buildLLMSummaryResponseForSection } from "@/lib/llms"; +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/ios/llms.txt")({ + server: { + handlers: { + GET: () => buildLLMSummaryResponseForSection("ios"), + }, + }, +}); diff --git a/src/routes/react-native/llms-full[.]txt.ts b/src/routes/react-native/llms-full[.]txt.ts new file mode 100644 index 0000000..5d49710 --- /dev/null +++ b/src/routes/react-native/llms-full[.]txt.ts @@ -0,0 +1,10 @@ +import { buildLLMFullResponseForSection } from "@/lib/llms"; +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/react-native/llms-full.txt")({ + server: { + handlers: { + GET: () => buildLLMFullResponseForSection("react-native"), + }, + }, +}); diff --git a/src/routes/react-native/llms[.]txt.ts b/src/routes/react-native/llms[.]txt.ts new file mode 100644 index 0000000..bff403f --- /dev/null +++ b/src/routes/react-native/llms[.]txt.ts @@ -0,0 +1,10 @@ +import { buildLLMSummaryResponseForSection } from "@/lib/llms"; +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/react-native/llms.txt")({ + server: { + handlers: { + GET: () => buildLLMSummaryResponseForSection("react-native"), + }, + }, +}); diff --git a/src/routes/unity/llms-full[.]txt.ts b/src/routes/unity/llms-full[.]txt.ts new file mode 100644 index 0000000..031ea03 --- /dev/null +++ b/src/routes/unity/llms-full[.]txt.ts @@ -0,0 +1,10 @@ +import { buildLLMFullResponseForSection } from "@/lib/llms"; +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/unity/llms-full.txt")({ + server: { + handlers: { + GET: () => buildLLMFullResponseForSection("unity"), + }, + }, +}); diff --git a/src/routes/unity/llms[.]txt.ts b/src/routes/unity/llms[.]txt.ts new file mode 100644 index 0000000..430769f --- /dev/null +++ b/src/routes/unity/llms[.]txt.ts @@ -0,0 +1,10 @@ +import { buildLLMSummaryResponseForSection } from "@/lib/llms"; +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/unity/llms.txt")({ + server: { + handlers: { + GET: () => buildLLMSummaryResponseForSection("unity"), + }, + }, +}); diff --git a/src/routes/web-checkout/llms-full[.]txt.ts b/src/routes/web-checkout/llms-full[.]txt.ts new file mode 100644 index 0000000..e332066 --- /dev/null +++ b/src/routes/web-checkout/llms-full[.]txt.ts @@ -0,0 +1,10 @@ +import { buildLLMFullResponseForSection } from "@/lib/llms"; +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/web-checkout/llms-full.txt")({ + server: { + handlers: { + GET: () => buildLLMFullResponseForSection("web-checkout"), + }, + }, +}); diff --git a/src/routes/web-checkout/llms[.]txt.ts b/src/routes/web-checkout/llms[.]txt.ts new file mode 100644 index 0000000..c4f0c73 --- /dev/null +++ b/src/routes/web-checkout/llms[.]txt.ts @@ -0,0 +1,10 @@ +import { buildLLMSummaryResponseForSection } from "@/lib/llms"; +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/web-checkout/llms.txt")({ + server: { + handlers: { + GET: () => buildLLMSummaryResponseForSection("web-checkout"), + }, + }, +}); From da4046622d0db79cfe26448ceae68bc8aa697a31 Mon Sep 17 00:00:00 2001 From: Duncan Crawbuck Date: Thu, 11 Jun 2026 10:42:01 -0700 Subject: [PATCH 7/9] Avoid markdown alternates on docs 404s --- src/start.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/start.ts b/src/start.ts index 6dd6599..1e84cc1 100644 --- a/src/start.ts +++ b/src/start.ts @@ -110,7 +110,7 @@ function withDocsNegotiationHeaders(response: Response, pathname: string): Respo appendVaryAccept(headers); const contentType = headers.get("Content-Type"); - if (contentType?.toLowerCase().startsWith("text/html")) { + if (response.ok && contentType?.toLowerCase().startsWith("text/html")) { appendHeaderValue(headers, "Link", buildMarkdownAlternateLinkHeader(pathname)); } From 5cff604ed67d9e43c8539ff899704a2c69e44237 Mon Sep 17 00:00:00 2001 From: Duncan Crawbuck Date: Thu, 11 Jun 2026 13:56:42 -0700 Subject: [PATCH 8/9] Add SDK selector markdown routes --- plugins/remark-markdown-image-map.test.ts | 18 +++++ plugins/remark-markdown-image-map.ts | 21 +++--- src/lib/markdown-route.ts | 3 + src/lib/sdk-selector-markdown.test.ts | 37 ++++++++++ src/lib/sdk-selector-markdown.ts | 23 +++++++ src/routeTree.gen.ts | 84 +++++++++++++++++++++++ src/routes/sdk/{$}[.]md.ts | 25 +++++++ src/routes/sdk/{$}[.]mdx.ts | 25 +++++++ src/routes/sdk[.]md.ts | 17 +++++ src/routes/sdk[.]mdx.ts | 17 +++++ 10 files changed, 261 insertions(+), 9 deletions(-) create mode 100644 src/lib/sdk-selector-markdown.test.ts create mode 100644 src/lib/sdk-selector-markdown.ts create mode 100644 src/routes/sdk/{$}[.]md.ts create mode 100644 src/routes/sdk/{$}[.]mdx.ts create mode 100644 src/routes/sdk[.]md.ts create mode 100644 src/routes/sdk[.]mdx.ts diff --git a/plugins/remark-markdown-image-map.test.ts b/plugins/remark-markdown-image-map.test.ts index 67a0825..dd3daff 100644 --- a/plugins/remark-markdown-image-map.test.ts +++ b/plugins/remark-markdown-image-map.test.ts @@ -36,4 +36,22 @@ describe("remarkMarkdownImageMap", () => { }, ]); }); + + test("includes JSX image sources without shifting markdown image placeholders", async () => { + const data = await collectMarkdownImageMap(` +![](/images/first.png) + + + Frame screenshot + + +![](/images/third.png) +`); + + assert.deepEqual(data.markdownImageMap, { + __img0: "/docs/images/first.png", + __img1: "/docs/images/third.png", + __img2: "/docs/images/frame.png", + }); + }); }); diff --git a/plugins/remark-markdown-image-map.ts b/plugins/remark-markdown-image-map.ts index 5f4fec7..3111ce1 100644 --- a/plugins/remark-markdown-image-map.ts +++ b/plugins/remark-markdown-image-map.ts @@ -13,11 +13,6 @@ function getAttributeValue(node: any, name: string): string | undefined { } } -function getImagePlaceholder(node: any): string | undefined { - const src = getAttributeValue(node, "src"); - return src && imagePlaceholderPattern.test(src) ? src : undefined; -} - function getImageUrl(node: any): string | undefined { if (typeof node.url === "string") return normalizeDocsAssetPath(node.url); @@ -31,22 +26,30 @@ export default function remarkMarkdownImageMap() { return (tree: any, file: any) => { const imageMap: Record = {}; let imageIndex = 0; + const jsxImageUrls: string[] = []; visit(tree, "image", (node: any) => { - if (typeof node.url !== "string") return; + const url = getImageUrl(node); + if (!url) return; - imageMap[`__img${imageIndex}`] = normalizeDocsAssetPath(node.url); + imageMap[`__img${imageIndex}`] = url; imageIndex += 1; }); visit(tree, ["mdxJsxFlowElement", "mdxJsxTextElement"], (node: any) => { if (node.name !== "img") return; - const placeholder = getImagePlaceholder(node); const url = getImageUrl(node); - if (placeholder && url) imageMap[placeholder] = url; + if (!url) return; + + jsxImageUrls.push(url); }); + for (const url of jsxImageUrls) { + imageMap[`__img${imageIndex}`] = url; + imageIndex += 1; + } + if (Object.keys(imageMap).length > 0) { file.data.markdownImageMap = imageMap; file.data["mdx-export"] ??= []; diff --git a/src/lib/markdown-route.ts b/src/lib/markdown-route.ts index 0056837..15d4529 100644 --- a/src/lib/markdown-route.ts +++ b/src/lib/markdown-route.ts @@ -1,5 +1,6 @@ import { buildHtmlPathFromMarkdownRoute } from "./markdown-alternate"; import { buildLLMSummaryText } from "./llms"; +import { buildSdkSelectorMarkdown } from "./sdk-selector-markdown"; import { getPageMarkdownText, source } from "./source"; export function slugsFromMarkdownSplat(splat?: string) { @@ -7,6 +8,8 @@ export function slugsFromMarkdownSplat(splat?: string) { } export async function getMarkdownForPageSlugs(slugs: string[], request: Request) { + if (slugs[0] === "sdk") return buildSdkSelectorMarkdown(slugs); + const page = source.getPage(slugs); if (!page) return undefined; diff --git a/src/lib/sdk-selector-markdown.test.ts b/src/lib/sdk-selector-markdown.test.ts new file mode 100644 index 0000000..7559b2f --- /dev/null +++ b/src/lib/sdk-selector-markdown.test.ts @@ -0,0 +1,37 @@ +import assert from "node:assert/strict"; +import { describe, test } from "node:test"; +import { buildSdkSelectorMarkdown } from "./sdk-selector-markdown"; + +describe("buildSdkSelectorMarkdown", () => { + test("renders sdk selector markdown with platform-specific markdown links", () => { + assert.equal( + buildSdkSelectorMarkdown(["sdk"]), + [ + "# Choose your SDK", + "", + "SDK docs are shared across platforms. Pick your SDK to continue.", + "", + "- [iOS](/docs/ios.md)", + "- [Android](/docs/android.md)", + "- [Flutter](/docs/flutter.md)", + "- [Expo](/docs/expo.md)", + ].join("\n"), + ); + }); + + test("renders sdk placeholder subpaths as concrete platform markdown links", () => { + assert.equal( + buildSdkSelectorMarkdown(["sdk", "guides", "advanced", "game-controller-support"]), + [ + "# Choose your SDK", + "", + "SDK docs are shared across platforms. Pick your SDK to open this page for the platform you use.", + "", + "- [iOS](/docs/ios/guides/advanced/game-controller-support.md)", + "- [Android](/docs/android/guides/advanced/game-controller-support.md)", + "- [Flutter](/docs/flutter/guides/advanced/game-controller-support.md)", + "- [Expo](/docs/expo/guides/advanced/game-controller-support.md)", + ].join("\n"), + ); + }); +}); diff --git a/src/lib/sdk-selector-markdown.ts b/src/lib/sdk-selector-markdown.ts new file mode 100644 index 0000000..6d4c104 --- /dev/null +++ b/src/lib/sdk-selector-markdown.ts @@ -0,0 +1,23 @@ +import { buildMarkdownAlternatePath } from "./markdown-alternate"; +import { replaceSdkPlaceholder, SDK_SELECTOR_SLUGS, type SdkSlug } from "./sdk-navigation"; + +const sdkLabels: Record<(typeof SDK_SELECTOR_SLUGS)[number], string> = { + ios: "iOS", + android: "Android", + flutter: "Flutter", + expo: "Expo", +}; + +export function buildSdkSelectorMarkdown(slugs: string[]) { + const requestedPath = `/docs/${slugs.join("/")}`; + const description = + slugs.length === 1 + ? "SDK docs are shared across platforms. Pick your SDK to continue." + : "SDK docs are shared across platforms. Pick your SDK to open this page for the platform you use."; + const links = SDK_SELECTOR_SLUGS.map((sdk) => { + const pagePath = replaceSdkPlaceholder(requestedPath, sdk as SdkSlug); + return `- [${sdkLabels[sdk]}](${buildMarkdownAlternatePath(pagePath)})`; + }); + + return ["# Choose your SDK", "", description, "", ...links].join("\n"); +} diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index a064b4f..a8e45ad 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -12,6 +12,8 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as Char123Char125DotmdxRouteImport } from './routes/{$}[.]mdx' import { Route as Char123Char125DotmdRouteImport } from './routes/{$}[.]md' import { Route as SitemapDotxmlRouteImport } from './routes/sitemap[.]xml' +import { Route as SdkDotmdxRouteImport } from './routes/sdk[.]mdx' +import { Route as SdkDotmdRouteImport } from './routes/sdk[.]md' import { Route as RobotsDottxtRouteImport } from './routes/robots[.]txt' import { Route as LlmsDottxtRouteImport } from './routes/llms[.]txt' import { Route as LlmsChar123sectionChar125DottxtRouteImport } from './routes/llms-{$section}[.]txt' @@ -24,6 +26,8 @@ import { Route as WebCheckoutLlmsDottxtRouteImport } from './routes/web-checkout import { Route as WebCheckoutLlmsFullDottxtRouteImport } from './routes/web-checkout/llms-full[.]txt' import { Route as UnityLlmsDottxtRouteImport } from './routes/unity/llms[.]txt' import { Route as UnityLlmsFullDottxtRouteImport } from './routes/unity/llms-full[.]txt' +import { Route as SdkChar123Char125DotmdxRouteImport } from './routes/sdk/{$}[.]mdx' +import { Route as SdkChar123Char125DotmdRouteImport } from './routes/sdk/{$}[.]md' import { Route as SdkSplatRouteImport } from './routes/sdk/$' import { Route as ReactNativeLlmsDottxtRouteImport } from './routes/react-native/llms[.]txt' import { Route as ReactNativeLlmsFullDottxtRouteImport } from './routes/react-native/llms-full[.]txt' @@ -63,6 +67,16 @@ const SitemapDotxmlRoute = SitemapDotxmlRouteImport.update({ path: '/sitemap.xml', getParentRoute: () => rootRouteImport, } as any) +const SdkDotmdxRoute = SdkDotmdxRouteImport.update({ + id: '/sdk.mdx', + path: '/sdk.mdx', + getParentRoute: () => rootRouteImport, +} as any) +const SdkDotmdRoute = SdkDotmdRouteImport.update({ + id: '/sdk.md', + path: '/sdk.md', + getParentRoute: () => rootRouteImport, +} as any) const RobotsDottxtRoute = RobotsDottxtRouteImport.update({ id: '/robots.txt', path: '/robots.txt', @@ -126,6 +140,16 @@ const UnityLlmsFullDottxtRoute = UnityLlmsFullDottxtRouteImport.update({ path: '/unity/llms-full.txt', getParentRoute: () => rootRouteImport, } as any) +const SdkChar123Char125DotmdxRoute = SdkChar123Char125DotmdxRouteImport.update({ + id: '/sdk/{$}.mdx', + path: '/sdk/{$}.mdx', + getParentRoute: () => rootRouteImport, +} as any) +const SdkChar123Char125DotmdRoute = SdkChar123Char125DotmdRouteImport.update({ + id: '/sdk/{$}.md', + path: '/sdk/{$}.md', + getParentRoute: () => rootRouteImport, +} as any) const SdkSplatRoute = SdkSplatRouteImport.update({ id: '/sdk/$', path: '/sdk/$', @@ -252,6 +276,8 @@ export interface FileRoutesByFullPath { '/llms-{$section}.txt': typeof LlmsChar123sectionChar125DottxtRoute '/llms.txt': typeof LlmsDottxtRoute '/robots.txt': typeof RobotsDottxtRoute + '/sdk.md': typeof SdkDotmdRoute + '/sdk.mdx': typeof SdkDotmdxRoute '/sitemap.xml': typeof SitemapDotxmlRoute '/{$}.md': typeof Char123Char125DotmdRoute '/{$}.mdx': typeof Char123Char125DotmdxRoute @@ -275,6 +301,8 @@ export interface FileRoutesByFullPath { '/react-native/llms-full.txt': typeof ReactNativeLlmsFullDottxtRoute '/react-native/llms.txt': typeof ReactNativeLlmsDottxtRoute '/sdk/$': typeof SdkSplatRoute + '/sdk/{$}.md': typeof SdkChar123Char125DotmdRoute + '/sdk/{$}.mdx': typeof SdkChar123Char125DotmdxRoute '/unity/llms-full.txt': typeof UnityLlmsFullDottxtRoute '/unity/llms.txt': typeof UnityLlmsDottxtRoute '/web-checkout/llms-full.txt': typeof WebCheckoutLlmsFullDottxtRoute @@ -292,6 +320,8 @@ export interface FileRoutesByTo { '/llms-{$section}.txt': typeof LlmsChar123sectionChar125DottxtRoute '/llms.txt': typeof LlmsDottxtRoute '/robots.txt': typeof RobotsDottxtRoute + '/sdk.md': typeof SdkDotmdRoute + '/sdk.mdx': typeof SdkDotmdxRoute '/sitemap.xml': typeof SitemapDotxmlRoute '/{$}.md': typeof Char123Char125DotmdRoute '/{$}.mdx': typeof Char123Char125DotmdxRoute @@ -314,6 +344,8 @@ export interface FileRoutesByTo { '/react-native/llms-full.txt': typeof ReactNativeLlmsFullDottxtRoute '/react-native/llms.txt': typeof ReactNativeLlmsDottxtRoute '/sdk/$': typeof SdkSplatRoute + '/sdk/{$}.md': typeof SdkChar123Char125DotmdRoute + '/sdk/{$}.mdx': typeof SdkChar123Char125DotmdxRoute '/unity/llms-full.txt': typeof UnityLlmsFullDottxtRoute '/unity/llms.txt': typeof UnityLlmsDottxtRoute '/web-checkout/llms-full.txt': typeof WebCheckoutLlmsFullDottxtRoute @@ -332,6 +364,8 @@ export interface FileRoutesById { '/llms-{$section}.txt': typeof LlmsChar123sectionChar125DottxtRoute '/llms.txt': typeof LlmsDottxtRoute '/robots.txt': typeof RobotsDottxtRoute + '/sdk.md': typeof SdkDotmdRoute + '/sdk.mdx': typeof SdkDotmdxRoute '/sitemap.xml': typeof SitemapDotxmlRoute '/{$}.md': typeof Char123Char125DotmdRoute '/{$}.mdx': typeof Char123Char125DotmdxRoute @@ -355,6 +389,8 @@ export interface FileRoutesById { '/react-native/llms-full.txt': typeof ReactNativeLlmsFullDottxtRoute '/react-native/llms.txt': typeof ReactNativeLlmsDottxtRoute '/sdk/$': typeof SdkSplatRoute + '/sdk/{$}.md': typeof SdkChar123Char125DotmdRoute + '/sdk/{$}.mdx': typeof SdkChar123Char125DotmdxRoute '/unity/llms-full.txt': typeof UnityLlmsFullDottxtRoute '/unity/llms.txt': typeof UnityLlmsDottxtRoute '/web-checkout/llms-full.txt': typeof WebCheckoutLlmsFullDottxtRoute @@ -374,6 +410,8 @@ export interface FileRouteTypes { | '/llms-{$section}.txt' | '/llms.txt' | '/robots.txt' + | '/sdk.md' + | '/sdk.mdx' | '/sitemap.xml' | '/{$}.md' | '/{$}.mdx' @@ -397,6 +435,8 @@ export interface FileRouteTypes { | '/react-native/llms-full.txt' | '/react-native/llms.txt' | '/sdk/$' + | '/sdk/{$}.md' + | '/sdk/{$}.mdx' | '/unity/llms-full.txt' | '/unity/llms.txt' | '/web-checkout/llms-full.txt' @@ -414,6 +454,8 @@ export interface FileRouteTypes { | '/llms-{$section}.txt' | '/llms.txt' | '/robots.txt' + | '/sdk.md' + | '/sdk.mdx' | '/sitemap.xml' | '/{$}.md' | '/{$}.mdx' @@ -436,6 +478,8 @@ export interface FileRouteTypes { | '/react-native/llms-full.txt' | '/react-native/llms.txt' | '/sdk/$' + | '/sdk/{$}.md' + | '/sdk/{$}.mdx' | '/unity/llms-full.txt' | '/unity/llms.txt' | '/web-checkout/llms-full.txt' @@ -453,6 +497,8 @@ export interface FileRouteTypes { | '/llms-{$section}.txt' | '/llms.txt' | '/robots.txt' + | '/sdk.md' + | '/sdk.mdx' | '/sitemap.xml' | '/{$}.md' | '/{$}.mdx' @@ -476,6 +522,8 @@ export interface FileRouteTypes { | '/react-native/llms-full.txt' | '/react-native/llms.txt' | '/sdk/$' + | '/sdk/{$}.md' + | '/sdk/{$}.mdx' | '/unity/llms-full.txt' | '/unity/llms.txt' | '/web-checkout/llms-full.txt' @@ -494,6 +542,8 @@ export interface RootRouteChildren { LlmsChar123sectionChar125DottxtRoute: typeof LlmsChar123sectionChar125DottxtRoute LlmsDottxtRoute: typeof LlmsDottxtRoute RobotsDottxtRoute: typeof RobotsDottxtRoute + SdkDotmdRoute: typeof SdkDotmdRoute + SdkDotmdxRoute: typeof SdkDotmdxRoute SitemapDotxmlRoute: typeof SitemapDotxmlRoute Char123Char125DotmdRoute: typeof Char123Char125DotmdRoute Char123Char125DotmdxRoute: typeof Char123Char125DotmdxRoute @@ -517,6 +567,8 @@ export interface RootRouteChildren { ReactNativeLlmsFullDottxtRoute: typeof ReactNativeLlmsFullDottxtRoute ReactNativeLlmsDottxtRoute: typeof ReactNativeLlmsDottxtRoute SdkSplatRoute: typeof SdkSplatRoute + SdkChar123Char125DotmdRoute: typeof SdkChar123Char125DotmdRoute + SdkChar123Char125DotmdxRoute: typeof SdkChar123Char125DotmdxRoute UnityLlmsFullDottxtRoute: typeof UnityLlmsFullDottxtRoute UnityLlmsDottxtRoute: typeof UnityLlmsDottxtRoute WebCheckoutLlmsFullDottxtRoute: typeof WebCheckoutLlmsFullDottxtRoute @@ -548,6 +600,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SitemapDotxmlRouteImport parentRoute: typeof rootRouteImport } + '/sdk.mdx': { + id: '/sdk.mdx' + path: '/sdk.mdx' + fullPath: '/sdk.mdx' + preLoaderRoute: typeof SdkDotmdxRouteImport + parentRoute: typeof rootRouteImport + } + '/sdk.md': { + id: '/sdk.md' + path: '/sdk.md' + fullPath: '/sdk.md' + preLoaderRoute: typeof SdkDotmdRouteImport + parentRoute: typeof rootRouteImport + } '/robots.txt': { id: '/robots.txt' path: '/robots.txt' @@ -632,6 +698,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof UnityLlmsFullDottxtRouteImport parentRoute: typeof rootRouteImport } + '/sdk/{$}.mdx': { + id: '/sdk/{$}.mdx' + path: '/sdk/{$}.mdx' + fullPath: '/sdk/{$}.mdx' + preLoaderRoute: typeof SdkChar123Char125DotmdxRouteImport + parentRoute: typeof rootRouteImport + } + '/sdk/{$}.md': { + id: '/sdk/{$}.md' + path: '/sdk/{$}.md' + fullPath: '/sdk/{$}.md' + preLoaderRoute: typeof SdkChar123Char125DotmdRouteImport + parentRoute: typeof rootRouteImport + } '/sdk/$': { id: '/sdk/$' path: '/sdk/$' @@ -819,6 +899,8 @@ const rootRouteChildren: RootRouteChildren = { LlmsChar123sectionChar125DottxtRoute: LlmsChar123sectionChar125DottxtRoute, LlmsDottxtRoute: LlmsDottxtRoute, RobotsDottxtRoute: RobotsDottxtRoute, + SdkDotmdRoute: SdkDotmdRoute, + SdkDotmdxRoute: SdkDotmdxRoute, SitemapDotxmlRoute: SitemapDotxmlRoute, Char123Char125DotmdRoute: Char123Char125DotmdRoute, Char123Char125DotmdxRoute: Char123Char125DotmdxRoute, @@ -842,6 +924,8 @@ const rootRouteChildren: RootRouteChildren = { ReactNativeLlmsFullDottxtRoute: ReactNativeLlmsFullDottxtRoute, ReactNativeLlmsDottxtRoute: ReactNativeLlmsDottxtRoute, SdkSplatRoute: SdkSplatRoute, + SdkChar123Char125DotmdRoute: SdkChar123Char125DotmdRoute, + SdkChar123Char125DotmdxRoute: SdkChar123Char125DotmdxRoute, UnityLlmsFullDottxtRoute: UnityLlmsFullDottxtRoute, UnityLlmsDottxtRoute: UnityLlmsDottxtRoute, WebCheckoutLlmsFullDottxtRoute: WebCheckoutLlmsFullDottxtRoute, diff --git a/src/routes/sdk/{$}[.]md.ts b/src/routes/sdk/{$}[.]md.ts new file mode 100644 index 0000000..feb7e76 --- /dev/null +++ b/src/routes/sdk/{$}[.]md.ts @@ -0,0 +1,25 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { + buildMarkdownRouteResponse, + getMarkdownForPageSlugs, + slugsFromMarkdownSplat, +} from "@/lib/markdown-route"; + +async function getSdkMarkdownResponse( + params: { _splat?: string }, + request: Request, + includeBody = true, +) { + const slugs = ["sdk", ...slugsFromMarkdownSplat(params._splat)]; + const body = await getMarkdownForPageSlugs(slugs, request); + return buildMarkdownRouteResponse(includeBody ? body! : null, slugs); +} + +export const Route = createFileRoute("/sdk/{$}.md")({ + server: { + handlers: { + GET: async ({ params, request }) => getSdkMarkdownResponse(params, request), + HEAD: async ({ params, request }) => getSdkMarkdownResponse(params, request, false), + }, + }, +}); diff --git a/src/routes/sdk/{$}[.]mdx.ts b/src/routes/sdk/{$}[.]mdx.ts new file mode 100644 index 0000000..cc1d630 --- /dev/null +++ b/src/routes/sdk/{$}[.]mdx.ts @@ -0,0 +1,25 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { + buildMarkdownRouteResponse, + getMarkdownForPageSlugs, + slugsFromMarkdownSplat, +} from "@/lib/markdown-route"; + +async function getSdkMarkdownResponse( + params: { _splat?: string }, + request: Request, + includeBody = true, +) { + const slugs = ["sdk", ...slugsFromMarkdownSplat(params._splat)]; + const body = await getMarkdownForPageSlugs(slugs, request); + return buildMarkdownRouteResponse(includeBody ? body! : null, slugs); +} + +export const Route = createFileRoute("/sdk/{$}.mdx")({ + server: { + handlers: { + GET: async ({ params, request }) => getSdkMarkdownResponse(params, request), + HEAD: async ({ params, request }) => getSdkMarkdownResponse(params, request, false), + }, + }, +}); diff --git a/src/routes/sdk[.]md.ts b/src/routes/sdk[.]md.ts new file mode 100644 index 0000000..6e6cd75 --- /dev/null +++ b/src/routes/sdk[.]md.ts @@ -0,0 +1,17 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { buildMarkdownRouteResponse, getMarkdownForPageSlugs } from "@/lib/markdown-route"; + +async function getSdkMarkdownResponse(request: Request, includeBody = true) { + const slugs = ["sdk"]; + const body = await getMarkdownForPageSlugs(slugs, request); + return buildMarkdownRouteResponse(includeBody ? body! : null, slugs); +} + +export const Route = createFileRoute("/sdk.md")({ + server: { + handlers: { + GET: async ({ request }) => getSdkMarkdownResponse(request), + HEAD: async ({ request }) => getSdkMarkdownResponse(request, false), + }, + }, +}); diff --git a/src/routes/sdk[.]mdx.ts b/src/routes/sdk[.]mdx.ts new file mode 100644 index 0000000..3b1b7b2 --- /dev/null +++ b/src/routes/sdk[.]mdx.ts @@ -0,0 +1,17 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { buildMarkdownRouteResponse, getMarkdownForPageSlugs } from "@/lib/markdown-route"; + +async function getSdkMarkdownResponse(request: Request, includeBody = true) { + const slugs = ["sdk"]; + const body = await getMarkdownForPageSlugs(slugs, request); + return buildMarkdownRouteResponse(includeBody ? body! : null, slugs); +} + +export const Route = createFileRoute("/sdk.mdx")({ + server: { + handlers: { + GET: async ({ request }) => getSdkMarkdownResponse(request), + HEAD: async ({ request }) => getSdkMarkdownResponse(request, false), + }, + }, +}); From 870762a6f69c95370b8a9e58e4a95cf245621d4d Mon Sep 17 00:00:00 2001 From: Duncan Crawbuck Date: Mon, 15 Jun 2026 14:27:52 -0700 Subject: [PATCH 9/9] Clarify llms markdown fetching --- src/lib/llms.test.ts | 29 +++++++++++++++++++++++++++++ src/lib/llms.ts | 6 ++++-- 2 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 src/lib/llms.test.ts diff --git a/src/lib/llms.test.ts b/src/lib/llms.test.ts new file mode 100644 index 0000000..4e9bd46 --- /dev/null +++ b/src/lib/llms.test.ts @@ -0,0 +1,29 @@ +import assert from "node:assert/strict"; +import { describe, test } from "node:test"; +import { buildLLMSummaryTextFromPages } from "./llms"; + +describe("buildLLMSummaryTextFromPages", () => { + test("keeps canonical page links and explains markdown fetch options", () => { + const text = buildLLMSummaryTextFromPages([ + { + url: "/docs/dashboard/templates", + data: { + title: "Templates", + description: "Use templates to start a paywall design.", + }, + }, + ]); + + assert.match( + text, + /canonical docs URLs and return HTML by default for browsers, `curl`, and clients that send `Accept: \*\/\*`/, + ); + assert.match(text, /send `Accept: text\/markdown`/); + assert.match(text, /append `\.md` to the route/); + assert.match( + text, + /- \[Templates\]\(\/docs\/dashboard\/templates\): Use templates to start a paywall design\./, + ); + assert.doesNotMatch(text, /\[Templates\]\(\/docs\/dashboard\/templates\.md\)/); + }); +}); diff --git a/src/lib/llms.ts b/src/lib/llms.ts index 7db001f..983a42f 100644 --- a/src/lib/llms.ts +++ b/src/lib/llms.ts @@ -114,11 +114,13 @@ export function buildLLMSummaryTextFromPages(pages: LLMPageLike[], section?: str ? getExamplePathForSection(section!) : "/docs/android/guides/advanced/game-controller-support.md"; lines.push( - "Curl the pages below to read docs for each page. You can request markdown files by appending .md to a route, for example:", + "The page links below are canonical docs URLs and return HTML by default for browsers, `curl`, and clients that send `Accept: */*`.", + "", + "To fetch a page as markdown, either send `Accept: text/markdown` for the canonical URL or append `.md` to the route, for example:", "", `- https://superwall.com${examplePath}`, "", - "Note: Doc URLs return a 307 redirect. Always use `curl -sL` (follow redirects) when fetching them.", + "When using `Accept: text/markdown`, doc URLs return a 307 redirect to the `.md` route. Always use `curl -sL` (follow redirects) when fetching them.", "", );