Sources
The fields of a source entry, its refs, and the options that shape the URLs.
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.
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 |
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 |
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:
repodownloads,originonly links. Naming your own repository inrepohas the build clone the checkout it is already standing in.origin.pathis 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" opensdocs/…rather than the copy's path. Locale folders map through the source's ownpath, so they have to sit under it.historycosts 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.
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.
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/….
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.
{
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.
{
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:
{
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.
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.
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.
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.
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; 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.
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
versiondeclared 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.versionof 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.