# 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

```ts
// 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.

> [!NOTE]
> **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](/en/docs/guides/markdown.md#urls).

## Links

The first argument of `toHref` is the collection name, and the id is type-checked against that collection:

```ts
toHref('docs', 'guides/search', locale);
toHref('changelog', 'v0.1.0', locale);
```

Content can link straight to files of another collection, in either direction:

```md
<!-- 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`:

```ts
// 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)`:

```astro
---
// 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:

```ts
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:

```js
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](/en/docs/guides/i18n.md#untranslated-pages).

## 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](/en/docs/guides/multiple-trees.md) is enough.
