Laravel Magazine

Sending Beautiful Emails with Laravel Mailables and Markdown

Eric Van Johnson · Tutorials
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.

Stay Updated

Subscribe to our newsletter

Get latest news, tutorials, community articles and podcast episodes delivered to your inbox.

Weekly articles
We send a new issue of the newsletter every week on Friday.
No spam
We'll never share your email address and you can opt out at any time.