Every web app should have a changelog. Not because it's required for launch, but because users who see regular updates churn less. They know the product is alive.
Here's how to add one, whether you want to build it yourself or use a hosted tool.
Option 1: The hosted widget (5 minutes)
If you want an embeddable, in-app widget, the thing that shows a little dot notification when there's a new update, you'll want a hosted tool. Building one from scratch that works reliably across different browsers, handles notification state, and looks good is more work than it sounds.
Patchlog offers this on its free plan. Here's how to add it:
Step 1: Create a project
Sign up at patchlog.io and create a project. Give it a name and slug (e.g., my-app).
Step 2: Get the embed snippet
From your project settings, copy the embed script. It looks like this:
<script
src="https://patchlog.io/widget.js"
data-project="YOUR_PROJECT_ID"
defer
></script>
One thing worth being precise about, because it fails silently if you get it
wrong: data-project takes the project ID, not the slug you chose in step 1.
The slug is what appears in your hosted changelog URL; the ID is what the widget
looks up. Copy the snippet from your project's widget settings page rather than
typing it by hand and the right value is already filled in.
Step 3: Add it to your app
Paste it just before the closing </body> tag in your HTML. In React or Vue, you can add it to your main layout component or use useEffect/onMounted to inject it:
React:
import { useEffect } from 'react';
export function Layout({ children }) {
useEffect(() => {
const script = document.createElement('script');
script.src = 'https://patchlog.io/widget.js';
script.setAttribute('data-project', 'YOUR_PROJECT_ID');
script.defer = true;
document.body.appendChild(script);
return () => document.body.removeChild(script);
}, []);
return <div>{children}</div>;
}
Vue 3:
<script setup>
import { onMounted, onUnmounted } from 'vue';
let script;
onMounted(() => {
script = document.createElement('script');
script.src = 'https://patchlog.io/widget.js';
script.setAttribute('data-project', 'YOUR_PROJECT_ID');
script.defer = true;
document.body.appendChild(script);
});
onUnmounted(() => script?.remove());
</script>
Laravel Blade / plain HTML:
<script
src="https://patchlog.io/widget.js"
data-project="YOUR_PROJECT_ID"
defer
></script>
Step 4: Post your first entry
Back in Patchlog, hit "New entry," write something short describing what you shipped, pick a category (Feature, Fix, Improvement), and publish.
The widget will show a notification dot the next time any user loads your app.
That's it.
Option 2: A dedicated /changelog page (no widget, 10 minutes)
If you don't want a widget, and would rather users found the changelog via search or you just want something simple, a static page is fine.
Using Markdown files (no database required)
Create a changelog/ directory in your project. Each entry is a Markdown file:
## 2025-03-08: Bulk order actions
You can now select multiple orders and mark them shipped in one click.
Previously you had to do this one at a time, which was terrible. Sorry.
**Also fixed:**
- Orders with special characters in the customer name no longer break the export
- Fixed a timezone bug where orders created near midnight showed the wrong date
Then serve them in your app. In Laravel:
Route::get('/changelog', function () {
$files = collect(glob(resource_path('changelog/*.md')))
->sortByDesc(fn($f) => $f)
->map(fn($f) => Str::markdown(file_get_contents($f)));
return view('changelog', ['entries' => $files]);
});
In Next.js:
import fs from 'fs';
import path from 'path';
import { marked } from 'marked';
export async function getStaticProps() {
const dir = path.join(process.cwd(), 'changelog');
const files = fs.readdirSync(dir).sort().reverse();
const entries = files.map(file => {
const raw = fs.readFileSync(path.join(dir, file), 'utf-8');
return marked(raw);
});
return { props: { entries } };
}
The trade-offs
Building your own:
- ✅ Full control over design
- ✅ No third-party dependency
- ❌ No widget/notification system
- ❌ No user feedback or reactions
- ❌ Won't show up in-app without extra work
Hosted tool:
- ✅ Widget and notifications built-in
- ✅ No code to maintain
- ✅ Usually has a public SEO-friendly page too
- ❌ Small monthly cost (typically $0-$10/month for basic plans)
- ❌ Dependency on a third-party service
What to write in your first entry
Keep it short. A changelog entry is not a blog post.
Good format:
## Feature: Dashboard redesign
The main dashboard has been redesigned to load faster and show
the metrics you actually care about first.
Click "Dashboard" to see the new layout. Your old layout
settings are preserved.
What to avoid:
- Jargon that users won't understand ("refactored the event emitter layer")
- Entries about internal changes that don't affect users
- Entries that are so vague they could mean anything ("various improvements")
The 10-minute challenge
Here's the plan:
- Minute 1-2: Sign up for a changelog tool (or create your Markdown directory)
- Minute 3-5: Get the embed snippet and add it to your app layout
- Minute 6-8: Write your first changelog entry about the last thing you shipped
- Minute 9-10: Deploy
If you've been putting it off, this is the post to stop doing that. Your users are wondering if your product is still being maintained. Tell them it is.