Speeding Up Laravel Tests with tmpfs (MySQL in RAM)
If your Laravel test suite is painfully slow and does a lot of database work (multi-tenancy, migrations, seeding), the bottleneck is almost…
When you build a Laravel application with authentication, Laravel automatically sends several emails to your users - like verification emails when they register or password reset emails when they forget their password. Out of the box, these emails use Laravel's default templates and wording. I'll show you how to customize these emails to match your application's branding and needs.
Laravel includes three main types of default emails:
Before we dive into customization, let's understand how Laravel handles sending these emails.
Laravel uses its notification system to send emails. Think of notifications as standardized messages that can be sent through different channels (email, SMS, Slack, etc.). When Laravel needs to send an email verification, it:
Here's a diagram of how it works:
User Registers → Laravel creates VerifyEmail notification → Notification uses email template → Email is sent
For example, when a user registers, Laravel automatically:
$user->notify(new VerifyEmail); // This is happening behind the scenes
The Notification Class: Controls what's in your email (subject, text, buttons)
// This defines what goes into the email
class VerifyEmail
{
public function toMail($user)
{
return (new MailMessage)
->subject('Please verify your email')
->line('Click the button to verify')
->action('Verify Email', $url);
}
}
The Email Template: Controls how your email looks (layout, styling)
{{-- This defines how the email looks --}}
<div class="email-template">
<h1>{{ $subject }}</h1>
<p>{{ $line }}</p>
<button>{{ $action }}</button>
</div>
The Custom Layout: A completely custom email design (optional)
{{-- This is your own custom email design from scratch --}}
@extends('emails.custom-layout')
@section('content')
{{-- Your completely custom email design --}}
@endsection
Let's learn how to customize each type of email...
## Customizing Email Verification
### Step 1: Create Your Custom Notification
First, create a new notification class:
```bash
php artisan make:notification CustomVerifyEmail
This creates app/Notifications/CustomVerifyEmail.php. Here's how to customize it:
<?php
namespace App\Notifications;
use Illuminate\Auth\Notifications\VerifyEmail as BaseVerifyEmail;
use Illuminate\Notifications\Messages\MailMessage;
class CustomVerifyEmail extends BaseVerifyEmail
{
public function toMail($notifiable)
{
$url = $this->verificationUrl($notifiable);
return (new MailMessage)
->subject('Welcome! Please verify your email')
->greeting("Hi {$notifiable->name}!")
->line('Welcome to ' . config('app.name') . '! We\'re excited to have you join us.')
->line('Please verify your email address by clicking the button below.')
->action('Verify My Email', $url)
->line('If you did not create an account, no further action is required.');
}
}
Laravel needs to know that it should use your custom notification instead of its default one. You do this in your User model:
<?php
namespace App\Models;
use App\Notifications\CustomVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
public function sendEmailVerificationNotification()
{
$this->notify(new CustomVerifyEmail);
}
}
Now whenever Laravel would normally send its default verification email, it will use your custom one instead.
php artisan make:notification CustomResetPassword
Then customize the notification:
<?php
namespace App\Notifications;
use Illuminate\Auth\Notifications\ResetPassword as BaseResetPassword;
use Illuminate\Notifications\Messages\MailMessage;
class CustomResetPassword extends BaseResetPassword
{
public function toMail($notifiable)
{
return (new MailMessage)
->subject('Reset Your Password')
->greeting("Hi there!")
->line('Forgot your password? No problem! Click the button below to reset it.')
->action('Reset Password', url(route('password.reset', [
'token' => $this->token,
'email' => $notifiable->getEmailForPasswordReset(),
], false)))
->line('This password reset link will expire in :count minutes.', [
'count' => config('auth.passwords.'.config('auth.defaults.passwords').'.expire')
])
->line('If you didn\'t request this, you can safely ignore this email.');
}
}
Again, tell Laravel to use your custom notification in your User model:
<?php
namespace App\Models;
class User extends Authenticatable
{
public function sendPasswordResetNotification($token)
{
$this->notify(new CustomResetPassword($token));
}
}
php artisan make:notification CustomTeamInvitation
Customize the notification:
<?php
namespace App\Notifications;
use Laravel\Jetstream\Notifications\TeamInvitation as BaseTeamInvitation;
use Illuminate\Notifications\Messages\MailMessage;
class CustomTeamInvitation extends BaseTeamInvitation
{
public function toMail($notifiable)
{
return (new MailMessage)
->subject("You're Invited to Join a Team!")
->greeting("Hello!")
->line("You've been invited to join the {$this->team->name} team!")
->line("You'll be joining as: {$this->invitation->role}")
->action('Accept Invitation', $this->acceptUrl)
->line('This invitation will expire soon, so accept it while you can!')
->line('If you weren\'t expecting this invitation, you can ignore this email.');
}
}
For team invitations, you register your custom notification in the JetstreamServiceProvider:
<?php
namespace App\Providers;
use Laravel\Jetstream\Jetstream;
use App\Notifications\CustomTeamInvitation;
use Laravel\Jetstream\JetstreamServiceProvider as ServiceProvider;
class JetstreamServiceProvider extends ServiceProvider
{
public function boot()
{
parent::boot();
Jetstream::useTeamInvitationModel(CustomTeamInvitation::class);
}
}
Now that we've customized what our emails say, let's customize how they look. There are two approaches:
Laravel's notification emails use a default template. You can customize this template to affect all notification emails at once:
First, publish the template to your application:
php artisan vendor:publish --tag=laravel-notifications
This creates resources/views/vendor/notifications/email.blade.php. Edit this file to customize the look:
<x-mail::message>
{{-- Add your logo --}}
<div style="text-align: center; margin-bottom: 30px;">
<img src="{{ asset('images/logo.png') }}" alt="{{ config('app.name') }}" style="max-width: 200px;">
</div>
{{-- Customize the greeting --}}
@if (! empty($greeting))
<h1 style="color: #2d3748; font-size: 24px; font-weight: bold; margin-bottom: 20px;">
{{ $greeting }}
</h1>
@endif
{{-- Style the content --}}
@foreach ($lines as $line)
<p style="color: #4a5568; font-size: 16px; line-height: 1.6; margin-bottom: 15px;">
{{ $line }}
</p>
@endforeach
{{-- Style the button --}}
@isset($actionText)
<div style="text-align: center; margin: 30px 0;">
<a href="{{ $actionUrl }}"
class="button"
style="background-color: #4f46e5; color: white; padding: 12px 24px;
text-decoration: none; border-radius: 5px; font-weight: bold;">
{{ $actionText }}
</a>
</div>
@endisset
{{-- Add a nice footer --}}
<div style="color: #718096; font-size: 14px; margin-top: 30px; padding-top: 20px; border-top: 1px solid #e2e8f0;">
© {{ date('Y') }} {{ config('app.name') }}. All rights reserved.
</div>
</x-mail::message>
If you want complete control over your email design, you can create your own layout from scratch:
{{-- resources/views/emails/layouts/custom.blade.php --}}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
/* Your custom email CSS */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.5;
margin: 0;
padding: 0;
background-color: #f3f4f6;
}
.container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.card {
background: white;
border-radius: 8px;
padding: 30px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.button {
display: inline-block;
padding: 12px 24px;
background-color: #4f46e5;
color: white;
text-decoration: none;
border-radius: 5px;
font-weight: bold;
}
</style>
</head>
<body>
<div class="container">
<div class="card">
{{-- Logo --}}
<div style="text-align: center; margin-bottom: 30px;">
<img src="{{ asset('images/logo.png') }}" alt="{{ config('app.name') }}" style="max-width: 200px;">
</div>
{{-- Content --}}
@yield('content')
{{-- Footer --}}
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e5e7eb; text-align: center; color: #6b7280;">
© {{ date('Y') }} {{ config('app.name') }}. All rights reserved.
</div>
</div>
</div>
</body>
</html>
{{-- resources/views/emails/verify-email.blade.php --}}
@extends('emails.layouts.custom')
@section('content')
<h1 style="color: #1f2937; font-size: 24px; margin-bottom: 20px;">Verify Your Email</h1>
<p style="color: #4b5563; margin-bottom: 20px;">
Thanks for signing up! Please verify your email address to get started.
</p>
<div style="text-align: center; margin: 30px 0;">
<a href="{{ $url }}" class="button">
Verify Email Address
</a>
</div>
<p style="color: #6b7280; font-size: 14px;">
If you did not create an account, no further action is required.
</p>
@endsection
public function toMail($notifiable)
{
$url = $this->verificationUrl($notifiable);
return (new MailMessage)
->view('emails.verify-email', ['url' => $url]);
}
Make sure your image paths are absolute URLs. Instead of:
<img src="{{ asset('images/logo.png') }}">
Use:
<img src="{{ url('images/logo.png') }}">
Email clients are picky about CSS. Always use inline styles instead of style tags:
<div style="color: blue;"> {{-- Good --}}
<div class="blue-text"> {{-- Might not work --}}
Make sure you're returning a view in your notification:
return (new MailMessage)->view('emails.custom', ['data' => $data]);
Instead of:
return (new MailMessage)->line('Hello')->action(...);
Give Vroni a GitHub issue, bug report, spec, or rough idea. It reads the repo, plans the change, writes code, runs checks, and works toward a review-ready pull request.
Take a look at vroni.com