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:
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:
{{-- 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{{-- resources/views/emails/order-shipped-plain.blade.php --}}
Halo {{ $namaPembeli }},
Pesanan #{{ $orderId }} sudah dikirim. Estimasi tiba 2-3 hari kerja.
Salam,
Toko LatihanSetelah Mail::to('[email protected]')->send(new OrderShipped('123', 'Budi')) dijalankan di sandbox, berkas log memperlihatkan struktur pesan menjadi multipart:
Output yang Dihasilkan:
[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():
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:
{{-- 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:
$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:
panjang_html: 13489 bytes
ada_tabel: true
ada_button: true
ada_gratis_ongkir: trueTiga 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:
<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:
<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:
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 .