{"slug": "generating-a-multilingual-llms-txt-in-astro", "title": "Generating a multilingual llms.txt in Astro", "summary": "A developer created a multilingual llms.txt generator for Astro sites using Content Collections and API routes. The implementation produces three endpoints—/llms.txt, /ja/llms.txt, and /llms-full.txt—from a single renderer, with language filtering and draft exclusion. The approach avoids manual upkeep by dynamically assembling the post list from the collection.", "body_md": "llms.txt is a Markdown index for LLMs, placed at the site root. Where `sitemap.xml`\n\nis a machine-readable list of URLs, llms.txt describes — with one-line notes — what the site is and where to start reading.\n\nIn Astro you can generate it from Content Collections as an API route, so the post list never has to be hand-maintained. This post is the minimum setup for a bilingual (EN/JA) site: emit `/llms.txt`\n\n, `/ja/llms.txt`\n\nand `/llms-full.txt`\n\nfrom one renderer.\n\nUp front: how much llms.txt actually helps AI-search traffic isn't a settled or measured thing yet. This is only about the implementation.\n\nAstro's file-based API routes return text when you drop a `.txt.ts`\n\nfile under `src/pages/`\n\n. Return a `text/plain`\n\n`Response`\n\nfrom a `GET`\n\nhandler.\n\n``` python\n// src/pages/llms.txt.ts\nimport type { APIContext } from \"astro\";\nimport { renderLlmsTxt } from \"../lib/llmsTxt\";\n\nexport async function GET(_context: APIContext) {\n  const body = await renderLlmsTxt({ docLang: \"en\" });\n  return new Response(body, {\n    status: 200,\n    headers: {\n      \"Content-Type\": \"text/plain; charset=utf-8\",\n      \"Cache-Control\": \"public, max-age=3600\",\n    },\n  });\n}\n```\n\nThe `.txt.ts`\n\nextension builds to the URL `/llms.txt`\n\n. Keep the assembly logic in `src/lib/llmsTxt.ts`\n\nand leave the route thin, so a per-language endpoint can reuse it.\n\nGet the post list with `getCollection`\n\nand lay it out on the fly. A hand-kept list goes stale — add a post, forget the index, and llms.txt drifts from the content.\n\n```\n// src/lib/llmsTxt.ts (excerpt)\nexport async function renderLlmsTxt(opts: LlmsTxtOptions): Promise<string> {\n  const blog = await getCollection(\"blog\", ({ data }) => !data.draft);\n  blog.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime());\n  // ...assemble sections and return join(\"\\n\")\n}\n```\n\nDon't drop the `({ data }) => !data.draft`\n\nfilter. Skip it and a half-written draft lands in llms.txt, advertising a URL you haven't published. Reuse the same exclusion sitemap and RSS use.\n\nThis is the part that matters for a multilingual site. Give the renderer two axes:\n\nSeparating them lets one renderer emit three endpoints.\n\n```\n// src/pages/llms.txt.ts        → English headings, posts from all languages\nrenderLlmsTxt({ docLang: \"en\" });\n\n// src/pages/ja/llms.txt.ts     → Japanese headings, Japanese posts only\nrenderLlmsTxt({ filterLang: \"ja\", docLang: \"ja\" });\n```\n\nThe English `/llms.txt`\n\nleaves `filterLang`\n\nunset on purpose — it's the whole-site entry point, so it surfaces posts in either language. The Japanese `/ja/llms.txt`\n\ncloses to the Japanese surface with `filterLang: \"ja\"`\n\n.\n\nOne design call. The English version can surface both languages, but the Featured section alone narrows to `docLang`\n\n.\n\n``` js\nconst featuredSource = filteredBlog.filter(\n  (p) => entryLangLocal(p.id) === opts.docLang,\n);\n```\n\nListing both halves of a translation pair in the featured slots spends two slots on one piece of content and halves the unique signal in a bounded list. So a limited list (featured) narrows by language; a full dump with loose size limits (llms-full.txt) carries both.\n\n`draft`\n\nexclusion in the shared renderer`filterLang`\n\nand `docLang`\n\n`filterLang`\n\nunset on the English version is deliberate — don't read it later as a bug and add a filterGenerate llms.txt from Content Collections as an Astro API route and the manual upkeep goes away. Split on filterLang and docLang and one renderer emits all three files.\n\nThe language cross-references in llms-full.txt, how it sits next to robots.txt, and how I word the usage/citation section are on the Aulvem site → [Generating llms.txt and llms-full.txt in Astro for a Bilingual Site](https://aulvem.com/blog/2026-06-14-aulvem-llms-txt-multilingual/)", "url": "https://wpnews.pro/news/generating-a-multilingual-llms-txt-in-astro", "canonical_source": "https://dev.to/aulvem/generating-a-multilingual-llmstxt-in-astro-3go3", "published_at": "2026-06-14 05:52:38+00:00", "updated_at": "2026-06-14 06:29:13.723082+00:00", "lang": "en", "topics": ["developer-tools", "large-language-models", "ai-tools"], "entities": ["Astro", "llms.txt", "Content Collections", "API route", "Aulvem"], "alternates": {"html": "https://wpnews.pro/news/generating-a-multilingual-llms-txt-in-astro", "markdown": "https://wpnews.pro/news/generating-a-multilingual-llms-txt-in-astro.md", "text": "https://wpnews.pro/news/generating-a-multilingual-llms-txt-in-astro.txt", "jsonld": "https://wpnews.pro/news/generating-a-multilingual-llms-txt-in-astro.jsonld"}}