Skip to content
MCG Filament CMS Demo

Notifications

This document explains how the package notifies a site's team about content changes, by Slack message, by email, or both. It covers enabling the feature, the twelve events that can be sent, how delivery behaves when a channel fails, how the Slack URL is protected from the MCP server, and how a host application can listen to the same events directly. It is written for the developer who enables the feature and for the site administrator who chooses which events are sent.

Notifications are the human-facing counterpart to Webhooks. The two features are separate because they serve different recipients with different thresholds:

  • Webhooks tell a decoupled front end to rebuild. A rebuild should run on every change, because a front end that misses a change serves outdated content.
  • Notifications tell a person. A message should be sent only for the changes a person has chosen to be interrupted by, so each event has its own toggle.

Both features use the same event keys, such as content.published and content.moved (src/Support/SiteEvents.php).

Enabling notifications

Notifications are sent only when two conditions are met: the feature is enabled in the deployment's environment, and at least one destination is configured in Site settings.

One-time setup

Step 1. In .env at the site's root, enable the feature. The default is false, so upgrading the package never starts sending messages on its own.

.env Environment file

CMS_NOTIFICATIONS_ENABLED=true

Step 2. In a terminal at the site's root, run php artisan config:clear so the new value takes effect.

Step 3. In the admin panel, on the Site settings screen (/cms/site-settings on a panel of the CMS's own), scroll to the Notifications section and fill in one or both destinations:

  • Slack webhook URL: create it in Slack under Apps β†’ Incoming Webhooks β†’ Add to Workspace. The URL determines the channel, so one URL posts to one channel.
  • Email recipients: any number of addresses. Press Enter after each address.

Either destination alone is enough. With neither filled in, nothing is sent and no delivery is attempted, whatever the event toggles say. An entry in Email recipients that is not a valid email address is skipped, so that one mistyped address does not fail the whole send.

Step 4. In the same section, under Send on, tick the events to be notified about. The events are listed under Events below.

Step 5. In the same section, click Send a test message in the section header. The test is sent immediately rather than queued, and it uses the values currently in the form, so a URL can be tested before the form is saved. A success notification titled "Test message sent" or an error notification titled "Could not send the test message", with the error text, appears on screen.

Step 6. At the bottom of the screen, click Save.

Behavior while the feature is disabled

While CMS_NOTIFICATIONS_ENABLED is false, the Notifications section shows a warning that notifications are switched off for the installation. The fields still save, and Send a test message still delivers, because the test posts directly to the destinations. The listener that turns a published page into a message (NotifyPeople) is registered only when cms.notifications.enabled is true. A successful test therefore proves only that the destination works. When the feature is disabled, the success notification says so and stays on screen until it is dismissed.

Send a test message requires the cms.settings.update ability when permissions are enabled.

Events

Event (label under Send on) Key Sent when Ticked by default
Content published content.published A version goes live Yes
Content unpublished content.unpublished A page is withdrawn from the site Yes
Content scheduled content.scheduled A page is given a date to go live or come down No
Draft saved content.saved An existing page is saved without publishing No
Content deleted content.deleted A page is moved to the bin Yes
Content restored content.restored A page is brought back from the bin No
Page moved content.moved A page changes address No
Media uploaded media.uploaded A new file is added to the media library No
Media replaced media.replaced A file is swapped for a new one, keeping its place on every page that uses it No
Redirect created redirect.created Any redirect is created, whether typed by an editor or written automatically No
Webhook endpoints changed webhooks.changed An endpoint is added, edited or removed in the Webhooks section No
Form submitted form.submitted A form is submitted. The message names the form and never includes the answers No

Three events are ticked by default: a publish, a withdrawal and a deletion are the changes a team most often needs to know about. The other nine are off, because a channel that reports every save stops being read.

A site whose Send on list has never been saved gets the three defaults. Once the list has been saved, it is used exactly as saved: unticking every event sends nothing, and the defaults do not return.

Behavior of individual events

  1. Draft saved is sent on every save of an existing page, which happens often while an editor is working. Two saves are not reported: a page's first version, which is a creation rather than an edit, and a save whose version has become the published version by the time the queued job runs, which is the save half of a save-and-publish. Without that second rule, a save-and-publish would be reported twice.
  2. Redirect created overlaps with Page moved. Moving a page writes a redirect automatically, so with both events ticked, one rename produces two messages.
  3. Form submitted is in addition to each form's own recipients. Each form already emails its own recipient list with every answer and a reply-to address. This event is a separate, short message for a team channel, and a person on both lists receives both. The answers are never included in this message, so text typed into a support form is not posted to a channel everyone in it can read.
  4. Media uploaded is sent only for files added to the CMS media library, not for files a host application adds to its own Spatie media collections. Replacing a file sends Media replaced, not Media uploaded.

Delivery

Sending never blocks or fails the change that caused it:

  1. NotifyPeople implements ShouldQueueAfterCommit, so it runs on the queue after the database transaction that wrote the change has committed.
  2. Slack and email are attempted independently. If Slack is unreachable, the email is still sent, and neither failure affects the save.
  3. A failed attempt is logged as a warning, "CMS notification via slack failed" or "CMS notification via mail failed", with the error message.
  4. The Slack request times out after cms.notifications.timeout seconds (default 5, set with CMS_NOTIFICATIONS_TIMEOUT). Emails are queued as separate mail jobs.

With a real queue driver, a queue worker must be running for notifications to be sent. With the sync driver, they are sent during the request.

The Slack URL and the MCP server

The list of refused groups is built in code, not read from config alone. SettingsPolicy::denied() (src/Mcp/Support/SettingsPolicy.php) returns the groups in cms.mcp.settings.denied, which ships as ['analytics'], with webhooks and notifications always added. The reason is that Laravel's mergeConfigFrom is a shallow merge: it supplies a top-level key the application's published config is missing, but it does not descend into a key the application has already published. An installation that published config/cms.php before either group existed would never receive a config addition, and would continue to return both groups in full to any MCP client that asked. That is how the webhooks group was once served with its signing secrets included. An application can add further groups to cms.mcp.settings.denied; it cannot remove webhooks or notifications from the refused list.

Configuration

config/cms.php PHP

'notifications' => [
    'enabled' => (bool) env('CMS_NOTIFICATIONS_ENABLED', false),
    'timeout' => (int) env('CMS_NOTIFICATIONS_TIMEOUT', 5),
],
Key Environment variable Default Effect
notifications.enabled CMS_NOTIFICATIONS_ENABLED false Registers the NotifyPeople subscriber. When false, no event sends a message
notifications.timeout CMS_NOTIFICATIONS_TIMEOUT 5 Timeout, in seconds, of the HTTP request to the Slack webhook

Listening to the events directly

Every notification is driven by an ordinary Laravel event, so a host application can listen to the same events without using the notification feature:

Plain text

Mcg\FilamentCms\Events\ContentPublished     ContentUnpublished
Mcg\FilamentCms\Events\ContentScheduled     ContentSaved
Mcg\FilamentCms\Events\ContentDeleted       ContentRestored
Mcg\FilamentCms\Events\ContentPathChanged   MediaUploaded
Mcg\FilamentCms\Events\MediaReplaced        RedirectCreated
Mcg\FilamentCms\Events\WebhooksChanged      FormSubmitted

These twelve classes are the complete set of domain events. Five of them were added for the notification feature: ContentScheduled, MediaUploaded, RedirectCreated, WebhooksChanged and FormSubmitted. The other seven existed before it. See Integrating another package for listening to them from a host application.

Mcg\FilamentCms\Events holds two further classes, ContentUpdatedForViewers and SiteUpdatedForViewers, which are not domain events. They implement ShouldBroadcastNow and tell a browser that is already showing a page to refresh. No notification is sent for either. Neither is suitable for triggering other work:

  • ContentUpdatedForViewers is dispatched on both save and publish, so a listener on it runs twice for a save-and-publish.
  • It is dispatched inside a try/catch, so that a broadcasting failure cannot break the write. A listener on it therefore does not run at all when broadcasting throws.

MediaUploaded is dispatched differently from the other events. It is dispatched from Spatie's MediaHasBeenAddedEvent (by src/Listeners/DispatchMediaUploaded.php) rather than from the media model's created event. Spatie's library writes the database row before it copies the file, and force-deletes the row if the copy fails. When MediaUploaded is dispatched, the file is already on the disk.