Mailable dari chapter sebelumnya mengurus logika; view-nya masih berupa teks polos. Email nyata butuh dua versi isi sekaligus: versi HTML untuk klien modern dan versi plain-text untuk klien lawas, spam filter yang membaca teks, serta pengguna yang menyukai ringan. Laravel memudahkan keduanya lewat opsi text di Content, plus satu senjata lain: Markdown mailable — template berbasis komponen mail::* yang otomatis menghasilkan HTML rapi dan plain-text sekaligus, lengkap dengan theming CSS. Pada chapter ini kita susun kedua gaya template dan membedah hasil render HTML-nya secara nyata.

Versi HTML dan Plain-Text #

Kita kembangkan mailable OrderShipped dengan dua view: emails.order-shipped untuk HTML dan emails.order-shipped-plain untuk teks:

php
public function content(): Content
{
    return new Content(
        view: 'emails.order-shipped',
        text: 'emails.order-shipped-plain',
    );
}

Kedua template tinggal berbeda gaya, tidak beda data — keduanya menerima properti mailable yang sama:

blade
{{-- resources/views/emails/order-shipped.blade.php --}}
Halo {{ $namaPembeli }},

Pesanan #{{ $orderId }} sudah kami kirim hari ini.
Estimasi tiba: 2-3 hari kerja.

Salam,
Toko Latihan
blade
{{-- resources/views/emails/order-shipped-plain.blade.php --}}
Halo {{ $namaPembeli }},

Pesanan #{{ $orderId }} sudah dikirim. Estimasi tiba 2-3 hari kerja.

Salam,
Toko Latihan

Setelah Mail::to('[email protected]')->send(new OrderShipped('123', 'Budi')) dijalankan di sandbox, berkas log memperlihatkan struktur pesan menjadi multipart:

Output yang Dihasilkan:

text
[2026-10-13 19:26:12] local.DEBUG: From: Toko Latihan <[email protected]>
To: [email protected]
Reply-To: [email protected]
Subject: Pesanan #123 Telah Dikirim
MIME-Version: 1.0
Date: Tue, 13 Oct 2026 19:26:12 +0000
Message-ID: <[email protected]>
Content-Type: multipart/alternative; boundary=qGWecAHw

--qGWecAHw
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable

Halo Budi,

Pesanan #123 sudah dikirim. Estimasi tiba 2-3 hari kerja.

Salam,
Toko Latihan

--qGWecAHw
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable

Halo Budi,

Header Content-Type: multipart/alternative dengan boundary pemisah menandai pesan dua-versi. Klien email memilih salah satu bagian sesuai kemampuannya: plain-text untuk klien lawas, HTML untuk klien modern. Isi versi teks persis dari view -plain — Laravel tidak membuatnya otomatis, jadi jika text tidak diisi, email hanya membawa HTML.

Markdown Mailable dengan Komponen mail:: #

Menulis HTML email dari nol menyiksa: table layout, inline CSS di semua elemen, kompatibilitas Outlook. Laravel membungkus semua itu ke dalam komponen mail::*. Kita buat class OrderReceipt dengan Content::markdown():

php
class OrderReceipt extends Mailable
{
    use Queueable, SerializesModels;

    public function __construct(
        public string $orderId,
        public array $items,
        public int $total = 0,
    ) {}

    public function envelope(): Envelope
    {
        return new Envelope(
            subject: "Struk Pesanan #{$this->orderId}",
        );
    }

    public function content(): Content
    {
        return new Content(
            markdown: 'emails.order-receipt',
        );
    }
}

Template markdown memakai komponen bawaan — x-mail::message sebagai wadah, x-mail::table untuk tabel, x-mail::button untuk tombol:

blade
{{-- resources/views/emails/order-receipt.blade.php --}}
<x-mail::message>
## Struk Pesanan #{{ $orderId }}

Berikut ringkasan pesanan Anda:

<x-mail::table>
| Produk | Jumlah | Harga |
| ------ | ------ | ----- |
@foreach ($items as $item)
| {{ $item['produk'] }} | {{ $item['jumlah'] }} | Rp{{ number_format($item['harga']) }} |
@endforeach
</x-mail::table>

@if ($total >= 500000)
Produk di atas sudah termasuk gratis ongkir. Selamat!
@endif

<x-mail::button :url="url('/pesanan/' . $orderId)">
Lihat Detail Pesanan
</x-mail::button>

Terima kasih,<br>
{{ config('mail.from.name') }}
</x-mail::message>

Directive Blade biasa tetap bekerja: @foreach mengisi baris tabel dari array $items, @if menampilkan pesan gratis ongkir bila total melewati ambang, dan heading ## di-render jadi <h2> (kita verifikasi sebentar lagi). Tombol menerima URL lewat binding :url dengan ekspresi PHP.

Membedah Hasil Render #

Rendering template dijalankan lewat render() untuk melihat HTML jadi:

php
$mail = new OrderReceipt('777', [
    ['produk' => 'Buku Laravel', 'jumlah' => 2, 'harga' => 350000],
    ['produk' => 'Stiker Elephant', 'jumlah' => 5, 'harga' => 15000],
], 775000);
$html = $mail->render();

echo 'panjang_html: ' . strlen($html) . " bytes\n";
echo 'ada_tabel: ' . var_export(str_contains($html, '<table'), true) . "\n";
echo 'ada_button: ' . var_export(str_contains($html, 'Lihat Detail Pesanan'), true) . "\n";
echo 'ada_gratis_ongkir: ' . var_export(str_contains($html, 'gratis ongkir'), true) . "\n";

Output yang Dihasilkan:

text
panjang_html: 13489 bytes
ada_tabel: true
ada_button: true
ada_gratis_ongkir: true

Tiga belas ribu byte untuk email “sederhana” menunjukkan beban kompatibilitas email: setiap elemen membawa inline CSS (font stack, box-sizing, padding) karena sebagian besar klien email mengabaikan <style> terpisah. Potongan tabel berikut adalah render nyata untuk baris pertama:

html
<tr> <td style="...">Buku Laravel</td>
     <td style="...">2</td>
     <td style="...">Rp350,000</td> </tr>

Tombol pun menjadi tabel berlapis dengan link bergaya button-primary:

html
<a href="http://localhost:8000/pesanan/777" class="button button-primary"
   target="_blank" rel="noopener" style="... background-color: #18181b; ...">
Lihat Detail Pesanan</a>

Warna #18181b berasal dari tema default. Untuk identitas visual sendiri, kita terbitkan tema: salin CSS ke resources/views/vendor/mail/html/themes/custom.css, lalu set properti $theme di mailable (atau mail.markdown.theme di config). Runner pembuktian dengan tema berlatar #f0fdf4 menghasilkan pakai_bg_hijau_muda: true — tema kustom dipakai tanpa menyentuh file vendor.

Memeriksa Template Lewat Mail::fake #

Saat menulis template, kita ingin tahu isinya benar tanpa membaca log berulang. Fake test memungkinkan inspeksi: closure assertSent menerima mailable, dan render() di dalamnya bisa diperiksa:

php
Mail::assertSent(OrderShipped::class, function (OrderShipped $mail) {
    return $mail->envelope()->subject === 'Pesanan #123 Telah Dikirim'
        && $mail->hasTo('[email protected]');
});

Pola ini kita perdalam di chapter Pengujian Email . Yang perlu dicatat sekarang: template bercabang (@if, @foreach) dan theming semuanya bisa diverifikasi lewat assertion pada string HTML, sehingga regresi tampilan tertangkap sebelum produksi.

Template siap, konten siap — tinggal satu kebutuhan transaksional yang belum: berkas. Chapter Email Attachments membahas melampirkan file dari path, storage, sampai data mentah. Daftar komponen markdown lengkapnya ada di dokumentasi Mail .