老闆叫我在 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
- Browser Developer Console
- #development=amp
- https://validator.ampproject.org
- Browser Extension
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,092