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.
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
classicpreset. - 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 production — baseUrl 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.
Related
- Developer Portal Frameworks & UI Setup — the parent overview of portal options.
- Integrating Docusaurus with OpenAPI Specs — the detailed spec-binding walkthrough.
- Mintlify vs Docusaurus for API Teams — choosing between the two.
- Mintlify Setup & Migration — the hosted alternative.
- Multi-Theme & Dark Mode Support — token-based theming across frameworks.