php elephant sticker
Photo by RealToughCandy.com on Pexels.com
Table of Contents

[Complete Practical Guide] Laravel Notification & Email Infrastructure — Mailable/Notifications, Deliverability, Unsubscribe, Webhooks, Accessible Templates, SMS/Push Integration, Measurement and Testing

What You’ll Learn (Key Points)

  • How to choose between Mailables and Notifications, and how to design channels (Email/SMS/Slack/Push/Database)
  • How to improve deliverability with proper domain setup (SPF/DKIM/DMARC), queued sending, retries, and idempotency
  • How to create accessible HTML emails and plain text versions, subject/preview text, unsubscribe and subscription management
  • How to handle bounces/complaints via inbound Webhooks, suppression lists, and metrics visualization
  • How to handle multi-language/time zones/personalization, staged notifications with SMS/Push, and tests + CI integration

Target Readers (Who Benefits?)

  • Laravel beginner to intermediate engineers: want to send notifications safely, reliably, and in a readable way
  • Tech leads at SaaS/Media/EC: want to standardize operational guidelines including unsubscribe and complaint handling
  • CS/Marketing/Accessibility specialists: want to shape notification content and policies that are understandable for everyone

Accessibility Level: ★★★★★

We’ll cover email body structure, alt text, plain text version, color-independent emphasis, subject and preview design, link wording, and clear subscription management.


1. Introduction: Notification Is Built on “Delivery”, “Understanding”, and “Choice”

The goal of notifications is to reliably reach users, be quickly understood, and allow them to control how they receive them at any time. Laravel provides Mailables and Notifications which let you handle multiple channels from the same codebase. But technology alone does not guarantee deliverability or readability. You need to think about domain configuration, unsubscribe, complaint handling, message design, and accessibility as part of the operations.

This article is a practical guide to designing and implementing a notification infrastructure that you can take directly into production.


2. Architecture: Choosing Between Mailables and Notifications

2.1 Which One Should I Use?

  • Mailable: When you want to finely design email only (HTML/text, attachments, layout control).
  • Notifications: When you want to fan out a single notification event across Email/SMS/Slack/Push/Database. It keeps the codebase organized even when you add more channels later.
// Example: Notification (Email + Database)
class OrderShipped extends Notification
{
    use Queueable;

    public function via($notifiable): array
    {
        return ['mail','database'];
    }

    public function toMail($notifiable): MailMessage
    {
        return (new MailMessage)
            ->subject('Your order has been shipped')
            ->greeting($notifiable->name.' ')
            ->line('Your order has been shipped. Here is your tracking number.')
            ->action('View shipment status', route('orders.track', $this->order->id))
            ->line('If you did not expect this email, you can safely ignore it.');
    }

    public function toDatabase($notifiable): array
    {
        return ['order_id' => $this->order->id, 'tracking' => $this->order->tracking_no];
    }
}

2.2 Queues Are Mandatory

Notifications and emails should always be queued. This prevents harming user experience and helps handle traffic spikes.

// .env
QUEUE_CONNECTION=redis
MAIL_MAILER=smtp  // or an API driver
Notification::route('mail', 'user@example.com')
    ->notify((new OrderShipped($order))->delay(now()->addSeconds(2)));

3. Infrastructure Setup for Deliverability

3.1 Domain Setup (Operational Basics)

  • SPF: Declares allowed senders for your domain.
  • DKIM: Signs outgoing messages with your domain, helping prevent tampering.
  • DMARC: Policy based on SPF/DKIM results, with reporting to drive ongoing improvement.
  • Using a subdomain (e.g., mail.example.com) lets you separate your app’s main domain from mail-sending and can help deliverability.

3.2 Sending in Practice

  • Queue & Retry: Prepare for transient SMTP issues with exponential backoff retries.
  • Throttling: Rate-limit large sends and “warm up” your sending IPs and domains.
  • Sender Identity: Set From and Reply-To according to purpose. Clearly distinguish support addresses and no-reply addresses.
  • Message Structure: Use multipart/alternative and always include a plain text part. Avoid HTML-only messages.

4. Designing Accessible Emails

Emails are read in mail clients, not browsers. CSS, JavaScript, and ARIA support is limited. Design with a minimal set of rules that work reliably.

4.1 Body Structure

  • Use headings, paragraphs, and lists to build hierarchical information.
  • Present critical information (order numbers, dates, amounts) as text, not only in images.
  • Link texts should be specific, not just “here”. Use “View shipment status” etc.

4.2 Alt Text and Contrast

  • Always set alt on images. Use empty alt="" for decorative images.
  • Buttons should be <a> tags styled as buttons, with meaningful text on the label. Don’t rely solely on color.
  • Ensure enough color contrast between background and text.

4.3 Always Provide Plain Text

  • Even where HTML is unavailable, the text version should preserve the essential meaning and information.
  • Do not break URLs across lines; keep each link as a single unbroken line.

