From dcfb076f5744e490da0baaa925e2ace3f2644bef Mon Sep 17 00:00:00 2001 From: Vishal Rana Date: Sun, 27 Sep 2026 19:13:03 -0700 Subject: [PATCH] docs(site): generate llms.txt from documentation --- README.md | 4 +++ site/scripts/check-site.mjs | 13 ++++++++ site/src/pages/llms.txt.ts | 61 +++++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 site/src/pages/llms.txt.ts diff --git a/README.md b/README.md index cb6d5e0a..684aba89 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,10 @@ Content is Markdown/MDX under `site/src/content/docs/` (`guide/`, `middleware/`, generated from each page's `sidebar.order` frontmatter. Every page needs a `title` and `description`. +The build also publishes `/llms.txt` and `/next/llms.txt`. Their page links and +descriptions come from that same content, and each index identifies its pinned +Echo source revision. `site:check` verifies both files and their site links. + ## Cookbook recipes Each folder under `cookbook/` is a self-contained example. Run one with: diff --git a/site/scripts/check-site.mjs b/site/scripts/check-site.mjs index cf5b1d25..001ce513 100644 --- a/site/scripts/check-site.mjs +++ b/site/scripts/check-site.mjs @@ -98,5 +98,18 @@ for (const locale of ['es', 'ja', 'pt-br', 'zh-cn']) { for (const path of ['sitemap-index.xml', 'pagefind/pagefind.js']) { if (!existsSync(join(distDir, path))) problems.push(`Missing ${path}`); } +for (const path of ['llms.txt', 'next/llms.txt']) { + const file = join(distDir, path); + if (!existsSync(file)) { problems.push(`Missing ${path}`); continue; } + const content = readFileSync(file, 'utf8'); + if (!content.startsWith('# Echo\n')) problems.push(`${path}: missing Echo heading`); + for (const [, link] of content.matchAll(/\]\((https:\/\/echo\.labstack\.com\/[^)]+)\)/g)) { + const pathname = new URL(link).pathname; + const resolved = resolve(distDir, `.${pathname}`); + const target = extname(resolved) ? resolved : join(resolved, 'index.html'); + if (!existsSync(target)) problems.push(`${path}: missing ${link}`); + checked++; + } +} if (problems.length) throw new Error(`${problems.length} site check failure(s):\n${problems.join('\n')}`); console.log(`Checked ${routes.length} routes and ${checked} internal links/assets; all resolve`); diff --git a/site/src/pages/llms.txt.ts b/site/src/pages/llms.txt.ts new file mode 100644 index 00000000..eefd1aa3 --- /dev/null +++ b/site/src/pages/llms.txt.ts @@ -0,0 +1,61 @@ +import { getCollection } from 'astro:content'; +import type { APIRoute } from 'astro'; +import stableSource from '../../echo-source.json'; +import nextSource from '../../next-source.json'; + +export const prerender = true; + +const origin = 'https://echo.labstack.com'; +const sections = [ + { title: 'Guide', prefix: 'guide/' }, + { title: 'Middleware', prefix: 'middleware/' }, + { title: 'Cookbook', prefix: 'cookbook/' }, +]; + +export const GET: APIRoute = async () => { + const base = import.meta.env.BASE_URL; + const source = base === '/next/' ? nextSource : stableSource; + const pages = await getCollection('docs'); + const englishPages = pages.filter(({ id }) => + !/^(?:es|ja|pt-br|zh-cn)\//.test(id), + ); + const lines = [ + '# Echo', + '', + '> Echo is a Go web framework. These docs cover setup, routing, requests, responses, middleware, and runnable examples.', + '', + `This index describes the ${source.release} docs, built against [Echo source](${source.repository.replace(/\.git$/, '')}/tree/${source.revision}). For package API signatures, see [pkg.go.dev](https://pkg.go.dev/github.com/labstack/echo/v5).`, + '', + ]; + + for (const { title, prefix } of sections) { + lines.push(`## ${title}`, ''); + const entries = englishPages + .filter(({ id }) => id.startsWith(prefix)) + .sort((a, b) => + (a.data.sidebar?.order ?? 999) - (b.data.sidebar?.order ?? 999) || + a.data.title.localeCompare(b.data.title), + ); + for (const { id, data } of entries) { + const route = id.endsWith('/index') ? id.slice(0, -'/index'.length) : id; + lines.push(`- [${data.title}](${origin}${base}${route}/): ${data.description}`); + } + lines.push(''); + } + + lines.push('## Other versions and languages', ''); + if (base === '/') lines.push(`- [Next version](${origin}/next/llms.txt): preview docs for the next Echo release.`); + else lines.push(`- [Stable version](${origin}/llms.txt): current release docs.`); + for (const [locale, label] of [ + ['es', 'Español'], + ['ja', '日本語'], + ['pt-br', 'Português'], + ['zh-cn', '简体中文'], + ]) { + lines.push(`- [${label}](${origin}${base}${locale}/): localized documentation.`); + } + + return new Response(`${lines.join('\n')}\n`, { + headers: { 'Content-Type': 'text/plain; charset=utf-8' }, + }); +};