Skip to content
← Back to projects

Laravel Newsletter

Compose, schedule and send double opt-in email newsletters with open/click tracking for Laravel applications.

Laravel Newsletter

#Laravel Newsletter

Buy Me A Coffee

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status Total Downloads License

Compose, schedule and send double opt-in email newsletters with open/click tracking for Laravel applications.

#Requirements

  • PHP 8.2+
  • Laravel 12 or 13

#Installation

You can install the package via composer:

composer require jeffersongoncalves/laravel-newsletter

The package uses Laravel's auto-discovery, so the service provider and facade are registered automatically.

#Publish Configuration

php artisan vendor:publish --tag=newsletter-config

#Publish Migrations

php artisan vendor:publish --tag=newsletter-migrations

#Run Migrations

php artisan migrate

#Publish Translations (optional)

php artisan vendor:publish --tag=newsletter-translations

#Configuration

The configuration file is located at config/newsletter.php:

return [
    // URI prefix for subscribe/confirm/unsubscribe/tracking/webview routes
    'route_prefix' => 'newsletter',

    // Email group used when a subscription request does not specify one
    'default_email_group' => 'Website',

    // Registers `newsletter:send-scheduled` on the schedule (hourly)
    'schedule_enabled' => true,

    // Tag outgoing links with UTM parameters and inject the open-tracking pixel
    'tracking_enabled' => true,

    // Registers a simple Tailwind-styled subscribe page at {route_prefix}/subscribe (GET)
    'subscribe_form_enabled' => true,
];

#Usage

#Composing a Newsletter

use JeffersonGoncalves\Newsletter\Models\Newsletter;
use JeffersonGoncalves\Newsletter\Enums\NewsletterContentType;

$newsletter = Newsletter::create([
    'subject' => 'Monthly Update',
    'sender_name' => 'Example Co',
    'sender_email' => 'news@example.com',
    'content' => '<p>Hello there!</p>',
    'content_type' => NewsletterContentType::Html,
    'send_unsubscribe_link' => true,
    'send_webview_link' => true,
    'route' => 'monthly-update',
    'published' => true,
    'utm_campaign' => 'monthly-update',
]);

$emailGroup = \JeffersonGoncalves\Newsletter\Models\EmailGroup::firstOrCreate(['title' => 'Website']);

$newsletter->emailGroups()->attach($emailGroup);

Attachments can be added through the attachments media collection (powered by spatie/laravel-medialibrary):

$newsletter->addMedia($uploadedFile)->toMediaCollection('attachments');

#Subscribing (Double Opt-in)

use JeffersonGoncalves\Newsletter\Facades\Newsletter;

$member = Newsletter::subscribe('subscriber@example.com', 'Website');
// $member->confirmed_at is null until the confirmation link is clicked

A confirmation email with a signed link is queued automatically. Once the subscriber clicks it (GET /newsletter/confirm/{emailGroupMember}), confirmed_at is set. Unsubscribing works the same way via GET /newsletter/unsubscribe/{emailGroupMember}.

#Subscribe Page

A minimal, Tailwind-styled (via CDN) subscribe form ships out of the box at GET /newsletter/subscribe, posting to the POST /newsletter/subscribe endpoint above. It doesn't depend on Filament or any panel — plain Blade, so it works in any Laravel app.

Disable it with NEWSLETTER_SUBSCRIBE_FORM_ENABLED=false if your app ships its own form and only needs the POST endpoint. Customize the markup by publishing the views:

php artisan vendor:publish --tag=newsletter-views

This publishes subscribe.blade.php, confirmed.blade.php, unsubscribed.blade.php and the shared layout.blade.php.

#Sending

use JeffersonGoncalves\Newsletter\Facades\Newsletter;

Newsletter::send($newsletter);

This resolves every confirmed, non-unsubscribed member of the newsletter's email groups (excluding anyone it was already sent to), dispatches one queued job per recipient inside a batch, and marks the newsletter as sent once the batch completes. Delivery outcomes are recorded per recipient in NewsletterSentRecipient.

#Sending a Test Copy

Newsletter::sendTest($newsletter, 'preview@example.com');

Renders and sends synchronously to a single address without touching delivery tracking.

#Scheduling

use JeffersonGoncalves\Newsletter\Enums\NewsletterStatus;

$newsletter->update([
    'status' => NewsletterStatus::Scheduled,
    'scheduled_at' => now()->addDay(),
]);

The newsletter:send-scheduled command runs hourly by default (disable via NEWSLETTER_SCHEDULE_ENABLED=false) and sends every newsletter whose scheduled_at is due.

#Tracking

When tracking_enabled is on, every external link in the rendered email is tagged with utm_source, utm_medium and utm_campaign. When a newsletter has send_webview_link enabled, a 1x1 open-tracking pixel is appended, incrementing total_views on each open.

#Detecting Broken Links

use JeffersonGoncalves\Newsletter\Actions\FindBrokenNewsletterLinksAction;

$broken = app(FindBrokenNewsletterLinksAction::class)->handle($newsletter);

Returns a collection of ['url' => ..., 'status' => int|null, 'error' => string|null] for every link/image that returned an error status or could not be reached.

#Events

NewsletterSent, NewsletterScheduled, EmailGroupMemberSubscribed, EmailGroupMemberConfirmed, EmailGroupMemberUnsubscribed.

#Testing

composer test

#Changelog

Please see CHANGELOG for more information on what has changed recently.

#Contributing

Please see CONTRIBUTING for details.

#Security Vulnerabilities

Please review our security policy on how to report security vulnerabilities.

#Credits

#License

The MIT License (MIT). Please see License File for more information.

New version available.