Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message
newsletter-development
jeffersongoncalves/laravel-newsletter
claude-code
newsletter
email
laravel
email-marketing
double-opt-in
mailing-list

Build and work with Laravel Newsletter features, including composing, scheduling, sending and tracking double opt-in email newsletters.

Install
php artisan boost:add-skill jeffersongoncalves/laravel-newsletter

Save this content to: .claude/skills/newsletter-development/SKILL.md

# Newsletter Development

## When to use this skill

Use this skill when:
- Composing or sending newsletters through `jeffersongoncalves/laravel-newsletter`
- Managing email groups and double opt-in subscribers
- Working with newsletter scheduling, tracking or delivery status

## Core Concepts

### Newsletter

A `Newsletter` holds the subject, sender, content (`content_type`: `rich_text`, `markdown` or `html`), status (`draft`, `scheduled`, `sending`, `sent`), and tracking counters (`total_recipients`, `total_views`). It belongs to many `EmailGroup` models through the `newsletter_email_group` pivot table.

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

$newsletter = Newsletter::create([
    'subject' => 'Monthly Update',
    'sender_email' => '[email protected]',
    'sender_name' => 'Example Co',
    'content' => '<p>Hello!</p>',
    'content_type' => NewsletterContentType::Html,
    'utm_campaign' => 'monthly-update',
]);

$newsletter->emailGroups()->attach($group);
```

### Email Groups & Members

`EmailGroup` represents a mailing list (e.g. "Website", "Customers"). `EmailGroupMember` rows track a single email address's subscription state per group: `unsubscribed`, `unsubscribed_at`, and `confirmed_at` (null until the double opt-in link is clicked).

```php
use JeffersonGoncalves\Newsletter\Facades\Newsletter;

$member = Newsletter::subscribe('[email protected]', 'Website');
// $member->confirmed_at is null until the subscriber clicks the confirmation link
```

### Sending a Newsletter

`SendNewsletterAction` validates the newsletter can be sent, resolves pending recipients (confirmed, non-unsubscribed, not already sent-to), and dispatches one `SendNewsletterEmailJob` per recipient inside a `Bus::batch()`. When the batch finishes, the newsletter is marked `sent`.

```php
use JeffersonGoncalves\Newsletter\Facades\Newsletter;

Newsletter::send($newsletter);
```

Each recipient's outcome is recorded in `NewsletterSentRecipient` (`status`: `sent` or `failed`, with `error_message` on failure).

### Sending a Test

`sendTest()` renders and sends the newsletter synchronously to a single address, bypassing status transitions and recipient tracking — useful for previewing before a real send.

```php
Newsletter::sendTest($newsletter, '[email protected]');
```

## Common Patterns

### Scheduling a Send

Set `status` to `scheduled` and `scheduled_at` to a future date. The `newsletter:send-scheduled` command (registered on the schedule hourly, gated by `config('newsletter.schedule_enabled')`) dispatches `SendScheduledNewslettersJob`, which sends every due newsletter via `SendNewsletterAction`.

```php
use JeffersonGoncalves\Newsletter\Actions\ScheduleNewsletterAction;

app(ScheduleNewsletterAction::class)->handle($newsletter, now()->addDay());
```

### Tracking Links and Opens

`InjectNewsletterTrackingAction` appends `utm_source`, `utm_medium` and `utm_campaign` query parameters to every external link in the rendered email, and appends an open-tracking pixel when `send_webview_link` is true. This runs automatically whenever `NewsletterMail` is built.

### Detecting Broken Links

```php
use JeffersonGoncalves\Newsletter\Actions\FindBrokenNewsletterLinksAction;

