minidocs

Markdown and llms.txt

Every page can have a Markdown version for agents to read, at the page’s URL with .md added. You can also generate llms.txt from the document tree as an index of the whole docs. Both are generated at build time as plain static files.

Markdown versions

Add one route file under src/pages:

// src/pages/[...path].md.ts
import type { APIRoute } from 'astro';
import { toMarkdown } from '@minidocs/astro/runtime';
import { pages } from '../lib/docs';

export function getStaticPaths() {
  return pages.map((page) => ({ params: page.markdownParams, props: page }));
}

export const GET: APIRoute<(typeof pages)[number]> = async ({ props: page }) =>
  new Response(`# ${page.entry.data.title}\n\n${await toMarkdown(page)}`, {
    headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
  });

toMarkdown converts only the content, without a title, so the route above adds the title itself. Add a description or other fields here the same way.

Then tell agents about it in the <head> of your layout:

<link rel="alternate" type="text/markdown" href={page.markdownHref} />

URLs

Drop the trailing slash of the page’s URL and add .md:

HTML Markdown
/docs/ /docs.md
/docs/guides/sidebar/ /docs/guides/sidebar.md

The /docs.md of the collection’s home page sits outside the docs/ folder, so this route goes directly under src/pages, not in docs/. It takes page.markdownParams, not the page.params of page routes.

One route file generates the Markdown versions of every collection; join the pages of each: const pages = [...docs, ...blog].

On multilingual sites, the route goes in src/pages/[...locale]/[...path].md.ts, and the Markdown version of an English page is /en/docs/guides/sidebar.md.

What gets converted

toMarkdown has Astro render the content to HTML and then converts it to Markdown, so component output and image URLs match the HTML page. Along the way:

  • Callouts become GitHub’s > [!TIP] syntax
  • Code blocks keep their language and lose the highlighting markup
  • Links to other pages point to their Markdown versions, so agents can keep reading Markdown
  • Other relative URLs become paths starting with /

GFM syntax such as tables is kept. Formatting is normalized, such as * for all emphasis.

Components

Components in your content render as usual, and their HTML is converted to Markdown. When a component’s look on the page does not convert well to text, such as an interactive widget with buttons, use isMarkdown to output simple HTML for the Markdown version:

---
// src/components/PropsTable.astro
import { isMarkdown } from '@minidocs/astro/runtime';

const { of } = Astro.props;
const props = await loadProps(of);
---

{isMarkdown(Astro) ? (
  <table>
    <tr><th>Prop</th><th>Type</th></tr>
    {props.map((prop) => <tr><td>{prop.name}</td><td>{prop.type}</td></tr>)}
  </table>
) : (
  <PropsExplorer props={props} />
)}

When your content uses components of frameworks such as React or Vue, pass the renderers of their integrations to toMarkdown:

import { getContainerRenderer } from '@astrojs/react';

await toMarkdown(page, { renderers: [getContainerRenderer()] });

llms.txt

llms.txt is a site index for large language models: the site name, a summary, then grouped lists of pages, each linking to the page’s Markdown version. minidocs generates it from the document tree:

// src/pages/docs/llms.txt.ts
import type { APIRoute } from 'astro';
import { toLlmsTxt } from '@minidocs/astro/runtime';
import { tree } from '../../lib/docs';

export const GET: APIRoute = () =>
  new Response(toLlmsTxt(tree, { title: 'My docs', describe: (entry) => entry.data.description }));

The file sits next to the docs home and generates /docs/llms.txt. Without prefix, put it at src/pages/llms.txt.ts to generate /llms.txt.

What goes in the file:

  • The heading is title
  • The summary is the description of the index page, read by describe, and is left out when there is none
  • Pages on the first level are listed after the summary, groups on the first level each become a section, and deeper levels are nested lists
  • With Astro’s site set, links are full URLs

llms-full.txt

To put the content of every page into one file, write the same kind of route:

// src/pages/docs/llms-full.txt.ts
export const GET: APIRoute = async () => new Response(await toLlmsFullTxt(tree));

Pages follow the order of the document tree, each with its title, source URL and the result of toMarkdown.

Multilingual sites

Generate one file per locale, with the route at src/pages/[...locale]/docs/llms.txt.ts and toLocaleParam giving each locale’s parameter:

export function getStaticPaths() {
  return locales.map((locale) => ({ params: { locale: toLocaleParam(locale) }, props: { locale } }));
}

export const GET: APIRoute<{ locale: Locale }> = ({ props: { locale } }) =>
  new Response(toLlmsTxt(tree[locale], { title: 'My docs' }));

locales is the list of the site’s locales, see Multilingual Sites.

Limitations

  • In static builds, response headers set in routes have no effect, and the hosting platform picks Content-Type by file extension. Some leave out charset=utf-8, which garbles non-ASCII text when the file is opened in a browser; agents are usually unaffected
  • client:only components do not render on the server and are left out of Markdown versions
  • Titles of code blocks are not kept
  • Rendering uses Astro’s Container API, which is still experimental in Astro 7
On this page
Search in
Type to start searching