From dfa4651ebc8a237478ffb65a670948108f846878 Mon Sep 17 00:00:00 2001 From: Luca Martial Date: Mon, 11 May 2026 16:40:34 -0700 Subject: [PATCH] feat(docs-site): add agent-readable docs routes --- docs-site/app/llms-full.txt/route.ts | 11 ++ .../app/llms.mdx/docs/[[...slug]]/route.ts | 27 +++++ docs-site/app/llms.txt/route.ts | 11 ++ docs-site/lib/llm-docs.ts | 101 ++++++++++++++++++ docs-site/middleware.ts | 51 +++++++++ docs-site/next.config.mjs | 11 +- 6 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 docs-site/app/llms-full.txt/route.ts create mode 100644 docs-site/app/llms.mdx/docs/[[...slug]]/route.ts create mode 100644 docs-site/app/llms.txt/route.ts create mode 100644 docs-site/lib/llm-docs.ts create mode 100644 docs-site/middleware.ts diff --git a/docs-site/app/llms-full.txt/route.ts b/docs-site/app/llms-full.txt/route.ts new file mode 100644 index 00000000..0edf170c --- /dev/null +++ b/docs-site/app/llms-full.txt/route.ts @@ -0,0 +1,11 @@ +import { buildLlmsFullTxt } from "@/lib/llm-docs"; + +export const dynamic = "force-static"; + +export async function GET() { + return new Response(await buildLlmsFullTxt(), { + headers: { + "Content-Type": "text/plain; charset=utf-8", + }, + }); +} diff --git a/docs-site/app/llms.mdx/docs/[[...slug]]/route.ts b/docs-site/app/llms.mdx/docs/[[...slug]]/route.ts new file mode 100644 index 00000000..e4cd9e4a --- /dev/null +++ b/docs-site/app/llms.mdx/docs/[[...slug]]/route.ts @@ -0,0 +1,27 @@ +import { + getLlmDocsPage, + getLlmDocsPages, + getPageMarkdown, +} from "@/lib/llm-docs"; +import { notFound } from "next/navigation"; + +export const dynamic = "force-static"; + +export async function GET( + _request: Request, + props: { params: Promise<{ slug?: string[] }> }, +) { + const params = await props.params; + const page = getLlmDocsPage(params.slug); + if (!page) notFound(); + + return new Response(await getPageMarkdown(page), { + headers: { + "Content-Type": "text/markdown; charset=utf-8", + }, + }); +} + +export function generateStaticParams() { + return getLlmDocsPages().map((page) => ({ slug: page.slug })); +} diff --git a/docs-site/app/llms.txt/route.ts b/docs-site/app/llms.txt/route.ts new file mode 100644 index 00000000..7b65782a --- /dev/null +++ b/docs-site/app/llms.txt/route.ts @@ -0,0 +1,11 @@ +import { buildLlmsTxt } from "@/lib/llm-docs"; + +export const dynamic = "force-static"; + +export function GET() { + return new Response(buildLlmsTxt(), { + headers: { + "Content-Type": "text/plain; charset=utf-8", + }, + }); +} diff --git a/docs-site/lib/llm-docs.ts b/docs-site/lib/llm-docs.ts new file mode 100644 index 00000000..4e7d024d --- /dev/null +++ b/docs-site/lib/llm-docs.ts @@ -0,0 +1,101 @@ +import { source } from "@/lib/source"; + +const siteOrigin = "https://ktx.dev"; + +export type LlmDocsPage = { + title: string; + description?: string; + url: string; + markdownUrl: string; + slug: string[]; + getMarkdown: () => Promise; +}; + +export function getLlmDocsPages(): LlmDocsPage[] { + return source.getPages().map(toLlmDocsPage); +} + +export function getLlmDocsPage(slug: string[] | undefined) { + const page = source.getPage(slug); + return page ? toLlmDocsPage(page) : null; +} + +export async function getPageMarkdown(page: LlmDocsPage) { + const description = page.description ? `\n\n> ${page.description}` : ""; + const body = await page.getMarkdown(); + + return normalizeMarkdown(`# ${page.title}${description} + +Canonical URL: ${page.url} +Markdown URL: ${page.markdownUrl} + +${body} +`); +} + +export function buildLlmsTxt() { + const pages = getLlmDocsPages(); + const byUrl = new Map(pages.map((page) => [page.url, page])); + const link = (url: string, label: string, fallbackDescription: string) => { + const page = byUrl.get(url); + const description = page?.description ?? fallbackDescription; + return `- [${label}](${url}): ${description}`; + }; + + return `# KTX + +> Agent-native context layer for analytics engineering and database agents. + +KTX provides semantic-layer files, warehouse scans, knowledge pages, provenance, and agent-facing tools that help coding agents answer analytics questions without inventing metrics or joins. + +## Start Here + +${link("/docs/getting-started/introduction", "Introduction", "What KTX is and who it is for")} +${link("/docs/getting-started/quickstart", "Quickstart", "Set up KTX and build your first context")} +${link("/docs/guides/serving-agents", "Serving Agents", "Expose KTX context through MCP and CLI tools")} +${link("/docs/guides/writing-context", "Writing Context", "Write semantic sources and knowledge pages")} + +## Machine-Readable Documentation + +- [Full documentation](/llms-full.txt): All docs pages in one plain-text markdown response +- [Quickstart markdown](/docs/getting-started/quickstart.md): Raw markdown for the setup guide +- [Agent CLI markdown](/docs/cli-reference/ktx-agent.md): Raw markdown for machine-readable agent commands +- [Serving Agents markdown](/docs/guides/serving-agents.md): Raw markdown for MCP and CLI workflows + +## CLI Reference + +${link("/docs/cli-reference/ktx-setup", "ktx setup", "Interactive project setup")} +${link("/docs/cli-reference/ktx-agent", "ktx agent", "Machine-readable commands for coding agents")} +${link("/docs/cli-reference/ktx-sl", "ktx sl", "Semantic-layer commands")} +${link("/docs/cli-reference/ktx-wiki", "ktx wiki", "Knowledge page commands")} +${link("/docs/cli-reference/ktx-connection", "ktx connection", "Connection management commands")} + +## Integrations + +${link("/docs/integrations/agent-clients", "Agent Clients", "Configure Claude Code, Cursor, Codex, and OpenCode")} +${link("/docs/integrations/primary-sources", "Primary Sources", "Connect KTX to databases and warehouses")} +${link("/docs/integrations/context-sources", "Context Sources", "Ingest dbt, LookML, Metabase, Looker, MetricFlow, and Notion")} +`; +} + +export async function buildLlmsFullTxt() { + const rendered = await Promise.all(getLlmDocsPages().map(getPageMarkdown)); + return [`# KTX Full Documentation`, `Source: ${siteOrigin}`, ...rendered].join( + "\n\n---\n\n", + ); +} + +function toLlmDocsPage(page: ReturnType[number]) { + return { + title: page.data.title, + description: page.data.description, + url: page.url, + markdownUrl: `${page.url}.md`, + slug: page.slugs, + getMarkdown: async () => normalizeMarkdown(page.data.content), + } satisfies LlmDocsPage; +} + +function normalizeMarkdown(markdown: string) { + return markdown.trim().replace(/\n{3,}/g, "\n\n"); +} diff --git a/docs-site/middleware.ts b/docs-site/middleware.ts new file mode 100644 index 00000000..4e06ea8b --- /dev/null +++ b/docs-site/middleware.ts @@ -0,0 +1,51 @@ +import { NextResponse, type NextRequest } from "next/server"; + +const markdownMimeTypes = new Set([ + "text/markdown", + "text/x-markdown", + "application/markdown", +]); + +export function middleware(request: NextRequest) { + if (!isMarkdownPreferred(request.headers.get("accept"))) { + return NextResponse.next(); + } + + const { pathname } = request.nextUrl; + if (!pathname.startsWith("/docs/") || pathname.endsWith(".md")) { + return NextResponse.next(); + } + + const rewriteUrl = request.nextUrl.clone(); + rewriteUrl.pathname = `/llms.mdx${pathname}`; + + return NextResponse.rewrite(rewriteUrl); +} + +export const config = { + matcher: ["/docs/:path*"], +}; + +function isMarkdownPreferred(acceptHeader: string | null) { + if (!acceptHeader) return false; + + const accepted = acceptHeader + .split(",") + .map((entry, index) => { + const [type = "", ...parameters] = entry.trim().split(";"); + const quality = parameters + .map((parameter) => parameter.trim()) + .find((parameter) => parameter.startsWith("q=")); + + return { + type: type.toLowerCase(), + quality: quality ? Number.parseFloat(quality.slice(2)) : 1, + index, + }; + }) + .filter((entry) => Number.isFinite(entry.quality) && entry.quality > 0) + .sort((a, b) => b.quality - a.quality || a.index - b.index); + + const preferred = accepted[0]?.type; + return preferred ? markdownMimeTypes.has(preferred) : false; +} diff --git a/docs-site/next.config.mjs b/docs-site/next.config.mjs index d07746eb..8b28486f 100644 --- a/docs-site/next.config.mjs +++ b/docs-site/next.config.mjs @@ -3,6 +3,15 @@ import { createMDX } from "fumadocs-mdx/next"; const withMDX = createMDX(); /** @type {import('next').NextConfig} */ -const config = {}; +const config = { + async rewrites() { + return [ + { + source: "/docs/:path*.md", + destination: "/llms.mdx/docs/:path*", + }, + ]; + }, +}; export default withMDX(config);