Multiple Collections
Markdown pages beyond the docs, such as a changelog, a blog or another set of docs with different fields, each get a collection of their own, also loaded with minidocs’ glob. File links then work across the whole site and are all checked, and Markdown versions and llms.txt cover these pages too.
Defining them
// src/content.config.ts
const docs = defineCollection({
loader: glob({ base: './src/content/docs', pattern: '**/*.{md,mdx}', prefix: 'docs' }),
schema: z.object({ title: z.string() }),
});
const changelog = defineCollection({
loader: glob({ base: './src/content/changelog', pattern: '**/*.md', prefix: 'changelog' }),
schema: z.object({ title: z.string(), date: z.coerce.date() }),
});
export const collections = { docs, changelog };
Each collection has its own prefix, the path its pages live under. Everything else works as for the docs: on multilingual sites the first level of every collection is the locale, and fallback follows the same rules.
Collections without pages, such as authors, keep using Astro’s own loaders.
URLs come from file paths
A page’s URL is always the locale segment, the prefix and the page id, and the id comes from the file path. With URLs made from dates, such as /blog/2026/10/launch/, or from a slug field in the frontmatter, the links minidocs computes would not match your routes. Put the date in the file name instead, such as 2026-10-31-launch.md.
Routes
Files mirror URLs, so the route of each collection goes in the folder its prefix names:
src/pages/[...locale]/
├── index.astro → /, /en/
├── docs/[...slug].astro → /docs/…, /en/docs/…
├── changelog/
│ ├── index.astro → the list: /changelog/, /en/changelog/
│ └── [...slug].astro → /changelog/v0.1.0/…
└── [...path].md.ts → Markdown versions of every page
Every route is pages.map((page) => ({ params: page.params, props: page })). If a route file and its prefix disagree, the build fails and lists the pages that were not generated. Markdown versions are site-wide, so one route covers every collection, see Markdown and llms.txt.
Links
The first argument of toHref is the collection name, and the id is type-checked against that collection:
toHref('docs', 'guides/search', locale);
toHref('changelog', 'v0.1.0', locale);
Content can link straight to files of another collection, in either direction:
<!-- src/content/docs/en/guides/search.mdx -->
This was added in [0.1.0](../../../changelog/en/v0.1.0.md).
<!-- src/content/changelog/en/v0.1.0.md -->
See the [search guide](../../docs/en/guides/search.mdx).
The rules are the same as within a collection: links within one locale become relative URLs, so a changelog shown in Chinese through fallback keeps its links on the Chinese site, and linking into another locale’s directory picks that locale on purpose. A missing target, or one its collection’s filter leaves out, fails the build.
Lists, previous and next, and llms.txt
A flat list sorted by date is a tree too, defined with defineTree:
// src/lib/changelog.ts
export const entries = await getCollection('changelog');
export const pages = listPages(entries);
export const releases = await defineTree(entries, ({ dir }) =>
dir('', { compare: (a, b) => b.page!.entry.data.date.valueOf() - a.page!.entry.data.date.valueOf() }),
);
- The sidebar and previous and next links use
navState(releases, id)andneighbors(releases, id), written the same way as for the docs - The list page is the site’s own page rather than part of the collection, so it has its own route,
changelog/index.astro. It generates one page per locale withtoLocaleParam, walksreleases[locale]and links to each release withtoHref('changelog', page.id, locale):
---
// src/pages/[...locale]/changelog/index.astro
export function getStaticPaths() {
return locales.map((locale) => ({ params: { locale: toLocaleParam(locale) }, props: { locale } }));
}
---
- llms.txt can join the trees of several collections, and the entries
describereceives are typed as entries of any of them:
toLlmsTxt([...tree[locale], { label: 'Changelog', children: releases[locale] }], {
title: 'My docs',
describe: (entry) => entry.data.description,
});
Pagination and RSS are your own code as well, and the href from listPages is the URL of each page.
Content in one language
Say the changelog is only written in English. Its files all sit in en/, and other locales show them through fallback:
minidocs({ fallback: { en: 'zh', zh: 'en' } })
Every locale then has its own changelog pages: /changelog/v0.1.0/ is the Chinese interface around English content, and /en/changelog/v0.1.0/ is English. A reader coming from the Chinese docs keeps a Chinese header, sidebar and links back to the docs. Fallback leaves the docs as they are: the Chinese docs are complete, and zh: 'en' only applies to pages missing in Chinese.
These pages have isFallback set to true, so you can tell readers as shown in Multilingual Sites.
URL conflicts
Page URLs and Markdown URLs of every collection are checked together. For example, if the docs collection has no prefix and en/ happens to contain changelog/v0.1.0.mdx, the build fails:
[minidocs] "/en/changelog/v0.1.0/" is the URL of two pages:
src/content/changelog/en/v0.1.0.md (changelog)
src/content/docs/en/changelog/v0.1.0.mdx (docs)
So prefixes can nest, such as docs and docs/v1, and only an actual clash is an error.
When one collection is enough
When one set of docs is split into parts, such as “Guides” and “API Reference”, with the same URL prefix and fields, one collection with multiple trees is enough.