Email teks murni aman, tetapi pesan penting seperti nota pesanan atau tautan verifikasi sering perlu tampilan yang lebih kaya — logo, tombol, hierarki informasi. Di sinilah keinginan kita berbenturan dengan realitas: klien email bukan browser modern, dan HTML email punya aturan mainnya sendiri yang terasa seperti kembali ke era awal 2000-an. Chapter ini membahas struktur multipart/alternative, tata letak berbasis tabel dengan CSS inline, sampai trik preheader dan kesiapan mode gelap.

Realitas Klien Email #

Browser modern merender CSS dengan baik, tetapi klien email tidak. Gmail kerap membuang elemen <style> di bagian tertentu, Outlook desktop merender email dengan mesin Word yang dukungan CSS-nya terbatas, dan sebagian klien memotong email panjang. Akibatnya, praktik industri HTML email masih memakai: tabel untuk tata letak, atribut HTML waras seperti width dan cellpadding, serta CSS yang ditulis inline pada setiap elemen. Media query dan flexbox hanya bisa diandalkan pada sebagian klien, jadi desain harus tetap terbaca bahkan bila semua CSS gagal.

multipart/alternative: Dua Versi Satu Pesan #

Cara standar mengirim dua versi isi — teks murni dan HTML — adalah struktur MIME multipart/alternative: pesan berisi beberapa part yang dipisah string boundary, dan klien memilih versi terbaik yang bisa ditampilkan. Mari kita bangun pembentuknya sebagai fungsi murni:

php
<?php
function buatEmailMultipart(
    string $dari,
    string $kepada,
    string $subjek,
    string $teks,
    string $html,
    string $boundary
): string {
    $header = "From: $dari\r\n"
        . "To: $kepada\r\n"
        . "Subject: $subjek\r\n"
        . "MIME-Version: 1.0\r\n"
        . "Content-Type: multipart/alternative; boundary=\"$boundary\"\r\n";

    $pesan = "--$boundary\r\n"
        . "Content-Type: text/plain; charset=UTF-8\r\n"
        . "Content-Transfer-Encoding: 8bit\r\n"
        . "\r\n"
        . $teks . "\r\n"
        . "--$boundary\r\n"
        . "Content-Type: text/html; charset=UTF-8\r\n"
        . "Content-Transfer-Encoding: 8bit\r\n"
        . "\r\n"
        . $html . "\r\n"
        . "--$boundary--";

    return $header . "\r\n" . $pesan;
}

$pesan = buatEmailMultipart(
    "[email protected]",
    "[email protected]",
    "Pesanan #1234 Terkirim",
    "Halo Budi, pesanan #1234 sudah kami kirim.",
    "<p>Halo Budi, pesanan <strong>#1234</strong> sudah kami kirim.</p>",
    "=_batas_604f2a"
);

echo str_replace("\r\n", "\\r\\n\n", $pesan);

Output yang Dihasilkan:

text
From: [email protected]\r\n
To: [email protected]\r\n
Subject: Pesanan #1234 Terkirim\r\n
MIME-Version: 1.0\r\n
Content-Type: multipart/alternative; boundary="=_batas_604f2a"\r\n
\r\n
--=_batas_604f2a\r\n
Content-Type: text/plain; charset=UTF-8\r\n
Content-Transfer-Encoding: 8bit\r\n
\r\n
Halo Budi, pesanan #1234 sudah kami kirim.\r\n
--=_batas_604f2a\r\n
Content-Type: text/html; charset=UTF-8\r\n
Content-Transfer-Encoding: 8bit\r\n
\r\n
<p>Halo Budi, pesanan <strong>#1234</strong> sudah kami kirim.</p>\r\n
--=_batas_604f2a--

