Sending Beautiful Emails with Laravel Mailables and Markdown
Hand-coding HTML email is miserable. Tables inside tables, inline styles, and inconsistent rendering across clients have driven developers to despair for decades. Laravel's Markdown mailables sidestep all of it: you write Markdown, Laravel renders it into a clean, responsive template with a prebuilt component library. This tutorial builds a complete order-confirmation email from scratch.
Step 1: Generate a Markdown Mailable
The --markdown flag scaffolds both the Mailable class and a matching Markdown view:
php artisan make:mail OrderShipped --markdown=emails.orders.shipped
Step 2: Define the Mailable
The Mailable class is a small, structured object. Laravel 13 splits it into envelope(), content(), and attachments() methods:
class OrderShipped extends Mailable
{
use Queueable, SerializesModels;
public function __construct(public Order $order) {}
public function envelope(): Envelope
{
return new Envelope(
subject: "Your order #{$this->order->number} has shipped!",
);
}
public function content(): Content
{
return new Content(
markdown: 'emails.orders.shipped',
);
}
}
Step 3: Write the Markdown View
This is where the magic happens. You write Markdown peppered with Blade mail components, and Laravel renders a responsive HTML email plus a plain-text fallback automatically:
<x-mail::message>
# Your order is on its way
Hi {{ $order->customer->first_name }}, great news — order
**#{{ $order->number }}** shipped today.
<x-mail::panel>
Tracking number: **{{ $order->tracking_number }}**
</x-mail::panel>
<x-mail::table>
| Item | Qty | Price |
|:----------------|:---:|---------:|
@foreach ($order->items as $item)
| {{ $item->name }} | {{ $item->quantity }} | ${{ $item->price }} |
@endforeach
</x-mail::table>
<x-mail::button :url="$order->trackingUrl()">
Track Your Package
</x-mail::button>
Thanks for shopping with us,<br>
The {{ config('app.name') }} Team
</x-mail::message>
The message, panel, table, and button components are all built in. They render consistently across email clients, which is the part you would otherwise spend days fighting.
Step 4: Add Attachments
Attach a PDF invoice straight from storage using the attachments() method:
public function attachments(): array
{
return [
Attachment::fromStorageDisk('s3', $this->order->invoice_path)
->as("invoice-{$this->order->number}.pdf")
->withMime('application/pdf'),
];
}
Step 5: Send It — On a Queue
Sending mail synchronously blocks the request while your app talks to an SMTP server. Always queue transactional email. Because the Mailable uses the Queueable trait, you just call queue() instead of send():
use Illuminate\Support\Facades\Mail;
Mail::to($order->customer->email)->queue(new OrderShipped($order));
Make sure a queue worker is running (php artisan queue:work) and the email goes out in the background while your user gets an instant response.
Step 6: Preview Without Sending
You do not need to fire real emails to iterate on the design. Return the Mailable directly from a route and Laravel renders it in your browser:
Route::get('/mail/preview', function () {
$order = Order::with('items', 'customer')->latest()->first();
return new App\Mail\OrderShipped($order);
});
For local development, point MAIL_MAILER=log to dump emails into your log file, or use a tool like Mailpit to catch them in a fake inbox. Either way, you can refine the template in seconds instead of spamming your own inbox.
That is a production-quality transactional email: structured Mailable, responsive Markdown template, an attachment, background sending, and a fast preview loop. The next email you build — password resets, receipts, weekly digests — follows the exact same shape.