4.4 Subject and Preview Text

  • Subjects should include content + identifier (e.g., [Example] Shipment complete: Order #12345).
  • Put a short preview sentence at the very beginning (e.g., You can track your shipment online.) to improve how it looks in inbox lists.

4.5 Dark Mode and Layout

  • Table-based layout + inline CSS is the standard.
  • You can’t fully control dark mode. Avoid text over background images; keep it simple.

5. Template Management with Mailables

5.1 Blade Templates

// app/Mail/OrderShippedMail.php
class OrderShippedMail extends Mailable implements ShouldQueue
{
    use Queueable, SerializesModels;

    public function __construct(public Order $order){}

    public function build()
    {
        return $this->subject('Shipment complete: Order #'.$this->order->number)
            ->from('no-reply@mail.example.com', config('app.name'))
            ->view('mail.orders.shipped')        // HTML
            ->text('mail.orders.shipped_text');  // Plain text
    }
}
{{-- resources/views/mail/orders/shipped.blade.php --}}
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
  <tr>
    <td>
      <h1 style="font-size:20px;line-height:1.4;margin:0 0 16px;">Your order has been shipped</h1>
      <p style="margin:0 0 12px;">Order number: {{ $order->number }}</p>
      <p style="margin:0 0 12px;">Total: ¥{{ number_format($order->total) }}</p>
      <p style="margin:0 0 20px;">You can check your shipment status using the button below.</p>
      <p>
        <a href="{{ route('orders.track', $order->id) }}"
           style="display:inline-block;background:#2563eb;color:#fff;padding:10px 16px;text-decoration:none;border-radius:4px;">
           View shipment status
        </a>
      </p>
      <p style="margin:20px 0 0;">※ This email is sent from a notification-only address. For questions, please contact support.</p>
    </td>
  </tr>
</table>
{{-- resources/views/mail/orders/shipped_text.blade.php --}}
Your order has been shipped.

Order number: {{ $order->number }}
Total: ¥{{ number_format($order->total) }}

Check your shipment status:
{{ route('orders.track', $order->id) }}

※ This email is sent from a notification-only address. For questions, please contact support.

5.2 Componentization

Turn common headers/footers, CTA buttons, and info boxes into Blade components to unify wording and reduce maintenance costs.


6. Unsubscribe and Subscription Management

6.1 Legal Compliance and UX

  • Every marketing email should have a clearly visible unsubscribe path (footer/header).
  • Transactional notifications (receipts, etc.) should be a separate category, so they don’t get accidentally disabled.
  • A settings page that lets users control topics/frequency can significantly reduce support tickets.

6.2 Implementation (Signed Links)

// Unsubscribe link (with expiration)
$url = URL::temporarySignedRoute(
    'unsubscribe',
    now()->addDays(7),
    ['user' => $user->id, 'topic' => 'marketing']
);
// routes/web.php
Route::get('/unsubscribe', function(Request $r){
    abort_unless($r->hasValidSignature(), 403);
    $user = User::findOrFail($r->integer('user'));
    $user->unsubscribe($r->string('topic'));
    return view('mail.unsubscribe_done');
})->name('unsubscribe');

6.3 List-Unsubscribe Header

Setting the List-Unsubscribe header helps some clients show a one-click unsubscribe UI. Use a signed URL here as well.


7. Inbound Webhooks: Bounces, Complaints, Suppression

7.1 Why You Need This

To keep deliverability healthy, you must ingest and react to feedback signals. Email addresses that hard-bounce or generate complaints must be suppressed from future sends.

7.2 Flow

  1. Provide an endpoint that receives Webhooks from your email provider.
  2. Validate authenticity via signature verification.
  3. Classify event types (hard/soft bounce, complaint, open/click if captured).
  4. Update the suppression list, blocking further sends to affected recipients.
  5. Reflect events in metrics.
// Signature verification (outline)
$payload = $request->getContent();
$sig = $request->header('X-Signature');
$calc = hash_hmac('sha256', $payload, config('services.mail.webhook_secret'));
abort_unless(hash_equals($calc, $sig), 401);

8. Multi-language, Time Zones, and Personalization

8.1 Locale and Placeholders

  • Use ->locale() on MailMessage and Mailable.
  • Use translation keys + variables to generate complete sentences, preventing unnatural grammar.
  • Sanitize names, amounts, and dates, and always provide fallbacks for missing data.

8.2 Timing

  • Adjust send times to the user’s time zone.
  • Daily/weekly digests can lower user fatigue and unsubscribe rates.

9. SMS, Push, and In-App Notifications: Channel Orchestration

9.1 Staged Notifications

  • Start with in-app notifications (database) as a quiet channel.
  • Use email for important items, escalate to SMS/Push only for urgent events.
  • Each channel should lead to the same wording and same action to avoid confusion.

9.2 SMS Considerations

  • Be mindful of character limits and URL shorteners. Communicate key points concisely and concretely.
  • Respect quiet hours and user preferences; avoid late-night SMS.

9.3 Web Push

  • Get explicit consent, make opting out easy, and properly classify notifications and their priority.

10. Measurement, Monitoring, and Dashboards

  • Aggregate sends, successes/failures, bounce types, complaints, opens/clicks (if tracked) per day and per template.
  • Trends matter most: configure alerts for anomalous spikes (e.g., complaint rate), then revisit content and recipient lists.
  • Handle personally identifiable behavioral data with strict minimization, honoring privacy policies and user choices.

11. Test Strategy: Fakes, Rendering, Accessibility

11.1 Mail/Notification Fakes

use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Notification;

Mail::fake();
Notification::fake();

$user = User::factory()->create();
Notification::send($user, new OrderShipped($order));

Notification::assertSentTo($user, OrderShipped::class, function($n, $channels){
    return in_array('mail', $channels);
});

11.2 Rendering Tests

  • Generate both HTML and text versions as strings and check for essential content (subject, CTA, order number, etc.).
  • Test that alt= attributes are not missing, and that URLs are absolute.
$mailable = new OrderShippedMail($order);
$html = $this->renderMailable($mailable); // Custom helper to get rendered HTML
$this->assertStringContainsString('View shipment status', $html);
$this->assertMatchesRegularExpression('/alt="[^"]*"/', $html);

11.3 Previews and Manual Visual Checks

  • Provide a preview route in your local environment and visually check in major clients.
  • Full screenshot tests in CI are hard, but static HTML checks and link validation go a long way.

12. Sample: Subscription Settings and Template Skeleton

12.1 Subscription Model

// app/Models/Subscription.php
class Subscription extends Model {
  protected $fillable = ['user_id','topic','enabled'];

  public static function enabled($user, $topic): bool {
    return static::where(compact('user','topic'))
        ->where('enabled', true)
        ->exists();
  }
}

12.2 Filtering at Notification Time

if (! Subscription::enabled($user->id, 'marketing')) {
    return; // Do not send
}

$user->notify(new CampaignStarted($campaign));

12.3 Signed Unsubscribe UI (Excerpt)

@extends('layouts.app')
@section('title','Email preferences')
@section('content')
<h1 class="text-xl font-semibold mb-4" id="title" tabindex="-1">Email delivery settings</h1>
<p class="mb-3">You can control whether to receive each topic.</p>
<ul class="space-y-2">
  @foreach($topics as $topic)
  <li>
    <form method="POST" action="{{ route('settings.subscribe.toggle') }}">
      @csrf
      <input type="hidden" name="topic" value="{{ $topic->name }}">
      <button class="px-3 py-1 rounded border">
        {{ $topic->label }}: {{ $topic->enabled ? 'Receive' : 'Do not receive' }}
      </button>
    </form>
  </li>
  @endforeach
</ul>
@endsection

13. Common Pitfalls and How to Avoid Them

  • Sending HTML-only → Use multipart/alternative and always include a text version.
  • Putting critical information in images → Always duplicate it in text.
  • “Click here” links everywhere → Use specific, action-oriented link text.
  • Hard-to-find unsubscribe → Add a clear footer + List-Unsubscribe header.
  • Ignoring complaints/bounces → Use Webhooks and update a suppression list.
  • Ambiguous consent handling → Use double opt-in and allow topic/frequency selection.
  • Late-night SMS → Design for time zones and quiet hours.
  • Sending without queues → Risk of slow responses and higher failure rate. Queues are essential.
  • Overly aggressive tracking → Respect privacy, track minimal data, and be transparent.

14. Checklist (For Team Distribution)

Deliverability

  • [ ] SPF/DKIM/DMARC configured
  • [ ] Queue sending, retries, throttling
  • [ ] Subdomain/From/Reply-To clarified

Content & Accessibility

  • [ ] Subject includes content + identifier, preview text present
  • [ ] HTML + text dual format
  • [ ] Images have alt, no color-only emphasis, specific link text
  • [ ] Critical information shown as text

Subscription Management

  • [ ] Unsubscribe link and settings page
  • [ ] List-Unsubscribe header
  • [ ] Distinction between transactional and marketing email

Webhooks & Suppression

  • [ ] Signature verification
  • [ ] Classification of hard/soft bounces and complaints, suppression list update
  • [ ] Metrics and alerts

Multi-language & Personalization

  • [ ] ->locale() and translation key usage
  • [ ] Time zone handling, digest/frequency options
  • [ ] Fallbacks for missing data

Testing & Operations

  • [ ] Mail/Notification fakes
  • [ ] Template rendering checks
  • [ ] Preview environment and link validation
  • [ ] Send logs and request IDs

15. Summary

  • Base your system on Laravel Mailables/Notifications and ensure deliverability through queued sending and proper domain setup.
  • Use HTML + text emails, with alt text, contrast awareness, and specific link text to maximize readability and accessibility.
  • Allow users to choose with clear unsubscribe and subscription settings, separating transactional and marketing emails.
  • Take in bounces/complaints via Webhooks, updating suppression lists and metrics.
  • Combine SMS/Push/In-app notifications in stages to design a notification experience that is effective but not excessive.
  • Build testing and preview into your operations to create a robust, low-failure notification platform. Calm, honest notifications become the foundation of long-term trust.

References

By greeden

Leave a Reply

Your email address will not be published. Required fields are marked *

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)