Docusaurus for API Portals: Setup, OpenAPI Integration & CI

Docusaurus v3 gives platform teams a React-based static-site foundation that doubles as an interactive API reference. This guide is part of Developer Portal Frameworks & UI Setup, and it focuses specifically on running Docusaurus as a spec-driven portal: wiring docusaurus-plugin-openapi-docs, generating MDX from an OpenAPI document, versioning the reference, and keeping everything synchronized through CI. It does not cover generic Docusaurus blogging features or non-API content systems — for those, the upstream framework docs are sufficient.

The work that actually breaks builds is rarely the framework itself. It is spec drift, plugin/peer-dependency mismatches across Docusaurus major versions, and stale generated MDX checked into the repository. The patterns below treat the OpenAPI document as the source of truth and regenerate the reference deterministically on every change, so the portal never lags behind the contract.

Docusaurus API portal pipeline An OpenAPI spec is linted, then gen-api-docs emits MDX, the theme renders it, and the static build deploys. openapi.yaml source of truth redocly lint fail fast gen-api-docs MDX per op docusaurus build deploy Spec-to-portal pipeline

Prerequisites & Environment Setup

Pin versions deliberately — docusaurus-plugin-openapi-docs tracks Docusaurus majors closely, and an unpinned upgrade is the single most common cause of a broken regeneration.

  • Node.js 18.0+ (Docusaurus v3 dropped Node 16). Node 20 LTS is the safe baseline.
  • A Docusaurus v3 site scaffolded with the classic preset.
  • An OpenAPI 3.0.x or 3.1.x document. Swagger 2.0 must be converted first.
# Scaffold a fresh site if you do not have one
npx create-docusaurus@latest portal classic --typescript

cd portal

# Install the OpenAPI plugin + its rendering theme, pinned
npm install docusaurus-plugin-openapi-docs@^4.4.0 \
            docusaurus-theme-openapi-docs@^4.4.0

# Validate the spec toolchain up front
npx @redocly/cli@latest --version   # expect 2.x

Place the spec under a stable directory the plugin can find:

portal/
├─ openapi/
│  └─ api.yaml          # root spec; external $ref files live alongside
├─ docs/                # hand-written guides + generated api/ output
├─ docusaurus.config.js
└─ sidebars.js

If you are converting from Swagger 2.0, run the conversion once and commit the OpenAPI output rather than converting in CI:

npx swagger2openapi swagger.yaml -o openapi/api.yaml

Core Configuration

The plugin reads the spec and emits MDX; the theme supplies the React components that render those MDX files (request builder, schema tables, try-it-out panel). Both must be registered in docusaurus.config.js. Every non-obvious key is annotated inline.

// docusaurus.config.js
// @ts-check
/** @type {import('@docusaurus/types').Config} */
module.exports = {
  title: 'API Portal',
  url: 'https://docs.example.com',
  baseUrl: '/',                       // MUST match the deploy subdirectory or version routes 404

  presets: [
    [
      'classic',
      /** @type {import('@docusaurus/preset-classic').Options} */
      ({
        docs: {
          // The OpenAPI theme replaces the default doc item renderer
          docItemComponent: '@theme/ApiItem',
        },
      }),
    ],
  ],

  plugins: [
    [
      'docusaurus-plugin-openapi-docs',
      {
        id: 'api',                    // plugin instance id, referenced by gen-api-docs
        docsPluginId: 'classic',      // which docs instance the output belongs to
        config: {
          petstore: {                 // an arbitrary key naming this spec
            specPath: 'openapi/api.yaml',
            outputDir: 'docs/api',    // generated MDX lands here
            sidebarOptions: {
              groupPathsBy: 'tag',    // one sidebar category per OpenAPI tag
              categoryLinkSource: 'tag',
            },
            downloadUrl: '/openapi/api.yaml',  // exposes the raw spec for download
            hideSendButton: false,    // keep try-it-out enabled
          },
        },
      },
    ],
  ],

  themes: ['docusaurus-theme-openapi-docs'],
};

Generated MDX should be treated as build output. Add docs/api/ to .gitignore and regenerate it in CI rather than committing it — committed output is the second most common source of drift after unpinned plugins.

# Generate one MDX file per operation, grouped by tag
npm run docusaurus gen-api-docs api

# Wipe generated output before a clean regeneration
npm run docusaurus clean-api-docs api

Reference external $ref files from the root document to keep api.yaml readable. The plugin resolves them at generation time, so a missing target fails gen-api-docs rather than the later build — fail-fast is the goal.

Integration Pattern

The portal should rebuild deterministically whenever the spec or the hand-written guides change. The workflow below lints the contract, regenerates the reference, builds the static site, and deploys to GitHub Pages. It fails fast: a lint violation stops the run before any broken MDX is produced. This is the same fail-fast posture used across the Redocly & OpenAPI UI Configuration guide.

# .github/workflows/portal-build.yml
name: Build API Portal
on:
  push:
    branches: [main]
    paths: ['openapi/**', 'docs/**', 'docusaurus.config.js']
permissions:
  contents: write          # required for the gh-pages deploy step
