HTML メールと向き合う
仕事で HTML メールを書く必要が出てきたので調査してみる。
HTML メール界隈、思っていたより魔境そう…
スタイルはインラインで記述する
レイアウトは <table> タグで行う
table レイアウト?マジで?
まともにゼロから書くのは人間の所業ではないので別の手段を探す。
フレームワークがいくつかあるっぽい。
React で書いたコンポーネントを HTML メールの仕様に沿ったマークアップに変換。
こちらも同様だが、独自のマークアップ言語。歴史は長い。
メーラーの HTML / CSS 対応状況は Can I use の HTML メール版があるのでそれで確認する。
とりあえず React Email 触ってみる
import { Container } from '@react-email/container';
import { Head } from '@react-email/head';
import { Html } from '@react-email/html';
import { Section } from '@react-email/section';
import { Text } from '@react-email/text';
import * as React from 'react';
export default function Email() {
return (
<Html>
<Head />
<Section style={main}>
<Container style={container}>
<Text>hello</Text>
</Container>
</Section>
</Html>
);
}
const main = {
backgroundColor: '#ffffff',
};
const container = {
paddingLeft: '12px',
paddingRight: '12px',
margin: '0 auto',
};
これが
<!DOCTYPE htmlPUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
</head>
<table style="width:100%;background-color:#ffffff" align="center" border="0" cellPadding="0" cellSpacing="0" role="presentation">
<tbody>
<tr>
<td>
<div><!--[if mso | IE]>
<table role="presentation" width="100%" align="center" style="max-width:37.5em;padding-left:12px;padding-right:12px;margin:0 auto;"><tr><td></td><td style="width:37.5em;background:#ffffff">
<![endif]--></div>
<div style="max-width:37.5em;padding-left:12px;padding-right:12px;margin:0 auto">
<p style="font-size:14px;line-height:24px;margin:16px 0">hello</p>
</div>
<div><!--[if mso | IE]>
</td><td></td></tr></table>
<![endif]--></div>
</td>
</tr>
</tbody>
</table>
</html>
こうなる。黒魔術感がすごい。
React Email をしばらく触っていたものの、まだ Beta ということもあり、挙動が若干 buggy かもしれない。ということで mjml も試す。
テンプレートはこんな感じ。
<mjml>
<mj-body>
<mj-section>
<mj-column>
<mj-image width="100px" src="/assets/img/logo-small.png"></mj-image>
<mj-divider border-color="#F45E43"></mj-divider>
<mj-text font-size="20px" color="#F45E43" font-family="helvetica">Hello World</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
吐き出される HTML。
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>
</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<style type="text/css">
@media only screen and (min-width:480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
}
</style>
<style media="screen and (min-width:480px)">
.moz-text-html .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
@media only screen and (max-width:480px) {
table.mj-full-width-mobile {
width: 100% !important;
}
td.mj-full-width-mobile {
width: auto !important;
}
}
</style>
</head>
<body style="word-spacing:normal;">
<div style="">
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
<tbody>
<tr>
<td style="width:100px;">
<img height="auto" src="/assets/img/logo-small.png" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;" width="100" />
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<p style="border-top:solid 4px #F45E43;font-size:1px;margin:0px auto;width:100%;">
</p>
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 4px #F45E43;font-size:1px;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;">
</td></tr></table><![endif]-->
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:helvetica;font-size:20px;line-height:1;text-align:left;color:#F45E43;">Hello World</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>
パッと見、かなり冗長だが、とりあえず mjml で進めてみることにする。
たとえばこういう感じの、1 カラム + 3 カラムの組み合わせからなるレイアウトを mjml で組む時、
<mj-section>
<mj-column width="100%">
<mj-text>A</mj-text>
</mj-column>
<mj-column width="33.3333333333%">
<mj-text>B</mj-text>
</mj-column>
<mj-column width="33.3333333333%">
<mj-text>C</mj-text>
</mj-column>
<mj-column width="33.3333333333%">
<mj-text>D</mj-text>
</mj-column>
</mj-section>
とすると、プレビューでは一見問題なさそうに見える。が、Windows の Outlook で確認したときに盛大にレイアウトが崩れてしまう。
こういう場合はふつうに section を分ける。2 段以上のカラムになる場合は素直に section を分けたほうがよさそう。
<mj-section>
<mj-column>
<mj-text>A</mj-text>
</mj-column>
</mj-section>
<mj-section>
<mj-column>
<mj-text>B</mj-text>
</mj-column>
<mj-column>
<mj-text>C</mj-text>
</mj-column>
<mj-column>
<mj-text>D</mj-text>
</mj-column>
</mj-section>
検証には litmus が便利。主要なメーラーとデバイス(macOS / Win / iOS / Android)の組み合わせでレンダリングされた結果をスクショしてくれる。ただし有料で、ベーシックプランでも $99/month という強気の価格設定。
Outlook で確認したときに想定外の罠に陥る可能性があるので、実機による検証環境が用意できない場合はなるべく早い段階で確認しておくべき。Outlook はとにかく曲者。
Outlook で line-height がおかしい
テキストの行間がやたらと広くなる。
CSS で mso-line-height-rule: exactly
を指定すればよいらしいのだが、mjml はデフォルトで mso-line-height-rule: exactly
が付与される。どうも line-height が % 指定だとダメっぽいので、px 指定にすることで解決。
Yahoo! メールでインライン CSS の height が勝手に min-height に書き換えられる
仕様らしい。いやどんな仕様だよ。
mjml だと(確認した限りでは)mj-button の height が無視されて潰れたような見た目になってしまう。height: this.getAttribute('height')
を追加。
参考
mjml 使う上でのコツとしては、
- なるべく
mj-
ではじまるビルトインのコンポーネントを使う。 - スタイルの調整も、用意されているプロパティの範囲内で行う。CSS を直接書くのは避けたほうがいい。
-
mj-text
やmj-table
で HTML を直接書くこともできるが、Outlook で崩れたり、うまくいかないことのほうが多いので避けたほうがいい。 - 画像化できる部分は画像化する。