Docusaurus Patterns
Quick Guide: Docusaurus 3.x is a React-powered static site generator for documentation. Everything is configured in
docusaurus.config.js(ESM), and@docusaurus/preset-classicbundles docs, blog, pages, sitemap and theme in one entry. Sidebars are autogenerated from the filesystem, ordered bysidebar_positionfront matter and_category_.json. Theme components are customised by swizzling. Content is MDX v3, which is stricter than Markdown.docusaurus docs:versionsnapshots the wholedocs/tree, anddocusaurus buildemits a staticbuild/directory.
Detailed Resources:
- examples/core.md — full config, autogenerated and manual sidebars, multi-instance docs, docs-only mode, custom pages, navbar item types
- examples/content.md — MDX strictness, admonitions, tabs, code blocks, assets, blog plugin and authors, remark/rehype, doc links
- examples/customization.md — swizzle safety, Infima CSS variables, versioning, i18n, search, deployment, Mermaid
- reference.md — CLI commands, front matter fields, plugin and theme option tables, import aliases
Before writing Docusaurus code
Keep all site configuration in docusaurus.config.js (or .ts). It is the single entry point the
build reads, and config split across files has no mechanism to be merged.
Start from @docusaurus/preset-classic. It wires docs, blog, pages, sitemap and the theme together;
decompose into individual plugins only when you need something the preset cannot express, such as a
second docs instance.
Swizzle with --wrap unless the change genuinely needs the component's internals. A wrapped
component keeps receiving upstream fixes; an ejected one is a snapshot you now own.
Order autogenerated sidebars with sidebar_position front matter and _category_.json. The
filesystem drives routing and sidebar structure, so ordering stays next to the content it orders.
Give each doc set its own plugin instance when it needs its own versioning. One instance holding both versioned and unversioned docs has no way to keep them apart.
Auto-detection: Docusaurus, docusaurus.config.js, docusaurus.config.ts, @docusaurus/preset-classic, @docusaurus/core, sidebars.js, docs:version, docusaurus build, docusaurus start, docusaurus deploy, docusaurus swizzle, MDX, category.json, sidebar_position, @site, @theme, @theme-original, plugin-content-docs, plugin-content-blog
Applies to:
docusaurus.config.js— site metadata, presets, plugins,themeConfig, navbar, footer- Sidebars: autogenerated, manual, multiple, and custom item generators
- Docs versioning and version banners
- Theme customization via swizzling and Infima CSS variables
- MDX content: admonitions, tabs, code block features, assets, heading anchors
- Custom pages in
src/pages/, in React or MDX - Blog plugin configuration and
authors.yml - Search wiring, i18n, and deploying the static build
Handled elsewhere:
- React component authoring itself — this skill covers the Docusaurus APIs a page or swizzled component calls, not the component model underneath.
- Request-time behaviour —
docusaurus buildemits static files, so anything needing a server at request time lives outside the site. - Content sourced from a CMS or database — the plugins read files from disk at build time.
- The visual language beyond the Infima variables the theme exposes — those variables are the seam, and what you set them to is a design decision.
- The prose itself — this skill settles how a doc is wired into the site, not what it says.
Docusaurus is an opinionated documentation framework that trades flexibility for convention. It decides routing (filesystem), content format (MDX) and structure (docs, blog, pages) so the work left is writing.
- Convention over configuration — the filesystem drives routing and sidebar generation. Fighting it is fighting the framework.
- Preset first —
preset-classicis the common plugin set already wired together; individual plugins are for setups it cannot express. - Content is data — front matter is the metadata layer.
sidebar_position,slug,tagsandcustom_edit_urllive in the document rather than in a separate index. - Swizzle rather than fork — wrapping preserves upstream compatibility; ejecting produces a snapshot with your name on it.
- Static output — there is no server runtime, no request-time rendering and no API routes.
Which sidebar strategy? Autogenerated, ordered by sidebar_position and _category_.json, is right
for almost everything — including large sites, as long as the sections map onto directories. A manual
sidebar in sidebars.js earns its maintenance only when the navigation has to group documents the
filesystem keeps apart, and the choice is not all-or-nothing: one sidebar array can hold an
autogenerated item alongside hand-written entries. Independent doc sets (an API reference beside a
guide) want separate plugin instances, each with its own sidebar, rather than either strategy stretched.
Wrap, eject, or neither? Adding content around a component is --wrap. Changing colours or spacing
is neither — set the Infima CSS variables in custom.css. Changing internal logic means checking the
component's safety level first: "Safe" makes ejecting acceptable, "Unsafe" means wrap or find another
route, and "Forbidden" means the component is not swizzlable at all.
Which content format? A documentation article or blog post is .md or .mdx in docs/ or blog/.
A standalone page that is mostly prose is .mdx in src/pages/; one that is mostly interactive is
.tsx in the same place. Embedding React components in documentation means .mdx with imports.
Version, or not? Versioning copies the entire docs/ tree, so each version costs its own build time
and output size. Cut a version when the documentation genuinely differs between releases — not on every
release.
Core patterns
Pattern 1: docusaurus.config.js
One ESM file: site metadata, the preset (which brings the plugins and theme), and themeConfig for
navbar, footer and theme behaviour.
export default {
title: "My Docs",
url: "https://docs.example.com",
baseUrl: "/",
onBrokenLinks: "throw",
presets: [
[
"@docusaurus/preset-classic",
{
docs: {
sidebarPath: "./sidebars.js",
editUrl: "...",
showLastUpdateTime: true,
},
blog: { showReadingTime: true },
theme: { customCss: ["./src/css/custom.css"] },
},
],
],
themeConfig: {
navbar: {
/* ... */
},
footer: {
/* ... */
},
},
};
onBrokenLinks: "throw" is what makes a broken internal link fail the build rather than ship.
Full code: examples/core.md
Pattern 2: Sidebars
An autogenerated sidebar reads the filesystem; ordering and labels come from front matter and
_category_.json beside the content.
// sidebars.js
export default { docs: [{ type: "autogenerated", dirName: "." }] };
// docs/guides/_category_.json
{
"label": "Guides",
"position": 2,
"collapsed": false,
"link": { "type": "generated-index" }
}
Without sidebar_position, items sort alphabetically by filename. Number prefixes (01-intro.md) are
the alternative and are stripped from the URL slug — pick one mechanism, not both.
Full code: examples/core.md
Pattern 3: Swizzling theme components
A wrapper renders the original and adds around it. The import is @theme-original/ — @theme/ would
resolve to the wrapper itself.
npx docusaurus swizzle --list
npx docusaurus swizzle @docusaurus/theme-classic Footer -- --wrap
// src/theme/Footer/index.js
import Footer from "@theme-original/Footer";
export default function FooterWrapper(props) {
return (
<>
<Footer {...props} />
<div className="custom-banner">Custom content below footer</div>
</>
);
}
Full code: examples/customization.md
Pattern 4: MDX content
MDX v3 is Markdown with JSX. Admonitions, tabs and code block metadata are the Docusaurus-specific additions.
---
title: My Document
sidebar_position: 1
---
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
:::tip[Pro Tip]
`note`, `tip`, `info`, `warning` and `danger`; the bracket sets a custom title.
:::
<Tabs groupId="package-manager">
<TabItem value="npm" label="npm" default>
...
</TabItem>
</Tabs>
MDX v3 is stricter than Markdown: an unclosed HTML tag, a bare {, or a < used as a comparison in
prose all fail the build.
Full code: examples/content.md
Pattern 5: Versioning
docs:version snapshots the whole docs/ directory. What remains in docs/ becomes the unreleased
current version.
npx docusaurus docs:version 1.0.0
docs: {
lastVersion: "current",
versions: {
current: { label: "2.0.0-beta", path: "next", banner: "unreleased" },
"1.0.0": { label: "1.0.0", path: "1.0.0", banner: "none" },
},
onlyIncludeVersions: ["current", "1.0.0"],
}
Versions are full copies rather than diffs, so onlyIncludeVersions is how you keep development builds
fast.
Full code: examples/customization.md
Red flags
Breaks at runtime:
- A bare
{or an unclosed tag in MDX — MDX v3 fails the build; escape with\{and\<, or use a code fence. @theme/ComponentNameimported inside a swizzle wrapper — the wrapper resolves to itself, and the import loops.baseUrlwithout a leading and trailing/— asset resolution breaks._category_.jsonwritten withnameororderinstead oflabelandposition— the fields are ignored and the category falls back to defaults.- A direct
@docusaurus/-package import inside MDX — reach for the@theme/and@site/aliases instead. - Docs-only mode configured with
routeBasePath: "/"butblogleft enabled, orsrc/pages/index.jsleft in place — the routes collide.
Surprising behaviour:
onBrokenLinksset to"ignore"or"log"ships dead links;"throw"is what a production config wants, withonBrokenMarkdownLinksat least at"warn".- Ejecting where wrapping would have done means upstream fixes stop arriving for that component.
- A hand-maintained
sidebars.jswhere autogeneration would work drifts out of step with the docs it describes. - No
editUrlmeans no "Edit this page" link, and noshowLastUpdateTimemeans a reader cannot tell how stale a page is. - Changes to
docusaurus.config.jsare not hot-reloaded — restart the dev server. - With
format: "detect"configured, only.mdxfiles process JSX; the default mode processes both extensions. - Static assets in
static/are served from the site root rather than frombaseUrl— userequire()oruseBaseUrl()for a path-safe reference. - Blog authors live in
blog/authors.yml, not in the site config. - The
slugfront matter field overrides the URL derived from the filename, which is how a rename keeps its URL. - A page in
src/pages/takes its route from its file path —src/pages/support.tsxbecomes/support. <Tabs>instances do not sync until they share agroupId.