jobs:
  validate-and-build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - name: Install dependencies
        run: npm ci
      - name: Validate OpenAPI            # fail fast on a malformed contract
        run: npx @redocly/cli@latest lint openapi/api.yaml
      - name: Regenerate API docs         # deterministic output, not committed
        run: |
          npm run docusaurus clean-api-docs api
          npm run docusaurus gen-api-docs api
      - name: Build site
        run: npm run build
      - name: Deploy to GitHub Pages
        if: github.ref == 'refs/heads/main'
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./build

For a deeper, end-to-end walkthrough of binding the spec — including multi-spec setups and per-tag sidebar tuning — see Integrating Docusaurus with OpenAPI Specs.

Advanced Options

Versioned API references

Run the versioning command to snapshot the current docs/ tree into versioned_docs/version-<label>/. Docusaurus then routes each version under its own prefix and renders a version dropdown automatically.

npm run docusaurus docs:version 2.0

The sidebar for each version lives in versioned_sidebars/version-2.0-sidebars.json:

{
  "tutorialSidebar": [
    {
      "type": "category",
      "label": "API Reference",
      "items": ["api/overview", "api/users", "api/orders"]
    }
  ]
}

Docusaurus manages version routing itself — do not hand-roll a versioned_sidebars.js that reads versions.json at runtime. Regenerate API MDX into the live (unversioned) tree, then cut a version only on a breaking release.

Theming with CSS variables and selective swizzling

Theming is layered: global CSS custom properties (Infima --ifm-* variables) control most of the palette, and component swizzling replaces individual React components only when variables are insufficient.

/* src/css/custom.css */
:root {
  --ifm-color-primary: #16306d;
  --ifm-font-family-monospace: 'JetBrains Mono', monospace;
}
[data-theme='dark'] {
  --ifm-color-primary: #4c9aff;
  --ifm-background-color: #0b1120;
}

Keep dark-mode parity for the reference UI itself; the token strategy is covered in depth in Multi-Theme & Dark Mode Support. Swizzle the OpenAPI theme’s components with npm run swizzle docusaurus-theme-openapi-docs <Component> -- --eject only when a CSS variable cannot achieve the result, and validate every override against WCAG 2.1 AA contrast (4.5:1 for normal text).

Splitting large specs for faster regeneration

Split a monolithic spec into per-resource files referenced by $ref and commit the fragments alongside the root. The plugin only re-emits MDX for operations whose source changed, so a focused edit regenerates a handful of files instead of the whole reference.

Verification & Testing

Confirm the pipeline locally before trusting CI:

# 1. Lint the contract — expect "No errors or warnings found"
npx @redocly/cli@latest lint openapi/api.yaml

# 2. Regenerate and count emitted files
npm run docusaurus gen-api-docs api
ls docs/api | wc -l          # one .mdx per operation + category metadata

# 3. Build and check for broken links (Docusaurus fails the build on broken links)
npm run build                # onBrokenLinks defaults to 'throw'

# 4. Serve the production build and spot-check try-it-out
npm run serve                # http://localhost:3000

A clean run prints [SUCCESS] Generated static files in "build". with no broken-link warnings. In the served site, open a tagged operation, expand its schema table, and execute a try-it-out request against a reachable endpoint to confirm the theme components hydrated.

Troubleshooting

gen-api-docs fails with “Could not resolve reference” — A $ref in api.yaml points to a missing or mistyped file. Run npx @redocly/cli lint openapi/api.yaml first; it reports the exact unresolved pointer and line. Fix the path relative to the root document, not relative to the referencing fragment.

Version routes return 404 in productionbaseUrl in docusaurus.config.js does not match the deploy subdirectory. If the site is served from https://docs.example.com/portal/, set baseUrl: '/portal/'. A mismatch breaks every versioned and asset URL even though local npm run serve works.

Plugin API errors after a Docusaurus upgrade — A major Docusaurus bump deprecated a plugin API while docusaurus-plugin-openapi-docs and docusaurus-theme-openapi-docs lag behind. Keep both packages pinned to the same minor line and matching the Docusaurus major; check the plugin changelog for the required peer-dependency range before upgrading either.

Stale reference served after a spec change — Generated MDX was committed and never regenerated, or a CDN cached index.html. Remove docs/api/ from version control, regenerate in CI, and set Cache-Control: no-cache on index.html while keeping Docusaurus’s content-hashed asset filenames for everything else.

FAQ

How does Docusaurus handle multiple API versions?

Docusaurus snapshots the entire docs/ tree when you run npm run docusaurus docs:version <label>, giving each version an isolated URL prefix such as /docs/2.0/api/. You can host legacy and current references in parallel and cut a new version on every breaking release.

Can I use Docusaurus without OpenAPI specs?

Yes. Docusaurus renders standard Markdown and MDX for conceptual guides, tutorials, and architecture pages. The OpenAPI plugin is additive and is not required to run a Docusaurus site.

Why does gen-api-docs fail with a $ref resolution error?

The plugin resolves external $ref pointers at generation time, so a missing or mistyped file path halts the build. Run a lint pass over the bundled spec first and confirm every $ref target exists relative to the root document.

Is Docusaurus suitable for enterprise SSO portals?

Docusaurus produces a static site, so authentication is enforced at the hosting or reverse-proxy layer rather than inside the app. Front the build with Cloudflare Access, signed CloudFront URLs, or an Nginx auth_request handler to gate access.