minidocs

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.

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) and neighbors(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 with toLocaleParam, walks releases[locale] and links to each release with toHref('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 describe receives 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.

On this page
Search in
Type to start searching