API Changelogs with Docusaurus

Introduction
In the previous posts, we built a solid API docs setup with Docusaurus and OpenAPI, and then moved that setup behind OAuth2 Proxy for internal access control.
That solved reference docs and access control, but one important part is still missing: a reliable changelog workflow. A new OpenAPI file alone does not tell consumers what really changed, what is risky, or what they need to migrate now.
In this post, we extend the existing setup with versioned OpenAPI snapshots, openapi-diff in CI, and a dedicated changelog section in Docusaurus. The goal is practical: release notes that are based on the contract, but written so API consumers can actually use them.
Context and Scope
This guide starts where the last two posts ended: Docusaurus is your portal, OpenAPI is your contract source, and OAuth2 Proxy may protect internal routes. We are not introducing a new stack here. We are adding the missing release layer on top of the existing setup.
The scope is narrow and practical: versioned spec snapshots, automated OpenAPI diffs in CI, and a changelog section integrated next to the API reference in Docusaurus. We also extend the internal-routing pattern so changelog pages can be protected the same way as /redoc and /spec when needed.
It does not cover SDK generation, API gateway rollout mechanics, or domain-specific compatibility rules for every business area. In shared platform environments with multiple teams or partner integrations, this workflow reduces ambiguity and avoids repeated support loops around every release.
Core Concepts
Docusaurus versioning is a good choice when you want to preserve a documentation snapshot and let users switch between versions in the UI. Docusaurus snapshots your docs at the moment you cut a version, so that version can evolve independently from whatever you change next in the main docs. That is exactly what you need for stable integration guides, historical reference pages, and migration notes that must remain accurate for consumers on older releases.
But version switching alone does not tell a consumer whether a change is safe, urgent, or already time-bound. It answers “which documentation snapshot am I reading?” and not “what changed in this release, and how worried should I be?”. Those are different jobs, and treating them as the same thing is where many API portals fall short.
The second concept is separating contract evidence from consumer guidance. OpenAPI defines the contract. A diff tool such as openapi-diff compares two contract snapshots and produces objective change data. That output is necessary, but consumers still need an editorial layer that translates change data into concrete actions.
The third concept is lifecycle consistency across docs and runtime behavior. If the changelog says a feature is deprecated, runtime signals should reflect that status as well, for example through deprecation and sunset headers (RFC 9745, RFC 8594). Reliable API communication is cross-channel, not page-only.
Implementation Walkthrough
We will extend the users example from the first post. Instead of one openapi.yaml, we now keep versioned snapshots and compare them.
Create an OpenAPI spec v1.1
In the first article, we used a single file under openapi.yaml. Keep that baseline as v1.0, then create v1.1 with actual contract changes. For this post, the example specs are published as downloadable static assets:
Below is a minimal, real OpenAPI excerpt showing the relevant delta between v1.0 and v1.1. This is a good example because it includes all three major change types in one release:
openapi: 3.0.3
info:
title: Example API
version: 1.0.0
paths:
/users:
get:
summary: List users
parameters:
- in: query
name: sort
schema:
type: string
enum: [createdAt, username]
responses:
'200':
description: OK
openapi: 3.0.3
info:
title: Example API
version: 1.1.0
paths:
/users:
get:
summary: List users
parameters:
- in: query
name: sort
schema:
type: string
enum: [createdAt, username, lastLogin]
- in: query
name: legacy
deprecated: true
schema:
type: boolean
responses:
'200':
description: A list of users
content:
application/json:
schema:
type: object
properties:
users:
type: array
items:
type: object
properties:
status:
type: string
enum: [invited, active, disabled]
/users/{id}/resend-verification:
post:
summary: Resend verification email
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
'204':
description: Verification email triggered
Generate OpenAPI diff
Start locally in your terminal by using the same openapi-diff CLI that you will later use in CI to generate diff artifacts. This is a crucial step to verify that the tool works with your specs and produces the expected output before automating it in CI
curl -fsSL -o openapi-v1.0.yaml "https://www.friedrichs-it.de/downloads/blog/versioned-api-docs-with-docusaurus-changelog/v1.0/openapi.yaml"
curl -fsSL -o openapi-v1.1.yaml "https://www.friedrichs-it.de/downloads/blog/versioned-api-docs-with-docusaurus-changelog/v1.1/openapi.yaml"
curl -L -o openapi-diff.jar "https://repo1.maven.org/maven2/org/openapitools/openapidiff/openapi-diff-cli/2.1.7/openapi-diff-cli-2.1.7-all.jar"
java -jar openapi-diff.jar openapi-v1.0.yaml openapi-v1.1.yaml --markdown diff-v1.1.md --json diff-v1.1.json
mkdir -p docs/api/changelog
node <<'NODE'
const fs = require('node:fs');
const markdown = fs.readFileSync('diff-v1.1.md', 'utf8');
const sanitized = markdown.replace(/^##### `([A-Z]+)`\s+([^\r\n]+)$/gm, '##### `$1 $2`');
fs.writeFileSync('docs/api/changelog/v1-1.md', sanitized);
NODE
openapi-diff can emit headings like ##### `POST` /users/{id}/....
In MDX, {id} is parsed as a JavaScript expression and causes id is not defined.
The node step above rewrites those headings to ##### `POST /users/{id}/..., which renders correctly in Docusaurus.
Once this command works locally, move the same logic into CI so every relevant PR produces a diff artifact automatically:
name: API Diff
on:
pull_request:
paths:
- "static/downloads/blog/versioned-api-docs-with-docusaurus-changelog/**"
- "docs/**"
jobs:
diff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- name: Download openapi-diff
run: |
curl -L -o openapi-diff.jar \
https://repo1.maven.org/maven2/org/openapitools/openapidiff/openapi-diff-cli/2.1.7/openapi-diff-cli-2.1.7-all.jar
- name: Compare v1.0 vs v1.1
run: |
java -jar openapi-diff.jar \
static/downloads/blog/versioned-api-docs-with-docusaurus-changelog/v1.0/openapi.yaml \
static/downloads/blog/versioned-api-docs-with-docusaurus-changelog/v1.1/openapi.yaml \
--markdown "diff-v1.1.md" \
--json "diff-v1.1.json"
mkdir -p docs/api/changelog
node <<'NODE'
const fs = require('node:fs');
const file = "diff-v1.1.md";
const markdown = fs.readFileSync(file, 'utf8');
const sanitized = markdown.replace(/^##### `([A-Z]+)`\s+([^\r\n]+)$/gm, '##### `$1 $2`');
fs.writeFileSync('docs/api/changelog/v1-1.md', sanitized);
NODE
- name: Upload diff artifact
uses: actions/upload-artifact@v4
with:
name: openapi-diff-v1.1
path: |
diff-v1.1.json
diff-v1.1.md
The diff artifact is your technical source. The changelog page is your consumer output.
Docusaurus integration
After this step, the changelog is visible in the docs sidebar and reachable via a stable URL.
First, make sure your docs plugin points to the sidebar file you are editing:
presets: [
[
'classic',
{
docs: {
sidebarPath: './sidebars.ts', // use './sidebars.js' if your project uses JS
},
},
],
]
For the underlying behavior, see the official docs for the Docs plugin configuration (sidebarPath) and routing (/docs base paths).
Create two docs files.
api/overview is the landing page for the whole API section in the sidebar. It should link to the current spec artifacts and the changelog entry.
# API Overview
This section contains API reference context and release communication.
## Release Artifacts
- [OpenAPI v1.0](/downloads/blog/versioned-api-docs-with-docusaurus-changelog/v1.0/openapi.yaml)
- [OpenAPI v1.1](/downloads/blog/versioned-api-docs-with-docusaurus-changelog/v1.1/openapi.yaml)
## Changelog
- [API Changelog v1.1](/docs/api/changelog/v1-1)

This is the changelog page generated from the openapi-diff output:
# API Changelog v1.1
### Example API (v1.1.0)
---
#### What's New
---
##### `POST /users/{id}/resend-verification`
> Resend verification email
#### What's Changed
---
##### `GET` /users
###### Parameters:
Added: `legacy` in `query`
Changed: `sort` in `query`
###### Return Type:
Changed response: **200 OK**
> A list of users
* Changed content type : `application/json`
* Changed property `users` (array)
Changed items (object):
* Added property `status` (string)
Enum values:
* `invited`
* `active`
* `disabled`
#### Result
---
API changes are backward compatible
Now register these doc IDs in sidebars.ts:
const sidebars = {
tutorialSidebar: [
'intro',
{
type: 'category',
label: 'API',
items: [
'api/overview',
{
type: 'category',
label: 'Changelog',
items: [
'api/changelog/v1-1',
],
},
],
},
],
}
export default sidebars
After restarting Docusaurus, the changelog appears in the left docs navigation under:
API -> Changelog -> v1-1
Expected route:
/docs/api/changelog/v1-1
If you already use docs versions, this changelog page becomes part of each version snapshot when you run the version command (Docusaurus versioning).

Internal route protection
In the internal-docs setup, /redoc and /spec are protected through OAuth2 Proxy. If changelog pages contain internal release details, protect their docs route as well.
# Protect /redoc + /spec + /docs/api/changelog routes
location ~ ^/(redoc|spec|docs/api/changelog) {
auth_request /oauth2/auth;
error_page 401 = @error401;
error_page 403 = @error401;
proxy_pass http://docusaurus:3000;
}
That keeps your lifecycle communication aligned with the same access-control model introduced in the internal-docs article.
Trade-offs and Alternatives
A raw-diff-only setup is easy to automate. But it pushes interpretation to every consumer team. In practice, that creates Slack ping-pong, support load, and slower integrations.
A prose-only changelog has the opposite problem: readable, but weak as evidence. Without a diff, you cannot quickly prove completeness or detect missed breaking changes.
The pragmatic default is a hybrid model. Use machine-generated diff output as evidence. Publish consumer-facing notes as interpretation.
Where to publish is a separate decision. Blog posts are good for announcements. Docs pages are better for lifecycle work because they sit next to reference docs, version with them, and behave like release artifacts.
Production Considerations
In production, consistency is non-negotiable. If the portal says "deprecated", runtime headers, support replies, and rollout plans must say the same thing.
Treat changelog completion as a release gate. If migration steps are missing, the release is not communication-complete.
Ownership is equally important. Every deprecation entry needs an owner and a target date. Without that, sunset policies drift and trust in the portal drops.
| Area | Practical rule |
|---|---|
| Spec storage | Keep immutable OpenAPI snapshots per released version. |
| CI evidence | Persist diff artifacts for every released comparison. |
| Consumer output | Publish Added/Changed/Deprecated/Removed/Migration Steps per release. |
| Access control | Protect changelog routes where lifecycle details are internal. |
| Governance | Assign owner and sunset date for every deprecation entry. |
Conclusion
This post is the missing bridge between the previous two articles. From the first post, we keep OpenAPI-driven API docs in Docusaurus. From the second post, we keep controlled access for sensitive routes. The new part is release communication: a changelog that consumers can actually execute.
Once you have this in place, each API release becomes easier to reason about. Teams no longer compare YAML files manually to understand impact. They can read a structured changelog, follow migration steps, and trust that the portal reflects the real lifecycle state.