`duxt.sources` is a list of source entries; `duxt.sourceOptions` says how they
become URLs. Both are read at build time. What they generate, and why, is in
[Sources](/concepts/sources).

Refs require an explicit `repo`, including when you publish another ref of this
repository. Without `repo`, a source reads the current local checkout and
rejects `refs` (including `latest`) and local locale-level `ref` overrides.
`origin.repo` and `origin.ref` only supply provenance links; they do not select
content. A locale with its own explicit `repo` may select its own `ref`. For
versions kept locally, use a source's `version` with distinct folders or a
generated section's file-based `versions`.

## A source entry

| Field            | Type                             | Notes                                                          |
| :--------------- | :------------------------------- | :------------------------------------------------------------- |
| `path`           | `string`                         | Folder holding the Markdown, relative to the repository root   |
| `repo`           | `string`                         | `owner/name` or a git URL. **Omitted means the local checkout** |
| `refs`           | ref list                         | Refs to publish as versions; requires an explicit `repo`       |
| `releases`       | `{ select, prereleases? }`       | Discover release tags to publish as versions — see below       |
| `tagComponent`   | `string`                         | Publish only one package's `<name>@vX.Y.Z` tags — see [Component tags](#component-tags) |
| `version`        | `string`                         | The version this source **is**, where no ref names one          |
| `label`          | text                             | Shown in the switcher and used in the URL; defaults to the ref  |
| `name`           | text                             | What to **call** this source where the site names it. Display only — see below |
| `slug`           | `string`                         | This source's URL segment. **Naming one claims it** — see below |
| `status`         | status                           | Lifecycle of every version this entry publishes                |
| `statusDefaults` | `{ latest?, branch?, tag? }`     | Lifecycle defaults by the kind of ref — see [Status](#status)  |
| `origin`         | `{ repo, ref?, path? }`          | Where pages read off disk live, for links back to them         |
| `history`        | `boolean`                        | Read this source's git history                                 |
| `content`        | `boolean`                        | Set `false` when the source supplies generated material but no Markdown pages |
| `exclude`        | `{ drafts?, partials? }`         | Override the source-relative globs used for drafts and partials |
| `flavor`         | `'tfplugindocs'`                 | Interpret the Markdown tree as a supported source dialect      |
| `locales`        | locale list                      | Languages this source is available in — see below              |
| `generated`      | section list                     | Artefacts published as pages beside the Markdown — see below   |

Two fields are easy to confuse and expensive to get wrong:

- **`repo` downloads, `origin` only links.** Naming your own repository in `repo`
  has the build clone the checkout it is already standing in.
- **`origin.path` is the folder on the forge.** Set it where the folder on disk
  is a copy — content copied into a build context, a sibling checkout read
  through `../` — so "Edit this page" opens `docs/…` rather than the copy's
  path. Locale folders map through the source's own `path`, so they have to sit
  under it.
- **`history` costs a full clone.** Content clones a remote repository with
  `--depth 1`, so its checkout holds one commit and every file looks as if the
  person who cut the tip wrote it — wrong data, not missing data. Turning this on
  unshallows the clone once. A source read off disk is a full checkout already and
  is read regardless.

**`slug` claims a segment.** A source is normally given one only once the list
names more than one repository — all or nothing, across the whole site. A source
that writes its own `slug` is served under it either way, and the sources that
write none stay where they are, which is what lets one thing sit at `/demo`
beside documentation that keeps the root.


**`name` is a name, `label` and `slug` are addresses.** The search dialog
captions each result with the source it came from, and before this key existed
it could only spell that out of the URL — so documentation served at the root
announced itself as `/`, and a source whose segment is an abbreviation announced
`tf`. `name` is display only: it reaches no collection, no prefix, no version
and no ranking, which is exactly why it may be prose and may be translated.

```ts
sources: [
  // A locale record, because the name is a noun that translates.
  {
    repo: 'acme/docs',
    path: 'docs',
    name: { 'en-GB': 'Handbook', de: 'Handbuch', fr: 'Manuel' }
  },
  // A plain literal, because a product name is the same word everywhere.
  { repo: 'acme/terraform-provider-acme', path: 'docs', slug: 'tf', name: 'Terraform Provider' }
]
```

Unset, the name falls back to the source's own segment (`demo` → **Demo**) and
then to `duxt.title`, which is itself the localized **Documentation** label on a
site that sets no title. Worth setting wherever the segment is an abbreviation.
Two different sources that would end up with one name are told apart
automatically, by their repository or their folder.

### Content, exclusions and source flavours

`content: false` keeps a source in the model without publishing ordinary
Markdown pages from its `path`. Use it when the source exists only to carry a
generated artefact. It does not disable the source's `generated` declarations.

Every source excludes draft and partial paths before Content creates its
collection. `exclude.drafts` and `exclude.partials` replace the corresponding
source-relative glob when a repository uses another convention; they do not add
a second global ignore file.

`flavor: 'tfplugindocs'` says that the Markdown follows the directory and
heading conventions produced by tfplugindocs. duxt then derives the provider
title and provider-aware category navigation instead of treating the tree as an
ordinary manual. The files are still Content pages: search, versions, locales
and machine-readable outputs use the same paths as every other source. See
[Publish Terraform provider docs](/guides/publish-terraform-provider-docs).

## Refs

A bare string is a **branch**. A tag has to say so: git keeps branches and tags
in separate namespaces, and asking for a tag under `refs/heads` fails the build
with `Could not find refs/heads/…`.

```ts
refs: [
  'main',                                     // a branch
  { branch: 'next', status: 'upcoming' },     // the same, with a lifecycle
  { tag: 'v2.0.0', label: 'v2' },             // a tag
  'latest'                                    // the newest semver tag, resolved at build
]
```

`'latest'` is reserved. A branch genuinely called `latest` needs the `{ branch }`
form.

## Releases

A source can **discover** the tags it publishes instead of listing them, which is
what keeps a long release history out of the config file. Discovery is opt-in and
always says what it selects: there is no implicit "every release", because which
releases stay reachable is a decision about a site's URLs rather than a default
someone inherits.

```ts
{
  repo: 'acme/sdk',
  path: 'docs',
  releases: { select: 'minor' }
}
```

| `select`  | Publishes                             |
| :-------- | :------------------------------------ |
| `'all'`   | every release                         |
| `'minor'` | the newest release in each minor line |
| `'major'` | the newest release in each major line |

**Pre-releases are excluded** unless `prereleases: true`. A release candidate
otherwise earns a permanent URL on the day it is tagged, and keeps it.

**The newest selected release is the default version**, exactly as the first ref
is. A `default: true` ref, or `sourceOptions.defaultRef`, still wins — discovery
supplies the fallback, it does not outrank what a maintainer wrote.

Lifecycle is unchanged: the source's own `status`, or `statusDefaults.tag`,
applies to every discovered tag. Nothing is inferred from how old a release is.

`refs` stays available beside `releases`. An entry naming a **discovered** tag
overrides that version's `label`, `status`, `default` and `locales` in place; one
naming anything else is published alongside the discovered set.

```ts
{
  repo: 'acme/sdk',
  releases: { select: 'major' },
  refs: [
    { tag: 'v1.4.2', label: 'v1', status: 'deprecated' },  // a discovered tag
    { branch: 'main', status: 'upcoming' }                 // beside them
  ]
}
```

**A selection matching no tag fails the build**, naming the source and the
selection — an empty documentation source published silently is worse than a
stopped build. Where git could not read the tags **at all** — an unreachable
remote, or a repository URL the layer refuses to run — the error says that
instead, so an outage is never reported as a configuration mistake. The same
distinction applies to `latest`.

### Component tags

A monorepo that publishes several packages on their own tags each one by
**component** — release-please writes `duxt@v0.4.0` beside
`duxt-typesense@v0.1.0`. Both shapes are versions: `latest`, `releases` and the
switcher's order read `<name>@vX.Y.Z` exactly as they read `vX.Y.Z`, and the
label and the URL segment show the version alone — `v0.4.0`, never
`duxt@v0.4.0`. The ref keeps the full tag, because that is the name git knows.

Where the repository tags more than one package, "the newest tag" means nothing
until it says whose. `tagComponent` names the package:

```ts
{
  repo: 'kirchDev/duxt',
  path: 'docs',
  tagComponent: 'duxt',
  refs: [{ tag: 'latest', default: true }]   // duxt@v…, never duxt-typesense@v…
}
```

**Plain `vX.Y.Z` tags still count** under a component: they are the package's
releases from before the repository adopted component tags, and dropping them
would unpublish every one of them. Another component's tags never do. Without
`tagComponent`, every tag that reads as a version is a candidate, prefixed or
not.

## Locales

Omitted, the source has one language and one collection. Listed, each entry
becomes a collection of its own.

```ts
locales: [
  'en-GB',                                              // the default: docs/ itself
  'de',                                                 // docs/de/
  { locale: 'fr', path: 'translations/fr' },            // somewhere else in this repo
  { locale: 'es', repo: 'acme/docs-es', path: 'docs' }  // another repository
]
```

A string is a folder inside the source's `path`. The object form overrides
`path`, `repo` and `ref` for that language alone — which is what lets a
translation live in a repository of its own, with its own maintainers and its
own release schedule.

**The default locale is the tree in `path` itself and takes no folder.** Which
one that is comes from `sourceOptions.defaultLocale`, and it has to agree with
`i18n.defaultLocale` — the build fails when they differ, because
`content.config.ts` resolves the collections without access to the Nuxt config.

A ref may carry its own `locales`, overriding the source's exactly as `status`
does. That is the usual shape: the current version is translated, the ones
behind it are not.

```ts
refs: [
  { tag: 'v2.0.0' },                     // inherits the source's locales
  { tag: 'v1.0.0', locales: ['en-GB'] }  // original only
]
```

Name a folder by LANGUAGE where a region adds nothing: `docs/pt/` serves both
`pt-PT` and `pt-BR`, because a page falls back through its base language. The
same rule the layer's own locale files follow.

## Status

`statusDefaults` supplies a lifecycle per kind of ref when the ref itself does
not name one. `latest` applies to the tag selected by the reserved `latest`
entry, `branch` to branches, and `tag` to explicit or discovered tags. An
explicit ref-level `status` wins over these defaults; the source-level `status`
is the common fallback when no kind-specific default is set.

```ts
statusDefaults: {
  latest: 'current',
  branch: 'upcoming',
  tag: 'deprecated'
}
```

| Value        | Means                                              |
| :----------- | :------------------------------------------------- |
| `upcoming`   | Not released yet — may still change                 |
| `current`    | The documentation to read                           |
| `maintained` | Older, still supported                              |
| `deprecated` | Older, and the reader should upgrade                |
| `eol`        | Dead — warned about, and out of the sitemap         |

What each one costs a page is in
[URLs and versions](/concepts/urls-and-versions).

## Generated sections

`generated` lists artefacts that are not Markdown and the type that reads each
one into pages. What the mechanism is, and the two policies a type answers, is in
[Generated sections](/concepts/generated-sections); the fields below are the same
whichever type reads them.

| Field        | Type                     | Notes                                                     |
| :----------- | :----------------------- | :--------------------------------------------------------- |
| `type`       | `string`                 | The registry key of the type — `changelog`, `openapi`, `bruno`, or your own |
| `path`       | `string`                 | The artefact, relative to the source's own root             |
| `label`      | `string`                 | The navbar entry, and — slugified — the URL segment         |
| `slug`       | `string`                 | Overrides the segment the label would produce               |
| `options`    | object                   | The knobs that type offers; each type validates its own     |
| `versions`   | version list             | The versions of this artefact — see below                   |
| `locales`    | `Record<string, string>` | An artefact per locale, read by a `per-locale` type only    |
| `navigation` | placement                | `'sections'` (the default), `'navigation'`, or `false`      |
| `icon`       | `string`                 | Falls back to the type's own                                |

**`path` is a path and never a URL.** For a source Content clones it is relative
to the root of that checkout, so the artefact travels with the version it belongs
to — one rule in both directions. An arbitrary location would let an artefact be
fetched from anywhere and reopen the build-time-network question `sources` has
already closed.

**`label` is a plain string, not a translated text**, for the same reason a
version's label is: a translated text is not a stable URL.

### A section with versions of its own

A version is normally a **checkout**: list `refs` on the source and every section
it carries is published once per ref. An API is usually not kept that way —
`openapi/v1.yaml` sits beside `openapi/v2.yaml` in one repository, versioned by
**file** — and without this key the two are two unrelated sections with two
entries in the navbar.

```ts [app/app.config.ts]
generated: [
  {
    type: 'openapi',
    label: 'API',
    path: 'openapi/v2.yaml',
    versions: [
      { version: 'v2', path: 'openapi/v2.yaml' },
      { version: 'v1', path: 'openapi/v1.yaml', status: 'deprecated' }
    ]
  }
]
```

| Field     | Notes                                                            |
| :-------- | :--------------------------------------------------------------- |
| `version` | Shown in the switcher and used in the URL                        |
| `path`    | The artefact this version reads; defaults to the declaration's    |
| `locales` | Per-locale artefacts for this version                            |
| `status`  | Lifecycle of this version — `deprecated`, `eol`, `upcoming`      |
| `default` | Served without a version segment. Defaults to the first entry     |

The default is served at `/api`, every other version under its own segment
(`/v1/api`), and the switcher offers them exactly as it offers a source's refs —
with the deprecation banner, the `noindex` and the canonical that go with a
version that is not the current one.

**Declared here rather than as two sources**, and that is the design rather than
a convenience: two sections are offered as versions of one another only when
**one declaration** produced them, and a source always publishes a documentation
tree — so a source per API version would publish the prose twice.

**Never beside a source versioned by `refs`.** Both want the same segment of the
URL, and the build says so instead of nesting them. A `global` type refuses them
too: it publishes one history at a version-neutral URL, so a list of versions is
a contradiction rather than a shape to resolve. A changelog is not one — it is
published per version, and takes a `versions` list like an API reference does.

### Which version is which

Three things are called a version around a reference page, and only two of them
are duxt's:

- **The version in the URL and the switcher** — a ref, or a `version` declared as
  above. It is what the badge in the header shows: which variant you are reading.
- **`duxt.version`** — the site's own string, shown in that badge only where
  there is nothing to switch between.
- **`info.version` of an OpenAPI document** — what the document says about the
  API it describes. It is drawn on the reference's own page, labelled **API
  version**, and never in the badge: a badge is the switcher's trigger, and a
  document's own claim is not somewhere you can switch to.

They often carry the same number, because a repository that tags `v2.0.0` usually
writes `2.1.0` in the document it tagged. They are still different facts, and
duxt derives neither from the other.

`options` is opaque to everything but the type that reads it — a granularity
means something to a changelog and nothing to an API reference. A key the type
does not know fails the build rather than being ignored, because a misspelled
option is a site quietly not getting what it configured.

## Source options

| Field         | Default          | What it does                                     |
| :------------ | :--------------- | :----------------------------------------------- |
| `showRepo`    | off              | Force a repository segment with a single repository |
| `showVersion` | off              | Force a version segment with a single version     |
| `defaultRef`  | the first ref    | The ref served without a version prefix           |
| `defaultLocale` | the first locale | The locale served from `path` itself, with no folder |

## The resolved manifest

The build turns the list above into `duxt.resolvedSources`, and that is what the
theme reads. `useDuxtCollection()` exposes it; each entry carries the collection
name, the URL `prefix`, `repo`, `version`, `name`, `isDefault`, `locale`,
`isDefaultLocale`, `status`, the repository and ref the pages came from, and
whether its `history` was read.

The locale is deliberately absent from `prefix`: i18n puts it in front of the
path already, so every language of one source shares a prefix and differs only
by collection.

## Documentation media

Place generated documentation images under `docs/_assets/`. References are relative to the Markdown file: a page at `docs/1.guides/1.demo.md` uses `../_assets/demo.gif`. A leading `/_assets/` addresses the source's documentation root. The same rule applies to the `dark` image attribute.

duxt publishes PNG, JPEG, GIF, SVG, WebP, AVIF, APNG and ICO outputs automatically, including remote sources at their selected refs. Public URLs separate repository, docs root and ref; they do not change page links or the configured image provider. `_assets/` never becomes a section or a page. Tapes, scripts, Dockerfiles and other capture inputs are not published.

Language-neutral images stay in the shared `_assets/` directory. A page in `docs/de/` may reference `../_assets/demo.gif`; an explicitly produced German variant in `docs/de/_assets/` uses `_assets/demo.gif`. Images are read from the page's own repository and ref. A locale with a separate repository can share images inside that checkout, but does not implicitly fetch media from another repository. Alt text and captions belong to the translated page.