老闆叫我在 Rails App 裡實作 AMP,

我該離職嗎?

My boss wants me to setup AMP on the website.

Should I quit my job?

蔡孟穎 (Meng-Ying Tsai)

  • 又名文月、八盤
  • 現職為 web developer
  • ❤: 喝淺焙咖啡、唱日卡、嚐甜食

一年365天歡迎餵食,請多指教 ヽ(●´∀`●)ノ

What is AMP?

Not this one 👉

What is AMP?

  • open-source HTML framework
  • created by Google
  • Accelerated Mobile Pages
    • the AMP project
  • fast, good user experience

What is AMP?

latency

3G

Mobile device

Poor network

performance

User dissatisfaction

What is AMP?

Why people build AMP pages

website traffic

$$$$

❗ AMP itself is not a ranking factor, but page speed is.

page speed

ranking

What is AMP?

How? 🤔

What is AMP?

1. Load JS asynchronously

to prevent render blocking

parsing HTML

fetch script

execute

fetch script

execute

fetch script

e...

parsing

wait........

parsing HTML

fetch script A

execute

fetch script B

execute

fetch script C

execute

resume parsing

wait........

What is AMP?

2. Disallow external stylesheet

to prevent render blocking

What is AMP?

3. Get dimensions before assets is full loaded

<amp-img
  alt="A image of ..."
  src="image.jpg"
  width="900"
  height="600"
  layout="responsive"
>
</amp-img>

What is AMP?

3. Get dimensions before assets is full loaded

  • Render layout before the resource is downloaded
  • Prevent style/layout recalculation

Cumulative Layout Shift!

What is AMP?

4. Only run GPU-accelerated animations

transition@keyframes:

  • opacity
  • transform
  • -vendorPrefix-transform

What is AMP?

5. Resources lazy loading

What is AMP?

5. Resources lazy loading

  • Only download resources if they are likely to be seen by the user
  • Prefetches lazy-loaded resources

bandwidth saving

fast load times

What is AMP?

6. AMP Google/Bing/... Cache

What is AMP?

6. AMP Google/Bing/... Cache

What is AMP?

What is AMP?

Trade-off?

 limits

 developer-unfriendly

no external stylesheet

photo source: 貓咪迷因cat memes

limit CSS sizes

limit script sizes

limitation on custom script

dimensions should be provided on the element

no animation without GPU-accelerated

you have to do everything in the AMP way!!

img, video, iframe  have to be replace with AMP components

No Javascript in AMP?

AMP do not support custom javascript

until amp-script is released in 2019

 

There are still lots of limits for having your script running in amp-script

  • serverside rendering

  • Rails (api) + Next.js

👀

How to build a AMP website with Rails?

  • Good choice if your site is based on Rails view

  • Hard to prerender the page

Server Side Rendering

  • automatically optimized with AMP Optimizer

Rails(API) + Next.js

  • Load data with amp-list

  • amp-script + preact (or other frameworks)

👀

<amp-img
  alt="A image of ..."
  src="image.jpg"
  width="900"
  height="600"
  layout="responsive"
>
</amp-img>
<amp-img 
fallback="" 
alt="A image of ..." 
class="i-amphtml-layout-responsive i-amphtml-layout-size-defined i-amphtml-element i-amphtml-layout" 
src="image.jpg" 
layout="responsive" 
width="900" 
height="600" 
i-amphtml-layout="responsive">
  <i-amphtml-sizer slot="i-amphtml-svc" style="padding-top: 66.6667%;"></i-amphtml-sizer>
  <img decoding="async" alt="A image of ..." src="image.jpg" class="i-amphtml-fill-content i-amphtml-replaced-content">
</amp-img>

Prerender

必須改成 amp-img

<amp-img layout='fixed' 
         width='800' 
         height='600' 
         src="https://source.unsplash.com/kAiyiesI_Kk/800x600" 
         alt="a lovely fox" 
/>

跟平常的 <img>比起來多了 layout 的部分,以後還會常常看到它 :P

AMP pages only

How to build a AMP website with Rails?

Normal / AMP

AMP only for news, media etc.

How to build a AMP website with Rails?

.amp

Mime::Type.register_alias "text/html", :amp

config/initializers/mime_types.rb

respond_to do |format|
  format.html
  format.amp
end

app/controllers/xxx_controller.rb

app/views/xxx/index.amp.erb

blahblahblah...

How to build a AMP website with Rails?

mobile

# use gem browser(https://github.com/fnando/browser)
request.variant = [:amp] if browser.device&.mobile?

app/controllers/xxx_controller.rb

app/views/xxx/index.html+amp.erb

blahblahblah...

desktop

  • Layout
  • Stylesheets
  • Fonts
  • Images
  • AMP Validation

How to build a AMP website with Rails?

Layout

<!doctype html>
<html amp lang="<%= I18n.locale %>">
  <head>
    <meta charset="utf-8">
    <script async src="https://cdn.ampproject.org/v0.js"></script>
    <title><%= yield(:title) %></title>
    <meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
    <script type="application/ld+json">
      <%= yield :structured_data %>
    </script>
    <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}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
    <style amp-custom>
      <%= File.read File.join(Webpacker.instance.config.public_path, Webpacker.instance.manifest.lookup('amp_desktop.css')) %>
    </style>
    <%= content_for(:amp_script) %>
  </head>
  <body>
    <%= yield %>
  </body>
</html>

internal CSS stylesheet

<style amp-custom>
<%= File.read File.join(Webpacker.instance.config.public_path, 
              Webpacker.instance.manifest.lookup('amp.css')) %>
</style>
// config/webpack/development.js
environment.config.merge({
  devServer: {
    writeToDisk: true
  }
})

Webpacker

CSS

Without adding this to the config,

Webpacker::DevServerRunner would serve stylesheets from memory

Canonical

JSDC 2019 - 先別管大亂鬥了, 你聽過 AMP 嗎?

Canonical

On your AMP page

<link rel="canonical" href="https://www.example.com/url/to/full/document.html">
<link rel="amphtml" href="https://www.example.com/url/to/amp/document.html">

On your non-AMP page

  • Append it with content_for to the layout
  • Append it with meta-tags or other gems

Fonts

Whitelisted Font Providers

  • Fonts.com 
  • Google Fonts 
  • Font Awesome 
  • Typekit 

Custom Fonts

include with @font-face

Images

background-image: url("../image.jpg");
<amp-img
  alt="A image of ..."
  src="image.jpg"
  width="900"
  height="600"
  layout="responsive"
>
</amp-img>

amp-img

stylesheet

Don't rely on this too much

Layout

⚠️ intrinsic is not available on IE11

⚠️ No !important in your CSS

fixed

fixed-height

flex-item

responsive

fill

intrinsic

  • Hard-code in Rails view 😈
  • Configure an amp-img helper

Images

amp-img helper

# app/helpers/application_helper.rb
# remember to require 'fastimage' on the top 😉

def amp_img(source, options = {})
  width, height = FastImage.size(source)
  options[:width] ||= width
  options[:height] ||= height
  options[:src] = source

  case options[:layout]
  when 'fill', 'flex-item', 'nodisplay'
    options.delete(:width, :height)
  when 'fixed-height'
    options[:width] = 'auto'
  when 'fixed', 'intrinsic', 'responsive'
  else
    options[:layout] = 'nodisplay'
  end

  content_tag(:'amp-img', options) do
    yield if block_given?
  end
end

amp-img helper

<%= amp_img asset_pack_url('media/images/blahblahblah.jpg'),
            layout: 'fixed' %>
<%= amp_img asset_pack_url('media/images/blahblahblah.jpg'),
            layout: 'fixed',
            width: '128',
            height: '128') %>

Get dimensions from FastImage

Specify size of an image 

Rich contents?

configure an parser!

Rich Content

# frozen_string_literal: true

require 'scrubbers/image'
require 'scrubbers/youtube'

class RichContentParser
  def initialize(content)
    @fragment = Loofah.fragment(content)
  end

  def parse
    self.class.scrubbers.each { |scrubber| @fragment.scrub!(scrubber) }

    @fragment.to_s
  end

  def self.scrubbers
    [Scrubbers::Image.new,
     Scrubbers::Youtube.new]
  end
end

Parser

Rich Content

# frozen_string_literal: true

module Scrubbers
  class Youtube < Loofah::Scrubber
    YOUTUBE_LINK_PATTERN = %r{www\.youtube\.com/embed/(.+)}i.freeze

    def scrub(node)
      return if node.name != 'iframe' || node['src'] !~ YOUTUBE_LINK_PATTERN

      youtube_uri = URI(node['src'])
      node.name = 'amp-youtube'
      node['layout'] ||= 'responsive'
      node['width'] ||= 480
      node['height'] ||= 270

      scrub_styles(node)
      scrub_attributes(node)
      append_data_params(node, youtube_uri)
    end
    # ...
  end
end

Scrubber

Rich Content

# app/models/your_model.rb

before_validation :ampify

def ampify
  self.amp_content = RichContentParser.new(content).parse
end

Ampify Content

AMP Validation

components

amp-sidebar

amp-social-share

amp-ad

amp-facebook

components

<amp-list>

<div>

<a>

<p>

components

  • amp-bind
  • amp-list
  • amp-script
  • amp-iframe
  • ...and more

amp-bind

amp-bind

require script

<script
  async
  custom-element="amp-bind"
  src="https://cdn.ampproject.org/v0/amp-bind-0.1.js"
></script>

<span [text]="myText">

binding

<button on="tap:AMP.setState({ myText: '...' })">

...</button>

event

action

Same as data-amp-bind-text

Events

  • tap

Actions

  • hide
  • show
  • toggleVisibility
  • toggleClass(class=STRING, force=BOOLEAN)
  • scrollTo(duration=INTEGER, position=STRING)

amp-bind with declared state

  • Contents won't change until AMP.setState({})

  • You can only bind attributes of an HTML element

[text]

[src]

[disabled]

[class]

[width]

[height]

[hidden]

[aria-label]

amp-bind with Remote Endpoint

Allowed-listed functions

  • Array: concat, filter, includes, indexOf, join...
  • Number: toExponential, toFixed, toString...
  • String: charAt, charCodeAt, concat, indexOf, replace...
  • Math: abs, cell, floor, max, min, pow...
  • Object: keys, values
  • Global: encodeURI, encodeURIComponent​
{
  "TYP_0100041": true,
  "TYP_0100042": false,
  "TYP_0100043": false,
  "TYP_0100044": true,
  "TYP_0100045": true,
}
[{
    "ID": "TYP_0100041",
    "OPEN": true
  },
  {
    "ID": "TYP_0100042",
    "OPEN": false
  },
  {
    "ID": "TYP_0100043",
    "OPEN": false
  },
  {
    "ID": "TYP_0100044",
    "OPEN": true
  }
]

Debug in console

1. append #development=amp

2. AMP.printState() to see states

Be careful with the restrictions

amp-bind Expression complexity limit 

amp-bind element's JSON data size limit 

Maybe not be quite capable of

producing complicated websites

😣 hard to read, hard to maintain, and hard to debug

😣 limits to total script size

😣 limits to the size of amp-state element's JSON data

amp-list

Display lastest infos on your AMP cache ✨

amp-list + amp-bind

amp-list

require script

<script
  async
  custom-element="amp-list"
  src="https://cdn.ampproject.org/v0/amp-list-0.1.js"
></script>

Source of an amp-list

  • remote endpoint
  • amp-state

🌟 src can be changed with [src]

require script

<script
  async
  custom-template="amp-mustache"
  src="https://cdn.ampproject.org/v0/amp-mustache-0.2.js"
></script>

amp-mustache

get value

{{myVariable}}

amp-mustache

conditionals

{{#isOpen}}開放中{{/isOpen}}

negative

conditionals

{{^isOpen}}未開放{{/isOpen}}

amp-mustache

loop

{{#cart_items}}

    <li>{{name}}: ${{price}}</li>{{/cart_items}}

amp-mustache

CORS in AMP

mySite.blah

mySite-blah.cdn.ampproject.org

???

CORS in AMP

CORS in AMP

module Api
  class BaseController < ApplicationController
    before_action :cors_verification
    
    ALLOWED_ORIGINS = [
      'https://mySite.blah',
      'https://mySite-blah.cdn.ampproject.org',
      'https://mySite.blah.amp.cloudflare.com'
    ]

    def cors_verification
      if request.headers['origin'].in?(ALLOWED_ORIGINS)
        origin = request.headers['origin']
      elsif request.headers['origin'].nil? && request.headers['AMP-same-origin']&.casecmp?('true')
        origin = request.base_url
      else
        render(json: { message: 'Unauthorized Request' }, status: :forbidden) && return
      end

      response.set_header('Access-Control-Allow-Credentials', 'true')
      response.set_header('Access-Control-Allow-Origin', origin)
    end
  end
end

amp-script

I AM NOT MAD

I JUST NEED TO RUN MY CUSTOM SCRIPT

amp-script

amp-script elements that have a script or cross-origin src attribute require a script hash

<head>
  <meta
    name="amp-script-src"
    content="sha384-....."
  />
</head>
def amp_script_hash(module_name)
  module_content = File.read(File.join('public', Webpacker.instance.manifest.lookup(module_name)))
  Base64.urlsafe_encode64(Digest::SHA2.new(384).hexdigest(module_content), padding: false)
end
<meta name='amp-script-src' content=<%=amp_script_hash('my_custom_script.js')%> />

amp-script

Get your script path from webpacker 

<amp-script
 layout='fixed'
 width='500'
 height='500'
 src=<%=Webpacker.instance.manifest.lookup('my_custom_script.js').gsub(/^\//, root_url)%>
></amp-script>

amp-script

👉 allowed APIs 👈

Element.querySelector 🔺 (Partial support)

document.querySelector 

Element.innerHTML

Event.preventDefault 

HTMLElement.dataset  

HTMLElement.innerText  

amp-script

  • Limited to 10 KB per <amp-script>
  • Limited to a total of 150 KB
  • No nested amp-script
  • Size limit checks real size instead of transferred size
  • Only amp-img and amp-layout are allowed to create

requires user gestures to change page content.

amp-script

Get amp-state

AMP.getState("yourStateName").then( /* ... */ )

Set amp-state

 AMP.setState({myStateName: myObject});

amp-iframe

Things is not working in amp-script?

Try amp-iframe!

amp-iframe

amp-iframe

amp-facebook

amp-instagram

amp-twitter

amp-vimeo

amp-youtube

...

amp-iframe

  • must be positioned outside the first 75% of the viewport or 600px from the top
    • add placeholder otherwise
  • No same origin when allow-same-origin

AMP cache

prerendering

&

optimizations

valid AMP page

AMP Cache

&

AMP search result

Google AMP Viewer URL

Original AMP Source

Signed Exchange

Signed Exchange

Signed Exchange

  • supported TLS certificate
    • Digicert
  • Cloudflare AMP Real URL

⚠️ Only supported on Chrome

Conclusion

AMP 很難搞,像極了愛情(Meng-Ying Tsai,民109)。

Nice page speed

Nice Core Web Vitals scores

Hard to customize

Hard to maintain

Interactive & gorgeous

or

fast & simple

🤔

Thanks for listening (ゝ∀・)⌒☆

祝大家都能離職成功保住飯碗!

老闆叫我在 Rails App 裡實作 AMP,我該離職嗎?

By Meng-Ying Tsai

老闆叫我在 Rails App 裡實作 AMP,我該離職嗎?

  • 1,118