$broken = app(FindBrokenNewsletterLinksAction::class)->handle($newsletter);
// Collection of ['url' => ..., 'status' => int|null, 'error' => string|null]
```

### Public Routes

`SubscriptionController` exposes double opt-in routes under `config('newsletter.route_prefix')` (default `newsletter`): `GET subscribe` (Blade form, gated by `config('newsletter.subscribe_form_enabled')`), `POST subscribe`, `GET confirm/{emailGroupMember}` (signed) and `GET unsubscribe/{emailGroupMember}` (signed). The subscribe/confirmed/unsubscribed views are plain Blade with Tailwind via CDN — no Filament dependency — and publishable with `php artisan vendor:publish --tag=newsletter-views` for custom styling.

## Troubleshooting

### Error: "This newsletter has already been sent."

**Cause**: `SendNewsletterAction` refuses to resend a newsletter whose `status` is already `sent`.

**Solution**: Create a new `Newsletter` record (or duplicate the existing one) instead of resending.

### Error: "The newsletter has no pending recipients to send to."

**Cause**: Every confirmed, non-unsubscribed member of the newsletter's email groups has already received it successfully, or no email groups have confirmed members yet.

**Solution**: Attach an `EmailGroup` with confirmed members, or check `NewsletterSentRecipient` for members that already received the newsletter.

## API Reference

### `Newsletter::send(Newsletter $newsletter): Newsletter`

Dispatches the batched send. Throws `InvalidArgumentException` if the newsletter was already sent, has an invalid `sender_email`, or has no pending recipients.

### `Newsletter::sendTest(Newsletter $newsletter, string $email): void`

Sends a synchronous preview copy without touching delivery tracking.

### `Newsletter::subscribe(string $email, ?string $groupTitle = null): EmailGroupMember`

Creates or reuses an `EmailGroupMember` and queues a confirmation email. Falls back to `config('newsletter.default_email_group')` when `$groupTitle` is null.

### `Newsletter::confirm(EmailGroupMember $emailGroupMember): EmailGroupMember`

Sets `confirmed_at` to now.

### `Newsletter::unsubscribe(EmailGroupMember $emailGroupMember): EmailGroupMember`

Sets `unsubscribed` to true and stamps `unsubscribed_at`.

Newsletter Development

When to use this skill

Use this skill when:

  • Composing or sending newsletters through jeffersongoncalves/laravel-newsletter
  • Managing email groups and double opt-in subscribers
  • Working with newsletter scheduling, tracking or delivery status

Core Concepts

Newsletter

A Newsletter holds the subject, sender, content (content_type: rich_text, markdown or html), status (draft, scheduled, sending, sent), and tracking counters (total_recipients, total_views). It belongs to many EmailGroup models through the newsletter_email_group pivot table.

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

$newsletter = Newsletter::create([
    'subject' => 'Monthly Update',
    'sender_email' => '[email protected]',
    'sender_name' => 'Example Co',
    'content' => '<p>Hello!</p>',
    'content_type' => NewsletterContentType::Html,
    'utm_campaign' => 'monthly-update',
]);

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

Email Groups & Members

EmailGroup represents a mailing list (e.g. "Website", "Customers"). EmailGroupMember rows track a single email address's subscription state per group: unsubscribed, unsubscribed_at, and confirmed_at (null until the double opt-in link is clicked).

use JeffersonGoncalves\Newsletter\Facades\Newsletter;

$member = Newsletter::subscribe('[email protected]', 'Website');
// $member->confirmed_at is null until the subscriber clicks the confirmation link

Sending a Newsletter

SendNewsletterAction validates the newsletter can be sent, resolves pending recipients (confirmed, non-unsubscribed, not already sent-to), and dispatches one SendNewsletterEmailJob per recipient inside a Bus::batch(). When the batch finishes, the newsletter is marked sent.

use JeffersonGoncalves\Newsletter\Facades\Newsletter;

Newsletter::send($newsletter);

Each recipient's outcome is recorded in NewsletterSentRecipient (status: sent or failed, with error_message on failure).

Sending a Test

sendTest() renders and sends the newsletter synchronously to a single address, bypassing status transitions and recipient tracking — useful for previewing before a real send.

Newsletter::sendTest($newsletter, '[email protected]');

Common Patterns

Scheduling a Send

Set status to scheduled and scheduled_at to a future date. The newsletter:send-scheduled command (registered on the schedule hourly, gated by config('newsletter.schedule_enabled')) dispatches SendScheduledNewslettersJob, which sends every due newsletter via SendNewsletterAction.

use JeffersonGoncalves\Newsletter\Actions\ScheduleNewsletterAction;

app(ScheduleNewsletterAction::class)->handle($newsletter, now()->addDay());

Tracking Links and Opens

InjectNewsletterTrackingAction appends utm_source, utm_medium and utm_campaign query parameters to every external link in the rendered email, and appends an open-tracking pixel when send_webview_link is true. This runs automatically whenever NewsletterMail is built.

Detecting Broken Links

use JeffersonGoncalves\Newsletter\Actions\FindBrokenNewsletterLinksAction;

$broken = app(FindBrokenNewsletterLinksAction::class)->handle($newsletter);
// Collection of ['url' => ..., 'status' => int|null, 'error' => string|null]

Public Routes

SubscriptionController exposes double opt-in routes under config('newsletter.route_prefix') (default newsletter): GET subscribe (Blade form, gated by config('newsletter.subscribe_form_enabled')), POST subscribe, GET confirm/{emailGroupMember} (signed) and GET unsubscribe/{emailGroupMember} (signed). The subscribe/confirmed/unsubscribed views are plain Blade with Tailwind via CDN — no Filament dependency — and publishable with php artisan vendor:publish --tag=newsletter-views for custom styling.

Troubleshooting

Error: "This newsletter has already been sent."

Cause: SendNewsletterAction refuses to resend a newsletter whose status is already sent.

Solution: Create a new Newsletter record (or duplicate the existing one) instead of resending.

Error: "The newsletter has no pending recipients to send to."

Cause: Every confirmed, non-unsubscribed member of the newsletter's email groups has already received it successfully, or no email groups have confirmed members yet.

Solution: Attach an EmailGroup with confirmed members, or check NewsletterSentRecipient for members that already received the newsletter.

API Reference

Newsletter::send(Newsletter $newsletter): Newsletter

Dispatches the batched send. Throws InvalidArgumentException if the newsletter was already sent, has an invalid sender_email, or has no pending recipients.

Newsletter::sendTest(Newsletter $newsletter, string $email): void

Sends a synchronous preview copy without touching delivery tracking.

Newsletter::subscribe(string $email, ?string $groupTitle = null): EmailGroupMember

Creates or reuses an EmailGroupMember and queues a confirmation email. Falls back to config('newsletter.default_email_group') when $groupTitle is null.

Newsletter::confirm(EmailGroupMember $emailGroupMember): EmailGroupMember

Sets confirmed_at to now.

Newsletter::unsubscribe(EmailGroupMember $emailGroupMember): EmailGroupMember

Sets unsubscribed to true and stamps unsubscribed_at.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel