Railsの既存コードをほぼ変更せずにAMP対応する8ステップ

#はじめに:通信量が少ない&&爆速なAMP

AMP(Accelerated Mobile Pages)はサイト表示を高速化する技術です。特に帯域制限されたスマホでも素早く快適に画面が表示されるので、(私もそうなのですが)スマホの通信料金を節約したい！帯域細い！というユーザーには待望の技術ではないでしょうか。


しかしその高速さの一方で、AMP開発チームがその技術を
> User Experience > Developer Experience > Ease of Implementation 

と言い切っている通り、「UX(スピード)最優先、作りやすさは二の次！」で扱いにくい側面もあります。
参考: https://www.ampproject.org/learn/design-principles/

そんなAMPを、なるべく既存のコードを修正せず(特に、画面テンプレートにはほとんど手を入れないで）AMP対応したいと思います。(開発環境: Rails5.0.1)

![スクリーンショット 2016-12-04 22.10.58.png](https://qiita-image-store.s3.amazonaws.com/0/8893/cac95865-cc4d-dcc5-543e-558b2546bc64.png)

Googleでの検索結果の例。「光速の体験」ですって。

# AMPの制約
AMP対応ページにおける制約のうち、大きなものは以下の通りです。



1. CSSは外部ファイル使用不可、50KBまで
	* 高速なページレンダリングのため外部CSSファイルは使えず```<style>```タグにてhtmlページ内に実装する必要があり、50KB以上の大きなCSSも許されされません。

2. 画像表示に```<img>```タグが使えない
	* AMPではロードに時間がかかるものは非同期処理されます。そのために<img>タグは使えず、専用の```<amp-img>```タグを使います。またレンダリング速度のためにwidth属性とheight属性を設定しておく必要があります。
 
3. Javascriptが必須のページは対応できない
    * AMPではJavascriptの動作に制限があり、サイト開発者が用意したScriptは動作させることができません。
4. Cookieが実質使用不可
    * Google検索結果からAMPページに遷移すると、
```
https://www.google.co.jp/amp/s/自サイトのドメイン/パス
```
というGoogleドメイン下のキャッシュページが表示されます。このページは自サイトのドメインではないため、自サイトのCookie情報が使われません。つまり会員向け機能などCookieを活用した機能は提供できません。


特に3と4の制約からインタラクティブなページのAMP対応は難しく、今回は「見るだけで完結するページ」をAMP対応させることとします。




#対応方針
各Railsのアクションに.amp拡張子をつけることで対応する方針とします。例えば
/domain/controller/action
というページがあった場合には
/domain/controller/action.amp
としてamp対応ページを返却することとします。

# Step1. ```.amp``` を認識させる
まず、```.amp```をMimetypeとしてRailsに認識させます。

``` config/initializers/mime_types.rb
Mime::Type.register_alias "text/html", :amp
```

# Step2. ```.amp```で反応するようにする
AMPに対応させるactionのrespond_toメソッドに修正を加えます。

``` app/controllers/blogs_controller.rb
def some_action
  #   :
  #　必要な処理
  #   :
  
  respond_to do |format|
    format.html
    @amp_ready = true
    format.amp do
      lookup_context.formats = [:amp, :html] # .htmlのテンプレートも検索する
      render
    end
  end
end 
```
ここでのポイントは2つです。

#### ```rel="amphtml"```のための```@amp_ready```変数

AMPに対応したページのオリジナルhtmlページには、「このページはAMPに対応しているよ！」と知らしめるために```@amp_ready = true``` をセットします。詳しくは後述します。

#### .htmlテンプレートを使うための```lookup_context```
RailsのActionViewは、拡張子に応じてテンプレートを探します。ですので.amp拡張子の場合は「.amp」がついているテンプレートしか探しません。このままでは各アクションごとに'action.amp.erb'を用意しなくてはならずツライので、.ampテンプレートがなければ.htmlテンプレートを探すように修正します。

```rb
lookup_context.formats = [:amp, :html]
```

これにより.htmlテンプレートの流用が可能になります(やった！)。

# Step3. 従来のhtmlページに```link rel="amphtml"```を付与する
さきほど```@amp_ready```変数を設定しましたが、この変数を利用して元のページに

```<link rel="amphtml" href="https://domain/controller/action.amp">``` というlinkタグを付与します。まずamp対応ページかどうかを判定する```amp_ready？```を実装します。

```app/helpers/application_helper.rb
module ApplicationHelper
  def amp_ready?
    defined?(@amp_ready) && @amp_ready == true
  end
end
```


続いてapplication.htmlには

```app/views/layouts/application.html.slim
(略)

html
  head
    (略)
    - if amp_ready?
      - amp_uri = URI.parse(request.url)
      - amp_uri.path = "#{amp_uri.path}.amp"
      - amp_uri.query = h(amp_uri.query) if amp_uri.query.present?
      link rel="amphtml" href = amp_uri.to_s.html_safe
```
のように、amp_readyの場合には```<link rel="amphtml">```を出力します。
細かい工夫ですが、URIオブジェクトを使うことで拡張子のあとにパラメータが付いていても正しく変換できます。
```blogs/action?foo=bar```　=> ```blogs/action.amp?foo=bar```

# Step4. AMP用レイアウトを用意する
AMPでは、```<amp-*>```タグ用のスクリプトなどをロードするボイラープレート(模範表現)が提供されています。これを読み込めるように```application.html```を修正します。


```app/views/layouts/application.html.slim
- if request.format ==  :amp
  = render 'layouts/application' #application.amp.slimを読み込む
- else
  doctype html
  html
    (以下従来のHTMLテンプレート)
```

読み込まれる```application.amp```は、ampproject.orgで提供されているボイラープレートをベースに改変します。
https://www.ampproject.org/docs/get_started/create/basic_markup



```app/views/layouts/application.amp.slim
| <!doctype html>
html amp=""
  head
    meta[charset="utf-8"]
    meta[name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1"]
    link rel='canonical' href = request.url.gsub('.amp', '')
    style[amp-boilerplate]
      | body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}
    noscript
      style[amp-boilerplate]
        | body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}
    style[amp-custom]
      - controller = params[:controller]
      - if Rails.application.assets && Rails.application.assets["amp/amp_#{controller}"]
        // development
        = Rails.application.assets["amp/amp_#{controller}"].to_s.html_safe
      - else
        // production
        = File.read "#{Rails.root}/public#{stylesheet_path("amp/amp_#{controller}", host: nil)}"
    script[async src="https://cdn.ampproject.org/v0.js"]
    = render 'header'
    = yield
    = render "footer"
```

# Step5. CSSの対応
前述のapplication.ampで、styleタグの部分にRails.application.assetsの処理が入っていたのに気づきましたか？
「AMPの制約」に記載したとおり、CSSは外部ファイルを利用できず、また容量制限があります。そこで以下のような方針で対応します。

* asset pipelineで作られるCSSをstyleタグで読み込む
* コントローラごとにAMP用CSSを作成することで容量を削減する

先のコードをもう一度確認します。

```haml
style[amp-custom]
　　- controller = params[:controller]
　　- if Rails.application.assets && Rails.application.assets["amp/amp_#{controller}"]
　　  // development
 　　 = Rails.application.assets["amp/amp_#{controller}"].to_s.html_safe
　　- else
 　　 // production
 　　 = File.read "#{Rails.root}/public#{stylesheet_path("amp/amp_#{controller}", host: nil)}"

```
Rails.application.assetsに注目してください。
開発環境ではRails.application.assetsにコンパイルされたCSSが格納されています。残念ながら本番環境ではこの変数はnilになっていますが、代わりにasset precompileしたファイルをstylesheet_pathヘルパによって取得することが可能です。そのファイルをFile.readで読み込みます。

読み込まれるAMP用CSS自体はconfig/applicatio.rbにてasset precompile対象に設定しておきます。AMP対応するファイルが増えるたびapplication.rbをメンテするのは面倒なので、```app/assets/stylesheets/amp```以下のCSSファイルはすべてasset precompile対象にします。

```config/application.rb
 amp_css_paths = Dir.entries("#{config.root}/app/assets/stylesheets/amp").select { |name| name =~ /css$/ }.map { |name| "amp/#{name}" }
 config.assets.precompile += amp_css_paths
```

参考: https://coderwall.com/p/wpyasq/how-to-create-an-amp-page-for-your-dynamic-content-in-rails


読み込むCSSファイルは"amp/amp_#{controller_name}"とし、オリジナルのapplication css の中から必要なものを取捨選択します。(50KBを超えなければよいので、小規模サイトならオリジナルのコンパイル済みCSSをそのまま使えるかもしれません。)

```app/assets/stylesheets/amp/amp_blogs.scss
// application.scssをコピーしてきたもの
// @import "news"
@import "blogs" //これだけ使う
// @import "comments"

```

# Step6. image_tagヘルパーのフック
「AMPの制約」に記載したとおり、すべての```<img>```タグは```<amp-img>```タグに置き換えないといけません。そこでimage_tagをフックし、必要に応じて```<amp-img>```が出力されるようにします。

```config/initializers/image_tag_helper.rb
module ActionView
  module Helpers
    module AssetTagHelper
      def amp_image_tag(source, options = {})
        src = options[:src] = ActionController::Base.helpers.path_to_image(source, skip_pipeline: options.delete(:skip_pipeline))

        unless src.start_with?('cid:', 'data:') || src.blank?
          options[:alt] = options.fetch(:alt) { ActionController::Base.helpers.image_alt(src) }
        end
        options[:layout] ||= 'fixed'
        ActionController::Base.helpers.tag('amp-img', options)
      end

      # amp-imgかimgかをrequest.formatによって使い分ける。
      def image_tag_with_amp(source, options = {})
        if amp?
          amp_image_tag(source, options)
        else
          image_tag_without_amp(source, options)
        end
      end

      def amp?
        Thread.current[:format] == 'amp'
      end
      alias_method_chain :image_tag, :amp
    end
  end
end
```
参考: https://github.com/rails/rails/blob/5-0-1/actionview/lib/action_view/helpers/asset_tag_helper.rb#L210

ポイントは2点です。

* ampの場合はamp_image_tagメソッドをコールし、ampでない場合は従来のimage_tagメソッドをコールする。(image_tag_with_amp)
* このヘルパーはrequestオブジェクトにアクセスできないので、Thread.current(グローバルオブジェクト)を用いてampかどうかの情報を引き渡します(amp?)。その情報は以下のようにapplication_cotrollerにてセットします。

```app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :set_format
    
  def set_format
    Thread.current[:format] = request[:format]
  end
end
```
以上により、ampのときだけ<amp-img>が使われるimage_tagメソッドができました。

この変更を加えた上でimage_tag使用箇所全てにwidht, height属性を設定します。



# Step7. JSON-LDへの対応(optional)
AMPではJSON-LDと呼ばれる構造化データを利用します。構造化データはタイトルや画像、作成日などのページのメタ情報をmachine-readableにするものです。サイトの性質によって異なりますので一概には言えませんが、ブログ、コラム、ニュースの類であればArticleというフォーマットを利用します。

参考: https://developers.google.com/search/docs/data-types/articles#amp-sd
参考: http://schema.org/NewsArticle


# Step8. デバッグ
AMPのバリデータはchromeに標準搭載されています。
URLに```#development=1```をつけてページを開き、
Chrome DevTool Consoleを立ち上げると、バリデーション結果が表示されます。
![image](https://qiita-image-store.s3.amazonaws.com/0/8893/f2e7be37-7917-515b-b7f7-f5f308d3aabb.png)
こちらで表示されたエラーを確認しながら、すべて対応していきます。
参考: https://www.ampproject.org/docs/guides/validate


# 最後に
おつかれさまでした！対応完了までにはちょっと手数が多いですが、画面テンプレートにはほとんど手を加えずに対応できたかと思います。
また、上記対応では触れませんでしたが、

* フォームがあるページには```<amp-form>```
* GoogleAnalyticsを仕込んでいるページには```<amp-analytics>```
* Adsenseなど広告を入れているページには```<amp-ad>```

を使って対応する必要があります。ビジネスユースではこれに加えて効果測定上の工夫も必要となるかと思いますが、個人ユースでは本稿の内容で概ね対応できると思います。

Make our mobile web faster, together!