Mari kita bedah struktur yang dihasilkan:

  1. Header Content-Type: multipart/alternative; boundary="...": mendeklarasikan bahwa pesan punya beberapa versi isi dan menetapkan string pemisahnya.
  2. --$boundary: setiap part dibuka dengan garis batas; part pertama berisi versi teks murni dengan header kecilnya sendiri.
  3. Part kedua: versi HTML dengan Content-Type: text/html, dipisah garis batas yang sama.
  4. --$boundary--: penutup dengan dua tanda minus di akhir menandakan pesan selesai.
  5. Urutan part: teks murni diletakkan lebih dulu, HTML kemudian — klien mengambil part terakhir yang mampu ditampilkannya, sehingga pengguna HTML melihat HTML dan pengguna teks melihat teks.

Sama seperti chapter sebelumnya, CRLF ditampilkan sebagai teks \r\n agar struktur terbaca. Bedah baris-per-baris fungsi ini juga menunjukkan mengapa library email sangat membantu: perakitnya rawan salah, dan satu boundary yang kacau membuat seluruh pesan rusak.

Tata Letak dengan Tabel dan CSS Inline #

Berikut kerangka HTML email yang mengikuti praktik industri, lengkap dengan preheader tersembunyi dan gambar ber-alt:

html
<!DOCTYPE html>
<html lang="id">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Pesanan Terkirim</title>
</head>
<body style="margin: 0; padding: 0; background-color: #f4f4f7;">
  <!-- preheader: cuplikan yang tampil di samping subjek pada daftar inbox -->
  <span style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">
    Pesanan #1234 Anda sudah dalam perjalanan.
  </span>
  <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color: #f4f4f7;">
    <tr>
      <td align="center" style="padding: 24px 12px;">
        <table role="presentation" width="600" cellpadding="0" cellspacing="0" border="0" style="width: 600px; max-width: 100%; background-color: #ffffff; border-radius: 8px;">
          <tr>
            <td style="padding: 32px; font-family: Arial, Helvetica, sans-serif; color: #222222;">
              <img src="https://cdn.toko.example/logo.png" alt="Logo Toko Example" width="120" style="display: block; border: 0;">
              <h1 style="margin: 24px 0 8px; font-size: 20px; line-height: 28px;">Pesanan Terkirim</h1>
              <p style="margin: 0 0 12px; font-size: 14px; line-height: 22px;">
                Halo Budi, pesanan <strong>#1234</strong> sudah kami kirim hari ini.
              </p>
              <p style="margin: 0; font-size: 14px; line-height: 22px;">
                Estimasi tiba: dua hari kerja. Terima kasih sudah berbelanja!
              </p>
            </td>
          </tr>
        </table>
      </td>
    </tr>
  </table>
</body>
</html>

Tabel terluar mengisi seluruh lebar sebagai latar, tabel kedua selebar 600 piksel menjadi kartu konten, dan setiap sel membawa CSS inline miliknya sendiri. Atribut role="presentation" memberi tahu pembaca layar bahwa tabel ini murni tata letak, bukan tabel data. Lebar 600 piksel adalah konvensi karena mayoritas klien menampilkan area baca selebar itu; max-width: 100% menjaga kartu tetap muat di layar ponsel.

Preheader, Gambar, dan Mode Gelap #

Tiga detail kecil sering dilupakan padahal berdampak besar. Pertama, preheader: cuplikan di sebelah subjek pada daftar inbox diambil dari teks pertama email, sehingga kita menyisipkan span tersembunyi berisi ringkasan satu baris — properti mso-hide: all tambahan menyembunyikannya di Outlook. Kedua, sebagian besar klien memblokir gambar eksternal secara bawaan; selalu sediakan alt yang bermakna, ukuran width/height agar tata letak tidak melompat, dan jangan menaruh informasi penting hanya di dalam gambar. Ketiga, mode gelap: klien bisa membalik warna latar sehingga kombinasi warna kontras lembut (misalnya abu muda di atas putih) bisa berubah tak terbaca — uji desain dengan warna yang masih nyaman dibaca setelah dibalik.

