弁護士ドットコム株式会社 Creators’ blog

弁護士ドットコムがエンジニア・デザイナーのサービス開発事例やデザイン活動を発信する公式ブログです。

Go html/templateで書くHTMLメールのハマりどころと対処法

この記事は弁護士ドットコム Advent Calendar 2023の 8 日目の記事です。

前日は @RA621H さんの「はじめての転職と今後の展望」でした。

クラウドサインのフロントエンドエンジニアの @RINYU_DRVO です。
今回は、Go html/template で書く HTML メールのハマりどころと対処法を紹介します。

この記事で伝えたいこと

  • HTML メールはフロントエンドエンジニアから見ると、html を書くだけの簡単な仕事に見えるが、実際そうではない
  • Go html/template で書く HTML メールのハマりどころと対処法を紹介する

背景

技術スタック

クラウドサインは、日本の法律に特化した弁護士監修の電子契約サービスです。
当サービスのシステムのバックエンドは Go 言語で書かれています。
(Go 言語を選定した背景は Go言語でのサービス開発に挑戦した話 をご覧ください)

そして、クラウドサインからのさまざまな通知は HTML メールで送信していますが、メールのレンダリングには Go html/template を使っています。
(HTML メールと対になるテキストメールも存在しており、そちらは Go text/template を使用しています)

新規開発

今回、クラウドサインに新たな機能が追加されるにあたり、その機能に対応して HTML メールのパターンを増やす改修が必要になりました。
そこで、以下の理由でフロントエンドエンジニアが設計・実装を担当することになりました。

  • サーバーサイドのコードではあるがユーザーに見える部分であるため
  • メールの内容が、おおよそ HTML と CSS で構成されているため
  • バックエンドエンジニアの負荷を減らすため

課題

改修の中で下記の課題が壁となりました。

  • ユニットテストが書けない
  • パーツ間の情報伝達の方法がフロントエンドフレームワークと違う
  • メーラー側の仕様に合わせる必要がある
  • 多言語対応が容易ではない
  • CSS をパーツごとに置くことができない

これらの問題が具体的にどう辛く、どのように対処したかを紹介します。

詳細

ユニットテストが書けない

つらかったこと

あいにく社内では、 Go html/template 用のテンプレートに対するユニットテストを書くための有効なプラクティスがありませんでした。
そのため、テストを手動で行う必要があり、特にリグレッションテストの手間が大変なことになります。

対処

ユニットテストは書けませんが、手動で HTML メールファイルを生成する自作ツールを利用しました。
それにより、テストの確認の手間を減らすことができました。

メール生成を行う自作ツールを使った場合、下記のようなコマンドの実行により HTML メールファイルを生成できます。
画面操作をすることなくメール内容を確認できるので、テストの手間を減らすことができます。

# 締結完了通知(complete)メールのhtmlファイルを生成する
$ cd To/Tool/Dir
$ go run . complete html > complete.html

自作ツールの詳細はこちらの記事をご覧ください。
弁護士ドットコムCreator's Blog - メールの修正を容易に確認できるツールを作成してみた

テンプレート間の情報伝達の方法がフロントエンドフレームワークと違う

つらかったこと

既存の Go テンプレート中には、親テンプレートの定義ファイル中で定義された値を、子テンプレート中で template もしくは block により参照するというような使い方をしているところがありました。
以下の例のような場合で、親テンプレート 1 または親テンプレート 2 を選んでレンダリングすることにより、子テンプレートのレンダリングする結果が変わります。

親テンプレート1
{{ define "親テンプレート1" }}
    {{ template "子テンプレート" . }}
{{ end }}

{{ define "子テンプレート埋め込み要素" }}
    子テンプレートに埋め込む内容1
{{ end }}
親テンプレート2
{{ define "親テンプレート2" }}
    {{ template "子テンプレート" . }}
{{ end }}

{{ define "子テンプレート埋め込み要素" }}
    子テンプレートに埋め込む内容2
{{ end }}
子テンプレート
{{ define "子テンプレート" }}
    {{ template "子テンプレート埋め込み要素" }}
{{ end }}

Vue や React では、親から子に情報を渡すときに props を使うことと比較すると、これは Vue で言うところの slot に近いですね。
この違いにより、フロントエンドエンジニアが Go テンプレートで書かれた HTML メール用のテンプレートを読むのに時間がかかりました。

対処

そこで、フロントエンドエンジニアとバックエンドエンジニアが Go html/template を使用したメール実装を用いてモブプロをしました。
フロントエンドエンジニアがタイピストとなり、バックエンドエンジニアが指示を出す形で行いました。

それによりバックエンドエンジニアが持っていた Go html/template に関する知見を共有し、フロントエンドエンジニアがソースコードを読むのにかかる時間を短縮できました。

また HTML メールの辛いところをチーム全体で共有でき、工数の考慮や設計にも役立てることができました。

メーラー側の仕様に合わせる必要がある

つらかったこと

これは Go html/template の問題ではないですが、HTML メールをレンダリングするユーザーエージェント (メーラー) の中に CSS のサポートの悪いものがあるため、テーブルレイアウトを利用せざるを得ない場合があります。

