From ccef001089dc0857dd9948c324635e78f245a891 Mon Sep 17 00:00:00 2001 From: Ajit Pratap Singh Date: Fri, 16 Oct 2026 06:27:46 +0530 Subject: [PATCH] feat(website): raw-markdown alternates for docs and complete llms-full.txt Serve every docs page as /docs/.md (rewrite to a static route handler, exempt from the trailing-slash redirect), advertise it with on each docs page, mention it in llms.txt, and share one markdown builder with llms-full.txt (42 docs). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RkzVu3JY9Y6BRWfjesWts9 --- website/next.config.ts | 9 ++++++ website/src/app/docs/[...slug]/page.tsx | 7 +++- website/src/app/md/docs/[...slug]/route.ts | 22 +++++++++++++ website/src/lib/llms.ts | 37 ++++++++++++++-------- 4 files changed, 61 insertions(+), 14 deletions(-) create mode 100644 website/src/app/md/docs/[...slug]/route.ts diff --git a/website/next.config.ts b/website/next.config.ts index 5ca75675..8c20185e 100644 --- a/website/next.config.ts +++ b/website/next.config.ts @@ -58,6 +58,15 @@ const nextConfig: NextConfig = { }, ]; }, + async rewrites() { + // Raw-markdown alternates: /docs/.md is served by the /md/docs route handler. + // Paths with an extension are exempt from the trailing-slash redirect. + return { + beforeFiles: [{ source: '/docs/:slug*.md', destination: '/md/docs/:slug*' }], + afterFiles: [], + fallback: [], + }; + }, async redirects() { return [ // /blog was release notes only; it is now /changelog. Single hop, trailing slashes kept. diff --git a/website/src/app/docs/[...slug]/page.tsx b/website/src/app/docs/[...slug]/page.tsx index 91f73d65..1c5d96ce 100644 --- a/website/src/app/docs/[...slug]/page.tsx +++ b/website/src/app/docs/[...slug]/page.tsx @@ -7,6 +7,7 @@ import remarkGfm from 'remark-gfm'; import rehypeSlug from 'rehype-slug'; import { getDocBySlug, getDocSlugs, getAdjacentDocs, extractHeadings, stripCodeFences } from '@/lib/docs'; import { DOCS_SIDEBAR } from '@/lib/constants'; +import { docMarkdownPath } from '@/lib/llms'; import { PROJECT } from '@/data/metrics'; import { Sidebar } from '@/components/docs/Sidebar'; import { Toc } from '@/components/docs/Toc'; @@ -44,12 +45,16 @@ export async function generateMetadata({ params }: PageProps): Promise const doc = getDocBySlug(slugStr); if (!doc) return { title: { absolute: 'Page not found | GoSQLX' } }; - return pageMeta({ + const meta = pageMeta({ title: `${doc.label} (Docs)`, description: doc.description, path: `/docs/${slugStr}/`, image: { url: `/og/docs/${slugStr}/`, alt: `${doc.label} (GoSQLX docs)` }, }); + return { + ...meta, + alternates: { ...meta.alternates, types: { 'text/markdown': docMarkdownPath(slugStr) } }, + }; } export default async function DocPage({ params }: PageProps) { diff --git a/website/src/app/md/docs/[...slug]/route.ts b/website/src/app/md/docs/[...slug]/route.ts new file mode 100644 index 00000000..3085d792 --- /dev/null +++ b/website/src/app/md/docs/[...slug]/route.ts @@ -0,0 +1,22 @@ +import { notFound } from 'next/navigation'; +import { buildDocMarkdown, MARKDOWN_HEADERS } from '@/lib/llms'; +import { getDocBySlug, getDocSlugs } from '@/lib/docs'; + +// Served publicly as /docs/.md through a rewrite in next.config.ts. +export const dynamic = 'force-static'; +export const dynamicParams = false; + +interface RouteContext { + params: Promise<{ slug: string[] }>; +} + +export function generateStaticParams() { + return getDocSlugs().map((slug) => ({ slug: slug.split('/') })); +} + +export async function GET(_req: Request, { params }: RouteContext): Promise { + const { slug } = await params; + const doc = getDocBySlug(slug.join('/')); + if (!doc) notFound(); + return new Response(buildDocMarkdown(doc), { headers: MARKDOWN_HEADERS }); +} diff --git a/website/src/lib/llms.ts b/website/src/lib/llms.ts index d5165397..bc3d4547 100644 --- a/website/src/lib/llms.ts +++ b/website/src/lib/llms.ts @@ -1,5 +1,5 @@ import { DOCS_SIDEBAR } from '@/lib/constants'; -import { getAllDocs } from '@/lib/docs'; +import { getAllDocs, type Doc } from '@/lib/docs'; import { PROJECT, QA_CORPUS, BENCH_ENV, PARSE_BENCHMARKS, opsPerSec, formatOps } from '@/data/metrics'; export const SITE_URL = 'https://gosqlx.dev'; @@ -52,6 +52,7 @@ export function buildLlmsTxt(): string { '## Optional', '', `- [Full documentation in one file](${SITE_URL}/llms-full.txt): Every docs page as plain text`, + `- Raw markdown of any docs page: append \`.md\` to its URL, e.g. ${SITE_URL}/docs/getting-started.md`, `- [Changelog](${SITE_URL}/changelog/): Release notes for every version`, `- [Changelog feed](${SITE_URL}/changelog/feed.xml): RSS 2.0`, `- [Source code](${REPO}): GitHub repository`, @@ -61,22 +62,32 @@ export function buildLlmsTxt(): string { return lines.join('\n'); } +export const MARKDOWN_HEADERS = { + 'Content-Type': 'text/markdown; charset=utf-8', +} as const; + +/** Path of the raw-markdown alternate of a docs page. No trailing slash: the path has an extension. */ +export function docMarkdownPath(slug: string): string { + return `/docs/${slug}.md`; +} + +/** One docs page as standalone markdown: title, canonical source URL, then the body. */ +export function buildDocMarkdown(doc: Doc): string { + return [ + `# ${doc.title}`, + '', + `Source: ${SITE_URL}/docs/${doc.slug}/`, + '', + doc.content.replace(/^\s*#\s+.+\n+/, '').trim(), + '', + ].join('\n'); +} + /** Index summary followed by the full markdown of every docs page. */ export function buildLlmsFullTxt(): string { const parts = [header().join('\n')]; for (const doc of getAllDocs()) { - parts.push( - [ - '---', - '', - `# ${doc.title}`, - '', - `Source: ${SITE_URL}/docs/${doc.slug}/`, - '', - doc.content.replace(/^\s*#\s+.+\n+/, '').trim(), - '', - ].join('\n') - ); + parts.push(['---', '', buildDocMarkdown(doc)].join('\n')); } return parts.join('\n'); }