morishita です。 実は、リリースしてからしばらく経つのですが、いこーよの施設詳細ページのAMP化をリリースしました。 そのことについて書きたいと思います。
AMP化の経緯と目的
いこーよでは以前から記事のページはAMPも提供してきました。 例えば、年齢別で見つかる! 関西の入場無料の遊園地&テーマパーク14選(HTML) に対する AMPページは 年齢別で見つかる! 関西の入場無料の遊園地&テーマパーク14選(AMP) です。 (※ スマートホンで見てください) 記事のAMP化についてはいこーよの記事をAMP化しましたを御覧ください。
最近Googleがしきりに宣伝していることもあり、AMPページを実装するサイトも増えてきました。 Googleの検索結果でも見かけることが増えてきているのではないでしょうか。 そして、記事やグルメ、レシピなど特定カテゴリーのAMPページはカード形式で非常に目立つ表示をされることがあります。
いまの所、GoogleはAMPページが「SEOに寄与することはない」と明言しています。 とはいえ、次のような効果が期待できるのはないかと考え、いこーよのメインコンテンツである施設詳細ページをAMP化することとなりました。
- 検索結果からすばやく表示できるAMPページを提供することでオーガニック流入を増やせるのではないか?
- 流入が増えればサイト内回遊も増やせるのではないか?
- カード形式で表示されることがあれば、上記の効果がブーストされるのではないか?
- 結果、サイトの価値が評価され検索順位も上がってくるのではないか?
エンジニア的には、AMPといえども動的でインタラクティブな要素を含むページも作れるようになってきているので それにチャレンジしてみたいという考えもありました。
施設詳細のAMPページ
さて、実装したAMPページですが、例えば京都水族館(HTML)のAMPページは 京都水族館(AMP)になります(※スマートホンで見てくだい)。 こんな感じです。
左がHTMLページで、右がAMPページです。多少レイアウトが異なりますが、デザインはほぼ同じにできています。 イルカの画像はHTML、AMPともカルーセルになっており、自動で動きますし、AMPでもHMTL同様「行きたい」ボタンが動作します。
だいたい同じページが実装できましたがAMPページはそもそも高速表示を目指した仕様なので、 HTMLの仕様を100%は実装せず、要素を省略して程よく減量しています。
さて、AMPといえばGoogleにキャッシュされてしまいますが次の要素はキャッシュを避けたかったので、 非同期でデータを取得し動的に表示するようにしています。
- 行きたい(ユーザ毎に異なるのでキャッシュは不適当)
- ユーザ評価(数字が変わるのでキャッシュされたくない)
- クーポン(配布終了後、キャッシュで表示されると困る)
- イベント(イベント終了後、キャッシュで表示されと残念)
- 自社配信広告(刻々と表示優先度が変わるのでキャッシュはダメ)
- 天気予報(天気が変わった後、古い情報を表示したくない)
それぞれ、amp-bindや amp-listを使って実現しています。
この中で最も複雑な「行きたい」ボタンの実装についてもう少し詳しく説明しましょう。
「行きたい」ボタンの実装
他の非同期要素はamp-listと amp-mustacheを使えば実装できます。
しかし、「行きたい」ボタンだけはユーザ毎に状態を保持する必要あり、次のAMPコンポーネントを利用して実装しています。
- amp-bind(amp-stateタグ)
- ユーザが「行きたい」をクリックしたか否かの状態管理
- amp-list
- 初期状態(過去に「行きたい」したかどうか)の取得
- amp-mustache
- amp-listが取得したデータを表示するためのテンプレート
- amp-form
- クリック時のデータ送信
次のコードは「行きたい」ボタン部分です(いこーよはテンプレートにslimを利用しています)。 ただ、クリックして数字をインクリメントして色が変わるだけのボタンの実装にしては複雑でしょう。 シンプルにするためCSSクラスなどを省略してもこれです。
/ ポイント①
amp-state#favorite src=favorites_api_url(@facility_id) credentials="include"
/ ポイント②
form.(method="post"
action-xhr=favorites_path
target="_top"
on="submit-success:AMP.setState({previousFavorite: favorite, favorite: {favorited: true, count: event.response.count }}); submit-error:AMP.setState({favorite: previousFavoriteWithCount})")
input type="hidden" name="id" value=@facility_id
/ ポイント③
amp-list(width="auto" height="62" items="."
single-item=true
noloading=true
src=favorites_api_url(@facility_id)
credentials="include")
/ ポイント④
template type="amp-mustache"
div
<button [class]="favorite.favorited ? 'favorite-done' : 'favorite-none'" [disabled]="favorite.favorited" disabled="">
div
| 行きたい!
br
<span class="u-text--s" [text]="favorite.count ? favorite.count : '--'">
| 8
</span>
</button>
div placeholder=true
button disabled=true
div
| --
ポイントを説明します。
ポイント① amp-state
タグ
このタグがユーザの「行きたい」をしているかどうかの状態を保持します。
タグのID属性favorite
で他のタグから参照できるようになります。
後述しますが、credentials="include"
が重要です。
ポイント② form
ユーザのクリックをサーバ側へ送信するために必要です。
AMPの中のformはサブミットしてもデフォルトでは画面遷移が発生せず、非同期にリクエストを送信し結果を受け取ります。
on
属性で成功の場合、失敗の場合、それぞれAMP.setState
でamp-state#favorite
が保持する値を変更します。
それにより、表示が切り替わる仕組みです。
ポイント③ amp-list
タグ
amp-list
はページのロード時に行きたいAPIへリクエストし、ユーザの状態を取得するのに使います。
amp-state
が初期値をAPIから取ってくれればいいのですが、あくまでユーザインタラクションを
受けてしか動作しないので、amp-list
を利用します。
取得した内容をもとに、次のtemplate
タグがクライアントサイドでレンダリングされます。
また、このタグでもcredentials="include"
は重要です。
ポイント④ template
タグ
mustache のテンプレートです。amp-list
が取得した情報を表示するために使います。
注目ポイントはdisabled属性について2種類の記述していることです。
[disabled]
あれば disabled=""
要らない気がするんですが、
disabled=""
は mustache が使います。
非同期レスポンスを取得してすでに行きたい済みの場合、mustache により disabled=true
になります。
行きたいをクリックしたときには AMP.setState
によりamp-state
が保持している値が変わります。
その結果、amp-bind
が”favorite.favorited”を参照している[disabled]
の値を書き換えるという仕掛けです。
テンプレート内の [text]
がfavorite.count
を参照しているので、「行きたい」しているユーザの数も
ユーザ操作によって変わります。
注意すべきところ
基本的にCORSだということ
通常、AMPページの流入口はGoogleの検索結果です。しかも、Googleによるキャッシュが
オリジンドメインではなくcdn.ampproject.org
のサブドメインとして表示されます。
一方、ページの表示時に発生する非同期リクエストはオリジンドメイン(iko-yo.net)にやってきます。
つまり、クロスドメインの非同期通信が発生します。
また、ユーザごとに異なる情報を返す必要があるので、Cookieによる識別も必要となります。
AMPでCookieをやり取りするためにはまず、通信するタグにcredentials="include"
をつけます。
CORSでCookieをやり取りするには、Access-Control-Allow-Origin
をテキトーに*
にして逃げず、
きちんとリクエストを送信してきたドメインに対して許可する必要があります。
それだけではなく、AMPでは Access-Control-Expose-Headers
やAmp-Access-Control-Allow-Source-Origin
も
返す必要もあります。
(詳しくはCORS Requests in AMP のざっくり訳 - Qiitaでも読んでください)
Slimとmustacheの相性が悪い
AMP内で動的なコンテンツの表示を行うには mastache テンプレートを使う必要があります。 mastache の特徴はカーリーブレイス”{{}}“で変数要素を囲うことですが、これがSlimと相性が悪い。 Slimでは部分的にHTMLタグを書いても構わないのでそれでしのぎました。 私の勘違いなのですが、Slim内のHTMLタグは閉じタグ書かなくても勝手に閉じてくれると思い込んでいて、 AMPテストでエラーとなる不具合がありました。 ページ自体は崩れなく表示され機能するので気づくのに時間がかかりました。 (AMPテストはインターネットに公開しないと使えない上に、これをパスしないとGoogleにインデックスされない)
要素の高さを指定するのはやはり辛い
AMPではレンダリングする要素の高さがレンダリング前に決まっていることが前提なので、
基本的には要素の高さを決める必要があります。しかし、クーポンやイベントは存在するときのみ表示したいし
何もなければ、要素ごと非表示にしたいと思います。
できないのか? というと「制限付き」で可能です。
例えば、amp-list
ではlayout="flex-item"
が使えます。これは取得したリスト要素の数に応じて高さが動的に変わるというレイアウトです。
なんだ、できるのね。と思ったのですが、これは要素がビューインすると動作しません。
したがってファーストビューに入る位置では使えないのです。
ファーストビューに入ってなくても、スクロールして表示が変わる前にビューインすると動きません。
表示される場所までスクロールしてからリロードした場合もダメです。
今回のAMPページでは、ファーストビューに入らないようにレイアウトを少し調整しました。
また、AMPキャッシュからの流入が主ならばページトップから表示されるだろうし、リロードされ場合は
仕方ないと割り切りました。
で、どうだったのか?
まあ、試行錯誤もしつつ割と時間を掛けてAMP化を果たしました。 そして、リリースから二ヶ月ほど経過しました。
期待した効果は得られたのか? 残念ながら、やはりカードで表示されるのは記事やレシピ、グルメだけで いこーよの施設詳細はダメなようです。 したがってオーガニック流入の押上要因にはなれませんでしたし、 施設詳細の検索結果の順位も上がりませんでした。 放置して、他の開発の影響を受けるとAMPページも修正しないといけなくなるので メンテナンスコストをがかかる可能性があるのならと、近々消える見込みです。 興味のある方は動いているうちにどんなものか見てみてください。
まあ、期待した成果は得られませんでしたが、 今回はAMPで動的なページを作る知見が得られたということで良しとしたいと思います。
こんな試行錯誤や期待通りにいかないことにもめげずに 一緒にいこーよを成長させるためのトライをしてくれるエンジニアを募集しております。 興味のある方はぜひお越しください!