marcelodelgado/filament-announcements
A Filament v5 plugin for institutional announcements and alerts on panel dashboards. Includes a severity-ordered widget, full CRUD resource with badges and bulk actions, per-user dismiss via pivot table, and automatic expiration using date windows—no jobs required.
Install and register the plugin
composer require marcelodelgado/filament-announcements
php artisan vendor:publish --tag="filament-announcements-migrations"
php artisan migrate
The publish tag is the one detail most likely to differ between releases — if the tag isn't listed in the README, run php artisan vendor:publish interactively and pick the provider from the list. Same for config: check whether a filament-announcements.php config file exists before assuming you can tune it.
Wire it into your panel in the PanelProvider:
use MarceloDelgado\FilamentAnnouncements\FilamentAnnouncementsPlugin;
public function panel(Panel $panel): Panel
{
return $panel
->plugin(FilamentAnnouncementsPlugin::make());
// ...
}
Where to look first
src/FilamentAnnouncementsPlugin.php) — it will tell you which resources, pages, and widgets get registered, and whether there are chainable options.Announcement model and its migration — this is where expiration and per-user dismissal are actually implemented, and it's what you'll query when you need to script or seed data.First real use case: log in as an admin, create an announcement with a title, body, and an expiry date a day or two in the future, save it, then log in as a non-admin user and confirm the widget shows it on the dashboard. Dismiss it as the regular user, refresh, and confirm it stays gone — that single loop validates the migration, the widget registration, and the dismissal table all at once.
Use it for operational messaging, not marketing. The description says "institutional announcements" — think maintenance windows, policy changes, "the finance export is delayed until Monday." These are time-boxed and audience-specific, which is exactly what the expiration field is for. If you find yourself wanting per-user targeting beyond "everyone sees it," you're outside this package's scope and should reach for Filament's notification system instead.
Seed announcements rather than creating them through the UI in every environment. For staging and demo environments, a seeder that inserts a handful of announcements (one active, one expired, one long-running) gives every developer the same visible state and lets you verify the expiration logic without waiting overnight. Keep the seeder idempotent — update-or-create on a stable slug or title rather than blind inserts, so re-running migrate:fresh --seed doesn't pile up duplicates.
Expiration is the load-bearing feature — lean on it hard. The workflow that works well: create the announcement with a hard expiry date the moment you know about the maintenance window, publish it, and then forget about it. You never have to remember to take it down. The anti-pattern is an announcement with no expiry, because those accumulate silently and your dashboard becomes a graveyard of stale notices within a quarter. Treat "no expiry date" as an exception that needs a reason.
Dismissal is per-user and therefore not a broadcast control. A dismissed announcement is gone for that user and stays gone. So if you publish an announcement and then edit its body to add critical new information, anyone who already dismissed it will not see the update. The reliable pattern for a materially changed message is a new announcement, not an edit to the existing one.
Ordering matters on a dashboard. The widget sits alongside whatever other widgets your panel already registers. If you've got stats overviews and charts, an announcement banner at the very bottom will be missed. Check the widget's sort order in the panel config and move it up — a non-dismissed alert that nobody scrolls to is worse than no alert, because it creates the illusion you communicated.
Integrate with your deployment flow. If you use something like Envoyer, Forge, or a GitHub Actions deploy, consider a post-deploy artisan command that creates an announcement only when the release contains something users need to know (a changed workflow, a renamed field). Keep this opt-in and rare — a "we deployed" announcement every single deploy trains people to dismiss without reading.
Use the generated resource for content, not for logic. The CRUD screen is for humans writing copy. Anything conditional — "only show this if the user's subscription is expiring" — belongs in your own code reading the announcements table, not in hand-maintained rows an admin has to remember to update.
Filament v5 only. The package advertises Filament v5 explicitly. If you're mid-upgrade on v3 or v4, this will not install cleanly, and a composer require that succeeds by pulling in a stray Filament version constraint is worse than one that fails. Pin your Filament version first, then add the plugin.
Low maturity means read the source, not just the README. Seven stars and zero dependents means this package has not been battle-tested across many Laravel and Filament minor versions. Before committing to it, open the plugin class and the widget and skim for anything version-sensitive — Filament's widget and resource APIs have moved between majors, and a plugin this young may lag behind point releases. If you're on a long-lived project, consider whether you'd rather copy the ~few hundred lines of model, migration, resource, and widget into your app than take the dependency. The feature is not complex, and ownership removes upgrade risk entirely.
The publish tag is the first thing that will break. Migration publish tags differ between package versions and authors aren't always consistent about them. If vendor:publish --tag="filament-announcements-migrations" reports nothing published, run it without the tag and inspect the prompt. Also check whether the service provider uses loadMigrationsFrom — in which case you may not need to publish at all, and publishing could give you a stale duplicate.
Widget registration is easy to miss. Adding ->plugin() does not always register dashboard widgets depending on how the plugin is written — some plugins require you to also call ->widgets([...]) or add the widget to ->discoverWidgets(). After installing, load the dashboard and confirm the widget's class appears in the rendered page source. If it doesn't, check the plugin's register() method to see what it actually does rather than assuming.
Dismissal state lives in a separate table — account for it in your queries. If you write your own query for "which announcements should this user see," you must left-join the dismissal records and filter out anything dismissed by the current user. Forgetting this produces the confusing bug where your custom view shows announcements the widget correctly hides.
Cache invalidation after publishing. If your panel caches widgets or you're using a query cache, a newly created announcement may not appear until the cache turns over. Confirm whether the widget queries live (most do) before chasing a phantom bug.
Debugging visibility issues in order of likelihood: (1) the announcement's expiry date is in the past or the "published" flag is off, (2) the current user dismissed it in a previous session — check the dismissal table directly rather than reasoning about it, (3) the widget isn't registered on this panel at all, (4) timezone mismatch between the server's app.timezone and how the expiry was entered. That last one is subtle: an announcement expiring "today" can vanish hours early or late depending on where the boundary is computed, so set expiry dates with an explicit time and check the stored value in the database against what you typed.
Extension point that actually matters: the widget view. If you need to change styling, add a severity color, or render the body as markdown, vendor-publish the views (if supported) or override the widget class in your app. Overriding the class is often cleaner than editing published views, because it survives package upgrades — extend the package widget, change what you need, and register yours instead.
Test the expiry path deliberately. Create an announcement expiring one minute in the future, confirm it shows, wait, confirm it hides. This is a two-minute check that catches timezone and comparison-operator bugs that otherwise surface in production on the morning of a maintenance window.
How can I help you explore Laravel packages today?