Muhammad Yasir Amin

Loading websiteโ€ฆ

More from Muhammad Yasir Amin

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Subscribe on YouTube

Laravel 13 Markdown Mailables: Send Beautiful Emails with Markdown Templates

๐Ÿ› ๏ธ Laravel Tutorials5 min read

Laravel 13 Markdown Mailables: Send Beautiful Emails with Markdown Templates

Muhammad Yasir Amin

Muhammad Yasir Amin

Senior Full Stack Engineerโ€ขNov 12, 2026
Laravel 13 Markdown Mailables: Send Beautiful Emails with Markdown Templates

To send an email with a Markdown mailable in Laravel 13, run php artisan make:mail WelcomeMail --markdown=mail.welcome, write the message with Markdown and the x-mail:: Blade components, and send it with Mail::to($user)->send(new WelcomeMail($user)). Laravel turns the Markdown into a responsive HTML email and generates a plain-text version automatically.

I like Markdown mailables because the templates stay readable for the whole team, while the output still looks professional in Gmail, Outlook and mobile clients. You get buttons, panels and tables without writing table-based email HTML by hand.

Prerequisites

  • Laravel 13.x on PHP 8.3 or newer.
  • A mailer configured in .env. For local work, MAIL_MAILER=log writes messages to the log, or use a local mail catcher such as Mailpit.
  • Correct MAIL_FROM_ADDRESS and MAIL_FROM_NAME values.

Step 1: Generate the mailable and template

Code
php artisan make:mail WelcomeMail --markdown=mail.welcome

This creates app/Mail/WelcomeMail.php and the template resources/views/mail/welcome.blade.php.

Step 2: Define the envelope and content

Public constructor properties are available in the template automatically. For computed values, pass them through with:

JavaScript
<?php

namespace App\Mail;

use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;

class WelcomeMail extends Mailable implements ShouldQueue
{
    use Queueable, SerializesModels;

    public function __construct(public User $user)
    {
    }

    public function envelope(): Envelope
    {
        return new Envelope(
            subject: "Welcome to ".config('app.name').", {$this->user->name}",
        );
    }

    public function content(): Content
    {
        return new Content(
            markdown: 'mail.welcome',
            with: [
                'dashboardUrl' => route('dashboard'),
                'trialEndsAt' => $this->user->created_at->addDays(14)->format('d M Y'),
            ],
        );
    }
}

Implementing ShouldQueue means every send() call is queued, so the request that triggers it is not slowed down by the mail server.

Step 3: Write the Markdown template

Open resources/views/mail/welcome.blade.php:

PHP
<x-mail::message>
# Welcome, {{ $user->name }}!

Your account is ready. Here is what you can do next:

- Complete your profile
- Invite your team
- Connect your first project

<x-mail::button :url="$dashboardUrl" color="success">
Open your dashboard
</x-mail::button>

<x-mail::panel>
Your free trial runs until **{{ $trialEndsAt }}**.
</x-mail::panel>

<x-mail::table>
| Plan    | Projects | Support  |
| :------ | :------: | -------: |
| Trial   | 3        | Email    |
| Pro     | Unlimited| Priority |
</x-mail::table>

Thanks,<br>
{{ config('app.name') }}
</x-mail::message>

The button component accepts a url and an optional color of primary, success or error. The panel highlights a block of text, and the table component converts a Markdown table into an HTML table, including column alignment.

One rule matters more than any other: do not indent the Markdown. Markdown treats indented lines as code blocks, so a nicely indented template renders as grey preformatted text.

Step 4: Send the email

CSS
use App\Mail\WelcomeMail;
use Illuminate\Support\Facades\Mail;

Mail::to($user)->send(new WelcomeMail($user));

A typical place is a listener for the Registered event or the end of your registration action. Start a worker with php artisan queue:work so queued mail is delivered.

How do I preview a Markdown email in the browser?

Return the mailable from a route. Laravel renders it like a view, which makes design iteration fast. Keep the route local-only:

JavaScript
use App\Mail\WelcomeMail;
use App\Models\User;
use Illuminate\Support\Facades\Route;

if (app()->environment('local')) {
    Route::get('/mail-preview/welcome', fn () => new WelcomeMail(User::firstOrFail()));
}

How do I customize the email design?

Publish the mail components:

Code
php artisan vendor:publish --tag=laravel-mail

The components land in resources/views/vendor/mail, with html and text folders. Edit html/header.blade.php to add your logo, and change colours in html/themes/default.css; Laravel inlines that CSS when it renders the email. For a completely separate look, add a new CSS file in the same themes folder and set the theme option under markdown in config/mail.php to its name. A single mailable can use a different theme through its $theme property.

Should I use a mailable or a notification for this email?

A mailable fits emails with their own design and content, such as welcome emails, receipts and newsletters, especially when several people work on the template. A notification fits short status messages that might also go to Slack, SMS or the database. Both can use the same Markdown components, so the choice is about structure, not looks.

Common mistakes

  • Indented Markdown. Shows up as code blocks. Keep lines flush left inside components.
  • Relative URLs. Email clients have no base URL. Use route() or url(), and set APP_URL correctly, especially for queued mail.
  • Heavy images. Many clients block images by default. Make sure the email still makes sense without them.
  • No worker running. Queued mailables sit in the jobs table until a worker processes them.
  • Custom fonts and complex CSS. Email clients ignore much of modern CSS. Stick to the theme's simple styles.

How to verify it works

Preview the template in the browser, then send one real message to a mail catcher to check the rendering in a client. In automated tests, fake the mailer and assert on the content:

JavaScript
<?php

use App\Mail\WelcomeMail;
use App\Models\User;
use Illuminate\Support\Facades\Mail;

it('queues a welcome email', function () {
    Mail::fake();
    $user = User::factory()->create();

    Mail::to($user)->send(new WelcomeMail($user));

    Mail::assertQueued(WelcomeMail::class, fn (WelcomeMail $mail) => $mail->hasTo($user->email));
});

it('renders the dashboard button', function () {
    $user = User::factory()->create();

    (new WelcomeMail($user))
        ->assertSeeInHtml($user->name)
        ->assertSeeInHtml('Open your dashboard');
});

Because the mailable implements ShouldQueue, the first test uses assertQueued rather than assertSent.

FAQ

Can notifications use the same Markdown components?

Yes. In a notification's toMail(), return (new MailMessage)->markdown('mail.welcome', [...]) and the template can use the same x-mail:: components.

Is the plain-text version generated automatically?

Yes. Laravel renders the text version from the same template using the components in the text folder.

Can I attach files to a Markdown mailable?

Yes. Add an attachments() method that returns Attachment objects, exactly as with a view-based mailable.

References

Article Index & Topic Tags
LaravelEmailMailMarkdown

YouTube Channel

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Muhammad Yasir Amin
Available for Contracts
Author & Solutions Architect

Senior Full Stack Engineer

Independent Senior Full Stack Engineer working on Laravel, React and Next.js applications, APIs and delivery workflows.

Laravel & PHP ApplicationsReact & Next.js InterfacesREST APIs & IntegrationsDatabase Queries & DebuggingDocker Development WorkflowsClear Scope & Handover
Discuss Your Next Step

What Does Your Application Need Next?

Share your current system, the problem you want to solve and what a useful result looks like. We can identify the right starting point and agree on a practical scope.

Technical Audit

Find the Next Useful Fix

Application Development

Scoped, Reviewable Milestones

Ongoing Support

Agreed Capacity and Priorities

Share your goals, current stack and requirements before the call.