$shibayu36->blog;

株式会社はてなでエンジニアをしています。プログラミングや読書のことなどについて書いています。

XslateのテンプレートをCSSインライン化する

HTMLメールを送信する時、クライアントによってはlinkでのcss指定やstyleタグを解釈しない場合があります。その場合、CSSをHTML要素にインライン化しないといけないのですが、今回はこのことについて書いてみます。

CSS::Inlinerを使う

perlにはCSS::Inlinerというモジュールがあって、これを使えばstyleタグの入ったhtmlに対して、CSSインライン化することが出来ます。

例えば以下の様なHTMLがあった場合

<html>
  <head>
    <style type="text/css">body { margin: 0px; } #a { padding: 0px; }</style>
  </head>
  <body>
    <p id="a">hoge</p>
    <p id="b">fuga</p>
  </body>
</html>

以下の様なコードを書けば

my $inliner = CSS::Inliner->new;
$inliner->read({ html => $html });
print $inliner->inlinify;

このようにInline化されます。後はこのHTMLを使って送信すればメールクライアントにスタイルを解釈させることが出来ます。

<html>
  <head></head>
  <body style="margin: 0px;">
    <p id="a" style="padding: 0px;">hoge</p>
    <p id="b">fuga</p>
  </body>
</html>

CSS::Inlinerを使う場合の問題

通常の場合はCSS::Inlinerを使えば問題ありませんが、問題はメール送信数が多くなってくると、CSSのインライン化がかなりの時間を使ってしまうという事です。

htmlの大きさやCSSの大きさにもよりますが、手元のPCで適当に100通くらいCSSのインライン化をしてみたら、大体6秒くらい時間がかかりました。もしこの速度で実行されたとすると、1000通で1分、10000通で10分と無視できない感じになってきます。

そこで送信前にXslateのテンプレートに対してCSSインライン化をし、その後renderするということを考えます。

Xslateのテンプレートをインライン化する

Xslateのテンプレートをインライン化することにより、10000通送る時でも最初の一回だけインライン化すれば済むことになります。

この時に以下の様なことが問題になります。

  • そのままインライン化すると、XslateのタグなどがHTML Entity化されてしまう
  • HTML Entity化される対象の文字を少なくするとCSSによってはHTMLが壊れる

そこで今回は一旦Xslate上のタグをHTML Entity化されない文字のプレースホルダーに置き換え、CSSインライン化した後、プレースホルダーをもとに戻すという方法を試してみました*1

まずxslateのテンプレートとして以下の様なものを使います。

my $template = <<'XSLATE';
<html>
  <head>
    <style type="text/css">body { margin: 0px; } #a { padding: 0px; }</style>
  </head>
  <body>
    [% IF 0 %]
    <p id="a">hoge</p>
    [% END # IF 0 %]
    <p id="b">fuga</p>
  </body>
</html>
XSLATE

その後以下の様にプレースホルダー変換、インライン化、プレースホルダー逆変換をします。

# [% %]のXslateのsyntaxを[mail_template_compiler_placeholder:1]
# のようなプレースホルダーに置き換える
my $placeholder_to_syntax = {};
my $placeholder_count     = 0;
$template =~ s{(\[%.+?%\])}{
    my $syntax = $1;
    my $placeholder = "[mail_template_compiler_placeholder:$placeholder_count]";
    $placeholder_to_syntax->{$placeholder} = $syntax;
    $placeholder_count++;
    $placeholder;
}gse;

# CSSインライン化
my $inliner = CSS::Inliner->new;
$inliner->read({ html => $template });
$template = $inliner->inlinify;

# placeholderをXslateのsyntaxに戻す
$template =~ s{(\[mail_template_compiler_placeholder:\d+\])}{
    my $placeholder = $1;
    my $syntax = $placeholder_to_syntax->{$placeholder};
}gse;

print $template;

すると以下のようにインライン化されます。

<html>
  <head> </head>
  <body style="margin: 0px;">
    [% IF 0 %]
    <p id="a" style="padding: 0px;">hoge</p>
    [% END # IF 0 %]
    <p id="b">fuga</p>
  </body>
</html>

あとはこれを使ってXslateでrenderすることで、たくさんのメールを毎回インライン化することなく、HTMLメール送信することが出来ます。

まとめ

今回はHTMLメールを送る時のCSSインライン化について書いてみました。Xslateのテンプレートをインライン化するものはバッドノウハウ感が高いので、何かしら良い方法があれば教えて貰いたいです。

*1:かなりバッドノウハウ感ありますが