Apps (Coming Soon) 30 / 32
Changelog
A drop-in product changelog for Laravel with per-user read tracking — the Changelog model, a HasChangelogs trait, a mark-as-read endpoint, and an optional Filament admin — so you can show a "what's new" indicator.
devdojo/changelog adds a product changelog (release notes) to a Laravel app, with per-user "read" tracking so you can show a "what's new" badge that clears once a user has seen the latest entry. It ships the Changelog model, a HasChangelogs trait for your User, a single POST /changelog/read endpoint, and an optional Filament admin resource. Like blog, it is front-end agnostic — you render the public changelog page in whatever theme you use.
It is one of the feature packages bundled by foundation, where it self-gates on a feature flag, and it works standalone in any Laravel 10/11/12/13 app.
Requirements
- PHP 8.2+ and Laravel 10 – 13 (only
illuminate/supportandilluminate/databaseare required). - Filament v4 — optional, and only if you want the bundled admin resource; the model, trait, and migrations have no hard dependency on it.
Overview
- A Changelog is a release note with a
title, a shortdescription, and a full HTMLbody. - Each user's read state is tracked through a
changelog_userpivot — a row means "this user has read this entry." - The
HasChangelogstrait adds achangelogs()relation and ahasChangelogNotifications()helper that returnstruewhen the newest entry is unread, driving a "what's new" indicator. POST /changelog/readmarks every unread entry as read for the signed-in user.
Installation
composer require devdojo/changelog
Publish the migrations and config, then migrate:
php artisan vendor:publish --tag=changelog:migrations
php artisan vendor:publish --tag=changelog:config
php artisan migrate
If your User model isn't discoverable from config('auth.providers.users.model'), set it explicitly:
CHANGELOG_USER_MODEL="App\\Models\\User"
Then wire the trait onto your User model and, optionally, register the Filament admin.
Note — migrations are publish-only. Run vendor:publish --tag=changelog:migrations before migrate, or the changelogs and changelog_user tables won't exist.
Configuration
Publishing changelog:config writes config/devdojo/changelog/settings.php under the config key devdojo.changelog.settings:
return [
// The host User model used for read-tracking.
// Null → falls back to auth.providers.users.model.
'user_model' => env('CHANGELOG_USER_MODEL'),
];
| Key | Default | Purpose |
|---|---|---|
user_model |
env('CHANGELOG_USER_MODEL') → null |
Host User model used for the read-tracking pivot. When null, resolved from auth.providers.users.model. |
Publish tags
| Tag | Publishes to |
|---|---|
changelog:config |
config/devdojo/changelog/settings.php |
changelog:migrations |
database/migrations |
Wiring your User model
Add the HasChangelogs trait to your User:
use Devdojo\Changelog\Traits\HasChangelogs;
class User extends Authenticatable
{
use HasChangelogs;
}
This adds:
$user->changelogs; // BelongsToMany — entries the user has read
$user->hasChangelogNotifications(); // bool — is the latest entry unread?
The data model
changelogs
| Column | Type | Notes |
|---|---|---|
id |
increments | |
title |
string(191) | |
description |
string(191) | short summary shown in the notification |
body |
text | full HTML body |
created_at / updated_at |
timestamps |
changelog_user (read-tracking pivot)
| Column | Type | Notes |
|---|---|---|
changelog_id |
unsigned int | FK → changelogs.id, onDelete('cascade') |
user_id |
unsigned big int | FK → users.id, onDelete('cascade') |
| primary key | (changelog_id, user_id) |
a row = "this user has read this entry" |
The Changelog model is mass-assignable for title, description, and body (via $fillable).
Usage
Creating entries
use Devdojo\Changelog\Models\Changelog;
Changelog::create([
'title' => 'v3.0 Released',
'description' => 'A big update with new features and improvements.',
'body' => '<p>Here is everything that changed…</p>',
]);
Most teams author entries from the Filament admin instead.
Read tracking and the "what's new" indicator
Show an indicator when the signed-in user has an unread latest entry (route('changelog') here is the app-defined page from the Folio example below — the package only registers the read endpoint):
@auth
@if (auth()->user()->hasChangelogNotifications())
<a href="{{ route('changelog') }}" class="badge">What's new</a>
@endif
@endauth
When the user views or dismisses the changelog, mark everything as read by POSTing to the package's endpoint:
| Method | URI | Name | Middleware |
|---|---|---|---|
POST |
/changelog/read |
changelog.read |
web, auth |
// e.g. when the "what's new" popup is dismissed
fetch('{{ route('changelog.read') }}', {
method: 'POST',
headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
});
Tip — hasChangelogNotifications() compares only the newest entry against the user's read pivot. Adding one new entry re-lights the badge for every user, even those who read older entries.
Rendering a changelog page
The package is headless — render list and detail with the model:
use Devdojo\Changelog\Models\Changelog;
$logs = Changelog::orderByDesc('created_at')->paginate(10); // list
$changelog = Changelog::findOrFail($id); // single entry
A minimal Folio example:
{{-- resources/views/pages/changelog/index.blade.php --}}
<?php
use function Laravel\Folio\name;
use Devdojo\Changelog\Models\Changelog;
name('changelog');
$logs = Changelog::orderByDesc('created_at')->paginate(10);
?>
<x-layout>
@foreach ($logs as $log)
<article>
<time>{{ $log->created_at->toFormattedDateString() }}</time>
<h2>{{ $log->title }}</h2>
<div>{!! $log->body !!}</div>
</article>
@endforeach
{{ $logs->links() }}
</x-layout>
How it's wired
ChangelogServiceProvider merges the settings config in register(). In boot() it registers the two publish groups (console only), then — gated on config('foundation.features.changelog', true) — loads the package's routes/web.php, which defines the single changelog.read route behind the web and auth middleware. The default-on true fallback means the route is registered even when no Foundation config is present.
The endpoint itself is ChangelogController@read: it finds every changelog the current user hasn't read (whereDoesntHave('users', …)) and attaches those IDs to the user through the changelog_user pivot, after which hasChangelogNotifications() returns false. The Changelog::users() relation resolves the pivot's other side from devdojo.changelog.settings.user_model, falling back to the auth config.
Filament admin
Register the plugin to get a Changelogs resource:
use Devdojo\Changelog\Filament\ChangelogPlugin;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->plugin(ChangelogPlugin::make());
}
The resource manages title, description, and a rich-text body. Filament is a suggested (optional) dependency and the resource is built for Filament v4 — the model, trait, and migrations work without it.
Using with Foundation
When foundation is installed, the changelog self-gates on its feature flag:
// config/foundation.php
'features' => [
'changelog' => true, // toggle at /foundation/setup to disable
],
Disabling changelog skips registration of the changelog/read route and the Filament resource; the model, trait, and migrations remain in place, so toggling is lossless. Standalone, with no Foundation present, the flag is absent and the changelog defaults to on.
Extension points
- Swap the pivot's User model — set
CHANGELOG_USER_MODEL/devdojo.changelog.settings.user_model. - Custom read semantics — the trait's
hasChangelogNotifications()is intentionally simple (latest-entry only); override it in yourUserfor per-entry unread counts by querying thechangelog_userpivot directly. - Edit the schema — the published migrations are yours; add columns such as a
versionorcategory. - Own the front-end — build the changelog page and "what's new" popup in your own theme against the
Changelogmodel.
The changelog pairs naturally with blog for long-form content and notifications for in-app alerts.