Skip to main content

Showing the changelog of your post

Showing the changelog of your post

· 4 min read
View raw
Christophe
Markdown, WSL and Docker lover ~ Insatiable curious.
Cet article existe aussi en français — lire la version française.

Showing the changelog of your post

⚡
TL;DR

This article demonstrates how to implement a detailed changelog for Docusaurus blog posts, extending beyond the default last_update field. It involves creating a custom React component that parses an updates array from the post's YAML frontmatter, allowing for multiple dated notes. The solution requires creating this component and then swizzling the BlogPostItem/Content theme to automatically display the update history.

As a blog author, no doubt, it happens that you'll edit previous posts to add / clarify some part. Or just remove obsolete information.

Docusaurus supports the last_update (source) entry like illustrated below, but you can only specify one date and no description.

last_update:
date: 1/1/2000
author: custom author name

Let's create a very small component to enhance this.

The Result​

Once the component is wired in (covered below), adding an updates entry to a post's frontmatter renders this, automatically:

Demo

Why It Works​

  • The component reads an updates array straight from the post's YAML frontmatter — no separate changelog file to maintain.
  • Unlike Docusaurus's native last_update, it accepts multiple entries, each with its own date and note.
  • Once swizzled in, it displays automatically on every post that has an updates entry — nothing to call manually per-article.

Installation​

We'll have to create a new component, and to swizzle the BlogPostPage, so the component will be executed automatically.

Create the Updated component​

Please create a new folder called src/components/Blog/Updated where you'll create two files: index.tsx and styles.module.css.

import type { JSX } from "react";
import clsx from "clsx";
import Translate from "@docusaurus/Translate";
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
import { parseMarkdown } from "@site/src/components/Blog/utils/markdown";
import styles from "./styles.module.css";
interface UpdateEntry {
date: string;
note: string;
}
interface Props {
updates?: UpdateEntry[];
}
export default function Updated({ updates }: Props): JSX.Element | null {
const { i18n } = useDocusaurusContext();
if (!updates || updates.length === 0) return null;
const sortedUpdates = [...updates].sort(
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
);
return (
<div className={clsx(styles.revisionHistoryContainer, "margin-top--lg")}>
<h3 className={styles.revisionHistoryTitle}>
<span aria-hidden="true">📜</span>{" "}
<Translate id="blog.updated.changelog">Changelog</Translate>
</h3>
<div className={styles.timeline}>
{sortedUpdates.map((update, i) => {
const isLatest = i === 0;
return (
<div
key={`${update.date}-${i}`}
className={clsx(
styles.timelineItem,
isLatest && styles.timelineItemLatest,
)}
>
<div className={styles.timelineIcon}>
<span aria-hidden="true">{isLatest ? "✨" : "📅"}</span>
</div>
<div className={styles.timelineContent}>
<div className={styles.timelineDate}>
<time dateTime={update.date}>
{new Date(update.date).toLocaleDateString(
i18n.currentLocale,
{ year: "numeric", month: "long", day: "numeric" },
)}
</time>
{isLatest && (
<span className={styles.latestBadge}>
<Translate id="blog.updated.latest">Latest</Translate>
</span>
)}
</div>
<div
className={styles.timelineDescription}
dangerouslySetInnerHTML={{
__html: parseMarkdown(update.note),
}}
/>
</div>
</div>
);
})}
</div>
</div>
);
}

This component displays a changelog or update history for a blog post. It takes an array of updates with dates and notes, sorts them by date (newest first), and renders them as a timeline where the most recent update is highlighted with a sparkle emoji and a 'Latest' badge. If there are no updates, it returns nothing.