普段 HTML5/CSS3 を用いて構成を考えているフロントエンドエンジニアにとっては、これがハードルになりました。

以下はテーブルレイアウトで組まれた HTML メールの例です。

{{define "Base" }}
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <!-- meta/style等 -->
</head>
<body>
    <table>
        <tr>
            <td>
                <center>
                    <table>
                        <tr>
                            <td>
                                <!-- ヘッダー -->
                            </td>
                        </tr>
                        <tr>
                            <td>
                                <!-- ボディ -->
                            </td>
                        </tr>
                        <tr>
                            <td>
                                <!-- フッター -->
                            </td>
                        </tr>
                    </table>
                </center>
            </td>
        </tr>
    </table>
</body>
{{ end }}

対処

ここに対しては試行錯誤を繰り返す他ありませんでした。
ボタンをテンプレートに切り出して配置しようとしても、ボタンが想定外の場所に配置されるなど、テーブルレイアウトに慣れるには時間がかかりました。

多言語対応が容易ではない

つらかったこと

クラウドサインでは書類を送信する際、日本語以外にも 英語中国語(繁体字、簡体字) の 4 言語に対応しています。
そのため、HTML メールも 4 言語に対応する必要があります。

しかし Go html/template では、多言語対応をするための仕組みがありません。
そのために、言語の数だけテンプレートを用意する必要があります。
すると、改修や新規作成の場合も言語の数だけ修正箇所が発生してしまいます。

以下はディレクトリ構成の例です。

mail
└── template
    ├── ja
    │   ├── button.html.tmpl
    │   └── body.html.tmpl
    ├── en 
    │   └── ...
    ├── zh-CHT
    │   └── ...
    └── zh-CHS
        └── ...

対処

現状は、愚直に言語の数だけテンプレートを用意する以外に取れる方法はありませんでしたが、
言語ごとに実装チケットを作成し、対処漏れを防ぐよう工夫しました。

CSSをパーツごとに置くことができない

つらかったこと

Vue.js や React.js では、コンポーネントごとに CSS を置きカプセル化する手法があります(Scoped CSS)。
しかし、Go html/template では、コンポーネントごとに CSS を置くことができません。

以下の例のような実装方法ができません。

CSS ファイル
.button {
    color: red;
}
ボタンテンプレート
{{ define "Button" }}
<a>
    <div class="button">
        ボタンラベル
    </div>
</a>

<link rel="stylesheet" href="button.css">
{{ end }}

そのため、現状では要素にスタイルをベタ書きするか、もしくは一番上位の <head> を指定しているテンプレートにスタイルを書くことになります。

対処

スタイルは一番上位のテンプレートに書かざるを得ません。
しかし、そこに定義したスタイルのクラスを Go html/template の条件分岐の機能により出し分けられることが確認できました。

そのため、子テンプレートのスタイルが分岐する場合は親テンプレートからクラスを指定する作りにしました。
それにより、要素の <style> 属性の中でスタイルを分岐させることを回避し、少しでもメンテナンスしやすいコードにしています。

ベーステンプレート
{{ define "Base" }}
<head>
    <style type="text/css">
        .button {
            font-size: 12px;
        }
        .button--red {
            color: red;
        }
        .button--yellow {
            color: yellow;
        }
    </style>
</head>
<body>
    <!--  -->
</body>
{{ end }}
ボタンテンプレート
{{ define "Button" }}
<a>
    <div class="{{ block "ButtonClass" .}}.button{{ end }}">
        ボタンラベル
    </div>
</a>
{{ end }}
ボタンを使用するテンプレート
{{ define "ParentTemplate" }}
    {{ template "Button" . }}
{{ end }}

{{ define "ButtonClass" }}
    {{ if eq .Color "red" }}
        button--red
    {{ else if eq .Color "yellow" }}
        button--yellow
    {{ end }}
{{ end }}

これにより子テンプレートのスタイルを親の指示で分岐させることができました。

今後やりたい改善

現状のままだと、HTML メールの改修や新規作成に時間がかかってしまいます。
いろいろな改善策を講じましたが、根本的にはより見通しがよくテスト可能な仕組みへの改修が必要だと感じました。

そこで、今後は HTML メールを Vue.js で生成する方法を模索していきたいと考えています。
メール生成にはVueEmailというライブラリを利用することで、Vue.js で HTML メールを生成できます。

サーバーサイドの情報をどう Vue.js のコンポーネントに渡すかなどの課題は依然として存在します。
しかしこの改善をすることにより、 HTML メールの開発をより効率的にできると考えています。

まとめ

今回は、普段 Vue.js を使っていて、Go を全く知らなかったフロントエンドエンジニアが Go テンプレートを使った HTML メールの生成に挑戦しました。
その過程でハマったところと、対処したところにはその対処法を紹介しました。
今後 HTML メールを Go 言語で作成される場合は、ぜひこの記事のナレッジを参考にしてください。

弁護士ドットコム Advent Calendar 2023 の明日の担当は @taaag51 さんの「Customer Reliability Engineering について考えたこと、気をつけていること」です。