Announcements
Publish a notice to every user by megaphone, email and web push.
Announcements are notices an administrator publishes for everyone: a maintenance window, a new feature, a change of terms. Each one is a page inside the application, an entry in a megaphone dropdown, and optionally an email. This guide covers turning the module on, publishing, what users see, and what the queue has to do.
Turn it on
| Option | Env variable | Default | What changes |
|---|---|---|---|
features.announcements |
FEATURE_ANNOUNCEMENTS |
false |
On: the Announcements resource appears at /admin/announcements, the megaphone dropdown appears in the top bar of the application, and /account/announcements opens. Off: the resource is hidden and its routes return 403, the account pages return 404, the dropdown is not rendered. |
FEATURE_ANNOUNCEMENTS is not listed in .env.example; add it yourself.
FEATURE_ANNOUNCEMENTS=true
The flag is read from config/features.php, so it takes effect after php artisan config:clear when configuration is cached.
Publish one
Go to /admin/announcements and click Create. The form:
| Field | Rules | Notes |
|---|---|---|
| Title | required, up to 255 characters | Shown in the dropdown, the list, the page and the email subject. |
| Slug | optional, unique | Left empty, it is generated from the title. It is the last segment of the public URL, /account/announcements/{slug}. |
| Description | required | A Markdown editor (Toast UI). Stored as Markdown, rendered as HTML on the page and in the email. |
| Status | required, default Draft | draft or published, the cases of App\Enums\AnnouncementStatus. Only published announcements are visible to users. |
| Send Email | default off | Whether the publication also sends an email. Hidden on the index and detail views. |

Two more fields, Published At and Created At, are read-only on the detail view.
There is no audience selector and no scheduled publication: every published announcement reaches every account at the moment you save it with the status Published. Publish drafts when you mean it.
What happens on save, in App\Observers\AnnouncementObserver:
- Saving with status Published for the first time fills Published At with the current time. Switching back to Draft clears it; publishing again stamps a new time, so the announcement comes back on top as unread for everyone.
- Every time the status becomes Published — on creation or after a change — the job
App\Jobs\SendAnnouncementEmailsJobis dispatched to the queue with the announcement. Editing the title or body of an announcement that is already published dispatches nothing.
Announcements soft-delete, so the Trash view of the resource can restore one. Permissions follow the usual shape (create announcement, retrieve announcement, update announcement, delete announcement); see /help/roles-and-permissions.
What users see
The reader side is three Livewire components in app/Livewire/Auth/Announcements/: Dropdown, Index and Show.
The megaphone. The top bar of the application shows a megaphone icon next to the notifications bell. A red badge carries the number of unread announcements, capped at 9+. Clicking it opens a dropdown with the ten most recent published announcements, newest first, each with its title and a 60-character excerpt; unread ones carry a coloured left border. The dropdown has Mark as Read, which marks everything read at once, and View all announcements.
The list. /account/announcements (also linked as Announcements in the account sidebar) pages through published announcements, ten per page, with title, 150-character excerpt and publication date.
The page. /account/announcements/{slug} renders the full Markdown with the publication date and a link back to the list. Only published announcements resolve; a draft's slug returns 404.
How reading is tracked
Read state is one timestamp per account: users.last_announcement_read_at. An announcement is unread when its Published At is later than that timestamp, or when the timestamp is empty. Two things set it to the current time: opening any announcement page, and Mark as Read in the dropdown. Both mark every announcement read, not just one.
There is no per-announcement read record and no read count. The admin panel shows nothing about who read what; what it does keep is which emails went out, described below.
Email and web push
The job runs User::whereNull('blocked_at') in chunks of 100 and sends each account App\Notifications\Announcements\AnnouncementNotification. Unverified accounts are included; blocked ones are not. The notification chooses its channels per account:
| Channel | Sent when |
|---|---|
| Send Email is on for the announcement AND the account has Notify me by email of all actions on my account enabled in its profile. | |
| Web push | The account has at least one push subscription, whether or not Send Email is on. See /help/web-push-and-the-pwa. |
The notification does not use the database channel, so it does not appear in the notifications centre; the megaphone is its in-app form. An account with neither condition receives nothing beyond the megaphone badge.
The email uses the application's standard mail layout through resources/views/emails/announcements/announcement.blade.php: the subject is {app name} — {title}, the body is the rendered Markdown, and a Read announcement button opens the announcement page. Every email that is actually sent is recorded in Communication Logs at /admin/communication_logs with the recipient, the notification class, the subject and the HTML body; a failed send is recorded there too with its status. That log is the closest thing to a delivery report the panel offers.
The job and the notification both implement ShouldQueue, so nothing is sent until a worker runs:
php artisan queue:work
Without a worker the announcement is still published and visible in the megaphone immediately; only the emails and pushes wait. Set up the worker as a service in production, as described in /help/queues-and-scheduled-work, and make sure the mailer in .env works before turning Send Email on for a large user base: one publication sends one email per account.