/*
* Updated — the timeline structure is good and is kept as-is. What changed:
* it no longer borrows --card-radius from PostCard's module (see that file),
* the timeline node is the shared .mk-dot, and the title is an eyebrow rather
* than a 1.1rem bold heading competing with the article's own.
*/
.revisionHistoryContainer {
composes: mk-surface from global;
padding: 1.25rem 1.375rem;
}
.revisionHistoryTitle {
composes: mk-eyebrow from global;
display: flex;
gap: 0.5rem;
align-items: center;
margin: 0 0 1rem;
}
.timeline {
position: relative;
}
/* The connecting line: centred on the 26px node (13px) rather than the old
* 30px one (15px). */
.timeline::before {
position: absolute;
top: 4px;
left: 13px;
width: 2px;
height: calc(100% - 8px);
content: "";
background: var(--mk-border);
}
.timelineItem {
position: relative;
display: flex;
min-height: 26px;
margin-bottom: 1.25rem;
}
.timelineItem:last-child {
margin-bottom: 0;
}
.timelineIcon {
composes: mk-dot from global;
position: absolute;
top: 0;
left: 0;
z-index: 1;
color: var(--mk-on-line);
background: var(--mk-n-300);
}
/* The latest revision is the only filled terracotta node, with a halo in the
* key tone — the halo used to be a color-mix() of the primary at 15%. */
.timelineItemLatest .timelineIcon {
background: var(--mk-primary);
box-shadow: 0 0 0 4px var(--mk-key-bg);
}
.timelineContent {
flex: 1;
padding-top: 0.15rem;
padding-left: 40px;
}
.timelineDate {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
margin-bottom: 0.3rem;
font-size: 0.85rem;
font-weight: 600;
color: var(--mk-ink-soft);
font-variant-numeric: tabular-nums;
}
.timelineItemLatest .timelineDate {
color: var(--mk-primary);
}
.latestBadge {
composes: mk-pill from global;
}
.timelineDescription {
font-size: 0.95rem;
line-height: 1.6;
color: var(--mk-ink);
}

This stylesheet styles a revision history timeline component. It creates a vertical timeline with connected dots representing different revisions, where the latest revision gets special styling with a filled dot and a glowing halo. The layout uses flexbox to position content next to timeline nodes, and CSS custom properties to theme colors consistently.

Shortly, the component will check if the YAML frontmatter of your blog post contains an updates entry. If this is the case, it will parse it. It has to be an array with two pieces of information: a date and a note.

Override the BlogPostPage template​

Please first check if you don't have the src/theme/BlogPostItem/Content/index.js file yet. If you're a reader of this blog and already have customized Docusaurus, perhaps you have it already: the "this article is over a year old" component swizzles that very same file.

If not, please start a console and run yarn swizzle @docusaurus/theme-classic BlogPostItem/Content.

From now, you'll have a new file on your disk: src/theme/BlogPostItem/Content/index.js.

We'll have to inject our new Updated component, please edit the index.js file to inject the required code.

Below are the minimum lines to add at the right place in your own file.

// [...]
import Updated from "@site/src/components/Blog/Updated";
export default function BlogPostContent({ children }) {
// [...]
const { metadata, isBlogPostPage } = useBlogPost();
// [...]
return (
<div>
// [...]
{isBlogPostPage && (
<>
// [...]
<Updated updates={metadata.frontMatter.updates} />
</>
)}
// [...]
</div>
);
}

This React component renders blog post content and conditionally displays an 'Updated' component when viewing a full blog post page. It retrieves metadata about the blog post using a custom hook and passes update information down to a child component that shows when the post was last modified.

More Demos​

Use the component​

Now, just edit any of your existing posts and add an updates entry in your YAML front matter like for instance

updates:
- date: 2024-11-19
note: review Dockerfile, use Quarto 1.6.36.
- date: 2024-12-01
note: Added new theme options for blog posts.
- date: 2024-12-15
note: Fixed bug with image rendering on mobile devices.
- date: 2025-01-05
note: Implemented search functionality for documentation pages.
- date: 2025-01-20
note: Optimized build process for faster deployment times.

Just save the post and refresh your web page. If you've done everything correctly, you'll see the information in your browser — the same rendering shown at the top of this article.

Feel free to update the src/components/Blog/Updated/styles.module.css to match your own styling preferences.

Conclusion​

A small parsed frontmatter array and one swizzled theme file replace last_update's single date with a real, multi-entry changelog — readers see exactly what changed and when, without you maintaining a separate file.

It pairs nicely with the banner warning readers that an article is over a year old: the first says what changed, the second says when it last did.


🦋 Join the conversation on BlueskyRequires a free Bluesky account to like, repost or comment. This block talks to Bluesky's public API directly from your browser (no cookies, no sign-in) — that means your IP address reaches Bluesky's servers just by this section loading.

Loading comments…

Was this article helpful?
Did you try the steps in this article?