Skip to content
duxt

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

FieldTypeNotes
pathstringFolder holding the Markdown, relative to the repository root
repostringowner/name or a git URL. Omitted means the local checkout
refsref listRefs to publish as versions; requires an explicit repo
releases{ select, prereleases? }Discover release tags to publish as versions — see below
tagComponentstringPublish only one package's <name>@vX.Y.Z tags — see Component tags
versionstringThe version this source is, where no ref names one
labeltextShown in the switcher and used in the URL; defaults to the ref
nametextWhat to call this source where the site names it. Display only — see below
slugstringThis source's URL segment. Naming one claims it — see below
statusstatusLifecycle 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
historybooleanRead this source's git history
contentbooleanSet 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
localeslocale listLanguages this source is available in — see below
generatedsection listArtefacts 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.

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' }
}
selectPublishes
'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'
}
ValueMeans
upcomingNot released yet — may still change
currentThe documentation to read
maintainedOlder, still supported
deprecatedOlder, and the reader should upgrade
eolDead — 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.

FieldTypeNotes
typestringThe registry key of the type — changelog, openapi, bruno, or your own
pathstringThe artefact, relative to the source's own root
labelstringThe navbar entry, and — slugified — the URL segment
slugstringOverrides the segment the label would produce
optionsobjectThe knobs that type offers; each type validates its own
versionsversion listThe versions of this artefact — see below
localesRecord<string, string>An artefact per locale, read by a per-locale type only
navigationplacement'sections' (the default), 'navigation', or false
iconstringFalls 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' }
    ]
  }
]
FieldNotes
versionShown in the switcher and used in the URL
pathThe artefact this version reads; defaults to the declaration's
localesPer-locale artefacts for this version
statusLifecycle of this version — deprecated, eol, upcoming
defaultServed 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

FieldDefaultWhat it does
showRepooffForce a repository segment with a single repository
showVersionoffForce a version segment with a single version
defaultRefthe first refThe ref served without a version prefix
defaultLocalethe first localeThe 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.

Was this page helpful?