読者です 読者をやめる 読者になる 読者になる

タイアップページでのLiquidの利用について

こんにちは。広告事業部の鈴木です。

皆さんはLiquidと呼ばれるテンプレートエンジンをご存知でしょうか? LiquidShopifyのメンバーを中心として開発されているテンプレートエンジンの一種で、 最近ではJekyllに使われていることでも知られています。 クックパッドの広告事業部では、広告商品の一つであるタイアップページ*1でこのLiquidを活用しています。

例. マルちゃん焼そば弁当コンテスト! [クックパッド] 簡単おいしいみんなのレシピが199万品

このタイアップページでは、そこからリンクされているレシピ応募コンテストページにユーザさんが投稿してくれたレシピ数を表示するのにLiquidが使われています。

f:id:szkmp:20150313165147p:plain

タイアップページにはデータベースに保存されたデザイン済みのHTMLに以下のようなプレイスホルダーになっているタグが含まれていて、ページがレンダリングされる際にコンテストへのレシピ投稿数と置換されます。

{{ contest_recipe_counts.537 }}

537はコンテストページのID *2

どうしてLiquidが便利なのか?

Liquidの他によく知られたテンプレートエンジンの例として、ERB, Haml, Slimが挙げられますが、これらはあらかじめ用意されたテンプレートファイルからHTMLを生成するのに向いています。これに対し、Liquidはデータベースに保存されたデザイナが用意してくれたHTML snippetの中にデータリソースを表示したいといった用途に向いています。

こんな業務を持っている人に便利
  • 独自のコンテンツ管理システムがある
  • CMSで入稿するコンテンツでデータベース内の情報を動的に取り扱いたいことがある
  • 一般的なCMSで入稿できるデータはテキストかHTMLに制限されているのが普通ですが、そこにデータソースを表示したいなどの理由で既に自前でカスタムタグを作って解析させている場合 *3
タイアップページの例の場合

クックパッド内のほとんどのページは共通のUIの上にコンテンツを配置する方法でデザインされています。 しかし、タイアップページはクライアントの商品イメージに合わせるためにそのような共通UIを使わず、 基本レイアウトを除くほぼすべてのUIをキャンペーンごとにデザインしなおします。

当然ながらそのデザインはクライアント側にチェックしてもらう必要がありますから、 デザインを決定するまで何度も何度も本番へデザイン変更を反映しなければいけません。

しかし、細かくデザインを修正するたびにデプロイするということになると、 デザイン修正のたびにエンジニアの作業が必要となり運用が面倒です。 そこでタイアップページは、入稿してもらったHTMLスニペットをデータベースに保存しておき、 「土台」となるHTMLページの枠に流し込むだけという構造にしています。 このような構造にすることで、デザイナがエンジニアを介さずHTMLを直接入稿できるようになるわけです。

そのように、静的HTMLが保存されている場合でも上記のタイアップページの様に”コンテストへの投稿数を動的に表示してほしい”といったような要件があるものです。 そういった事例を解決するのがLiquidです。 上述したようなLiquid tagをデザインに埋め込むことでデザインからデータソースにアクセスして表示することができます。

コード上のフロー

ここでコード上のフローを確認しましょう。

ControllerがModelのデータソースをロードしViewに渡す

Viewのレンダリングを開始する

Viewがデータソースをテンプレート上に展開する

Liquidでは展開されたデータソースのコンテンツ中のLiquid Tagをテンプレートエンジンがさらに展開するイメージです。

その他の用途例

Drop

Dropはあるモデルから、引数をとってsetterの役割をするmethodを排除してHashのような振る舞いの構造体を作ります。あるモデルから得られるデータリソースをデザイナがセキュアに表示できるように開放したいときに便利です。

例えば以下の例ではTieupRecipeというモデルをTieupRecipeDropに渡して、Liquidのテンプレートエンジンに{{ tieup_recipe.title }}を含んだ文字列をパースさせると、TieupRecipe#titleが取得できるというものです。

tieup_recipe = TieupRecipe.find(id)
tieup_recipe_drop = Pr::Liquid::TieupRecipeDrop.new(tieup_recipe)

template = Liquid::Template.parse("タイトル:{{ tieup_recipe.title }}")
template.render('tieup_recipe' => tieup_recipe_drop)
=> "タイトル:簡単!夏野菜の三色ロール"

TieupRecipeDropはあらかじめ自前で定義しておく必要があります。

class TieupRecipeDrop < Liquid::Drop
  def initialize(tieup_recipe)
    @tieup_recipe = tieup_recipe
  end

  def title
    @tieup_recipe.recipe.title
  end
end
Liquid Block

Liquid BlockはHTML tagを他のタグで囲みたい場合に使います。下記の例ではmobileというlabelの付いたTieupMailというモデルのレコードをデータベースから探して、そのメールを送るフォームを開くボタンを生成します。

  • tieup_mails tableのレコード

f:id:szkmp:20150313172414p:plain

  • 入稿されたLiquid tag
{% tieup_mail_colorbox mobile %}
  <img src="http://img5.cookpad.com/tieup/672/week_bt_sp.gif" width="74" height="74">
{% endtieup_mail_colorbox %}
  • 生成されたHTML
<a class="colorbox_link app_open_browser" data-iframe="true" data-dialog_width="700" data-dialog_height="400" href="http://cookpad.com/ct/88594" style="background: tranparent;">
  <img src="http://img5.cookpad.com/tieup/672/week_bt_sp.gif" width="74" height="74">
</a>
  • その見た目

f:id:szkmp:20150313172433p:plain

  • そのボタンをクリックすると表示されるメール送信フォーム

f:id:szkmp:20150313172448p:plain

※. colorbox_linkというCSS classがあるanchor tagをクリックするとJavascript Libraryがこのようなダイアログを表示するようになっています。

送信メールのレコードの中身が反映されています。送信メールの情報は管理画面で入稿されます。デザイナはメールフォームのlabel名さえ伝えられて知っていればメールフォームへのリンクを自由なデザインでコーディングでき便利です。

まとめ

以下のような場合にLiquidを採用すると便利です。

  • 頻繁にデータソースを含むページを修正する必要がある

  • 誰にでもデータソースを表示できるようになって欲しいが、誤ってデータソースが変更されたり、誤って公開されてはいけないデータソースが公開されるのを防ぎたい

用途例で示したように広告事業部のタイアップ案件では、Liquidを採用したことによってデザインフリーに(デザインに依存しないように)機能を切り出して積み立てることができるようになりました。デザインを絡めた業務フローがあるようなあなたの現場でもLiquidが思わぬライフセイビングになるかもしれませんよ。

*1:クライアントの製品の訴求を目的とした、タイアップ企画商品のページのこと

*2:詳しいタグの使い方はこちらを参考にしてください

*3:Liquidはカスタムタグの解釈と作成の機能を備えています

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/