Untuk menguji tampilan lintas klien, tim kecil biasanya mengirim pratinjau ke akun uji di Gmail, Outlook, dan Apple Mail, serta memakai pratinjau MailHog saat pengembangan lokal. Layanan berbayar seperti Email on Acid atau Litmus menyediakan screenshot di puluhan klien sekaligus bila anggaran memungkinkan.

Versi Teks Murni Otomatis #

Menulis dua versi isi secara terpisah rawan tidak sinkron. Untuk email sederhana, versi teks bisa diturunkan dari HTML secara otomatis:

php
<?php
function teksDariHtml(string $html): string
{
    $teks = preg_replace('/\s+/', ' ', $html);
    $teks = strip_tags($teks);
    $teks = html_entity_decode($teks, ENT_QUOTES, "UTF-8");

    return trim($teks);
}

$html = "<h1>Pesanan Terkirim</h1> <p>Halo Budi, pesanan <strong>#1234</strong> sudah kami kirim. Estimasi tiba: dua hari kerja.</p>";

echo teksDariHtml($html) . "\n";

Output yang Dihasilkan:

text
Pesanan Terkirim Halo Budi, pesanan #1234 sudah kami kirim. Estimasi tiba: dua hari kerja.

Langkah pertama meratakan semua whitespace menjadi satu spasi, langkah kedua membuang seluruh tag dengan strip_tags(), dan langkah ketiga mengubah entitas seperti &amp; kembali menjadi karakter aslinya. Hasilnya satu paragraf bersih yang siap menjadi part text/plain. Untuk email kompleks — tombol, kolom, daftar — versi otomatis seperti ini kurang rapi; chapter berikutnya menunjukkan pendekatan template yang menyediakan kedua versi secara eksplisit.

Pemeriksa Kecil untuk HTML Email #

Sebelum mengirim, kita bisa memeriksa HTML email terhadap kebiasaan yang berisiko. Fungsi berikut menjadi semacam linter mini yang teruji:

php
<?php
function periksaHtmlEmail(string $html): array
{
    $masalah = [];
    if (str_contains($html, "<style")) {
        $masalah[] = "tag <style> cenderung dibuang klien email";
    }
    if (preg_match('/\bclass="/', $html)) {
        $masalah[] = "atribut class tidak berguna tanpa <style> yang hidup";
    }
    if (!str_contains($html, 'role="presentation"')) {
        $masalah[] = "tabel tata letak sebaiknya memakai role=presentation";
    }

    return $masalah;
}

$html = "<style>.judul{color:#222}</style>"
    . "<table class=\"layout\"><tr><td>Halo Budi</td></tr></table>";

foreach (periksaHtmlEmail($html) as $masalah) {
    echo "- " . $masalah . "\n";
}

Output yang Dihasilkan:

text
- tag <style> cenderung dibuang klien email
- atribut class tidak berguna tanpa <style> yang hidup
- tabel tata letak sebaiknya memakai role=presentation

Fungsi periksaHtmlEmail() mengembalikan daftar catatan alih-alih sekadar boolean, sehingga pemanggil tahu persis apa yang harus diperbaiki. Contoh masukan sengaja berisi tiga pelanggaran sekaligus — <style>, atribut class, dan tabel tanpa role="presentation" — dan ketiganya tertangkap. Pemeriksa semacam ini cocok dimasukkan ke pengujian otomatis sebelum email dikirim ke production.

Ringkasan #

HTML email hidup di dunia tabel dan CSS inline karena klien email merender pesan dengan mesin yang jauh lebih terbatas dari browser. Struktur multipart/alternative menyediakan versi teks dan HTML dalam satu pesan dengan pemisah boundary, preheader dan alt gambar menjaga pengalaman di daftar inbox, dan pemeriksa kecil menjaga disiplin desain tetap teruji. Dengan bentuk pesan yang baik di tangan, tugas berikutnya adalah mengelola isi puluhan jenis email tanpa salinan yang berantakan — melalui template.

Referensi #