---
name: web-meta-framework-docusaurus
description: Docusaurus 3.x documentation framework — site configuration, docs/blog plugins, sidebars, versioning, MDX, swizzling, and deployment
---

# 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-classic` bundles docs, blog,
> pages, sitemap and theme in one entry. Sidebars are autogenerated from the filesystem, ordered by
> `sidebar_position` front matter and `_category_.json`. Theme components are customised by swizzling.
> Content is MDX v3, which is stricter than Markdown. `docusaurus docs:version` snapshots the whole
> `docs/` tree, and
> `docusaurus build` emits a static `build/` directory.

**Detailed Resources:**

- [examples/core.md](examples/core.md) — full config, autogenerated and manual sidebars, multi-instance docs, docs-only mode, custom pages, navbar item types
- [examples/content.md](examples/content.md) — MDX strictness, admonitions, tabs, code blocks, assets, blog plugin and authors, remark/rehype, doc links
- [examples/customization.md](examples/customization.md) — swizzle safety, Infima CSS variables, versioning, i18n, search, deployment, Mermaid
- [reference.md](reference.md) — CLI commands, front matter fields, plugin and theme option tables, import aliases

---

<critical_requirements>

## 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.

</critical_requirements>

---

**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 build` emits 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.

---

<philosophy>

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.

1. **Convention over configuration** — the filesystem drives routing and sidebar generation. Fighting it
   is fighting the framework.
2. **Preset first** — `preset-classic` is the common plugin set already wired together; individual
   plugins are for setups it cannot express.
3. **Content is data** — front matter is the metadata layer. `sidebar_position`, `slug`, `tags` and
   `custom_edit_url` live in the document rather than in a separate index.
4. **Swizzle rather than fork** — wrapping preserves upstream compatibility; ejecting produces a
   snapshot with your name on it.
5. **Static output** — there is no server runtime, no request-time rendering and no API routes.

</philosophy>

---

<decision_framework>

**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.

</decision_framework>

---

<patterns>

## 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.

```javascript
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](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.

```javascript
// sidebars.js
export default { docs: [{ type: "autogenerated", dirName: "." }] };
```

```json
// 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](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.

```bash
npx docusaurus swizzle --list
npx docusaurus swizzle @docusaurus/theme-classic Footer -- --wrap
```

```jsx
// 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](examples/customization.md)

### Pattern 4: MDX content

MDX v3 is Markdown with JSX. Admonitions, tabs and code block metadata are the Docusaurus-specific
additions.

```mdx
---
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](examples/content.md)

### Pattern 5: Versioning

`docs:version` snapshots the whole `docs/` directory. What remains in `docs/` becomes the unreleased
`current` version.

```bash
npx docusaurus docs:version 1.0.0
```

```javascript
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](examples/customization.md)

</patterns>

---

<red_flags>

## 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/ComponentName` imported inside a swizzle wrapper — the wrapper resolves to itself, and the import loops.
- `baseUrl` without a leading and trailing `/` — asset resolution breaks.
- `_category_.json` written with `name` or `order` instead of `label` and `position` — 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: "/"` but `blog` left enabled, or `src/pages/index.js` left in place — the routes collide.

**Surprising behaviour:**

- `onBrokenLinks` set to `"ignore"` or `"log"` ships dead links; `"throw"` is what a production config wants, with `onBrokenMarkdownLinks` at least at `"warn"`.
- Ejecting where wrapping would have done means upstream fixes stop arriving for that component.
- A hand-maintained `sidebars.js` where autogeneration would work drifts out of step with the docs it describes.
- No `editUrl` means no "Edit this page" link, and no `showLastUpdateTime` means a reader cannot tell how stale a page is.
- Changes to `docusaurus.config.js` are not hot-reloaded — restart the dev server.
- With `format: "detect"` configured, only `.mdx` files process JSX; the default mode processes both extensions.
- Static assets in `static/` are served from the site root rather than from `baseUrl` — use `require()` or `useBaseUrl()` for a path-safe reference.
- Blog authors live in `blog/authors.yml`, not in the site config.
- The `slug` front 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.tsx` becomes `/support`.
- `<Tabs>` instances do not sync until they share a `groupId`.

</red_flags>
