├── views ├── robots.ejs ├── error.ejs ├── content.ejs ├── site-navigation.ejs ├── footer.ejs ├── manifest.ejs └── header.ejs ├── public ├── images │ ├── logo.jpg │ ├── desert.jpg │ ├── fields.jpg │ ├── ocean.jpg │ ├── icon │ │ ├── 1024.png │ │ ├── 128.png │ │ ├── 144.png │ │ ├── 152.png │ │ ├── 192.png │ │ ├── 256.png │ │ └── 512.png │ └── mountains.jpg ├── stylesheets │ └── style.css ├── additional-sw-scripts.js └── javascripts │ ├── core.js │ └── amp-core.js ├── config.json ├── package.json ├── server.js ├── sw-precache-config-server.js ├── sw-precache-config-client.js ├── sw-precache-config-hybrid.js ├── CONTRIBUTING.TXT ├── content ├── ocean.xml ├── desert.xml ├── index.xml └── mountains.xml ├── bin └── www ├── app.js ├── README.md ├── routes └── all.js └── LICENSE /views/robots.ejs: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /views/error.ejs: -------------------------------------------------------------------------------- 1 |

2 | Error 3 |

4 |

5 | <% message %> 6 | <% error %> 7 |

8 | -------------------------------------------------------------------------------- /public/images/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/indexable-pwa-samples/HEAD/public/images/logo.jpg -------------------------------------------------------------------------------- /public/images/desert.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/indexable-pwa-samples/HEAD/public/images/desert.jpg -------------------------------------------------------------------------------- /public/images/fields.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/indexable-pwa-samples/HEAD/public/images/fields.jpg -------------------------------------------------------------------------------- /public/images/ocean.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/indexable-pwa-samples/HEAD/public/images/ocean.jpg -------------------------------------------------------------------------------- /public/images/icon/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/indexable-pwa-samples/HEAD/public/images/icon/1024.png -------------------------------------------------------------------------------- /public/images/icon/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/indexable-pwa-samples/HEAD/public/images/icon/128.png -------------------------------------------------------------------------------- /public/images/icon/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/indexable-pwa-samples/HEAD/public/images/icon/144.png -------------------------------------------------------------------------------- /public/images/icon/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/indexable-pwa-samples/HEAD/public/images/icon/152.png -------------------------------------------------------------------------------- /public/images/icon/192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/indexable-pwa-samples/HEAD/public/images/icon/192.png -------------------------------------------------------------------------------- /public/images/icon/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/indexable-pwa-samples/HEAD/public/images/icon/256.png -------------------------------------------------------------------------------- /public/images/icon/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/indexable-pwa-samples/HEAD/public/images/icon/512.png -------------------------------------------------------------------------------- /public/images/mountains.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/indexable-pwa-samples/HEAD/public/images/mountains.jpg -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "ampMode": "disabled", 3 | "renderMode": "client", 4 | "updateMode": "json", 5 | "googleSiteVerificationToken": "" 6 | } 7 | -------------------------------------------------------------------------------- /views/content.ejs: -------------------------------------------------------------------------------- 1 | <%- include('header') -%> 2 | 3 | <%- include('site-navigation') -%> 4 | 5 |

6 | <%- page.title %> 7 |

8 | 9 |
10 | <%- page.content %> 11 |
12 | 13 | 18 | 19 | <%- include('footer') -%> 20 | -------------------------------------------------------------------------------- /views/site-navigation.ejs: -------------------------------------------------------------------------------- 1 |
2 |

3 | Indexable PWA 4 |

5 | 6 | <% if (ampRequested) { %> 7 | Is AMP 8 | <% } else { %> 9 | Not AMP 10 | <% } %> 11 |
12 | 13 | 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "seo-pwa", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www" 7 | }, 8 | "dependencies": { 9 | "body-parser": "~1.15.1", 10 | "cookie-parser": "~1.4.3", 11 | "debug": "~2.2.0", 12 | "ejs": "^2.5.2", 13 | "express": "~4.13.4", 14 | "morgan": "~1.7.0", 15 | "serve-favicon": "~2.3.0", 16 | "sw-precache": "^4.2.1", 17 | "sw-toolbox": "^3.2.1", 18 | "whatwg-fetch": "^1.0.0", 19 | "xml2js": "^0.4.17" 20 | }, 21 | "devDependencies": {} 22 | } 23 | -------------------------------------------------------------------------------- /views/footer.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | Server Configuration 5 |
6 | AMP: <%- serverConfig.ampMode %> 7 |
8 | Render Mode: <%- serverConfig.renderMode %> 9 |
10 | Update Method: <%- serverConfig.updateMode %> 11 |
12 | 13 |
14 | All Samples: 15 |
16 | Server Rendered 17 |
18 | Client Rendered 19 |
20 | Hybrid Rendered 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var app = require('./app'); 18 | 19 | app.listen(3000, function () { 20 | console.log('PWA Sample App listening on port 3000!'); 21 | }); 22 | -------------------------------------------------------------------------------- /sw-precache-config-server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | module.exports = { 18 | "staticFileGlobs": [ 19 | "public/javascripts/*", 20 | "public/stylesheets/*", 21 | "public/images/*", 22 | "bower_components/*" 23 | ], 24 | 25 | "stripPrefix": "public", 26 | 27 | "importScripts": ["additional-sw-scripts.js"] 28 | }; 29 | -------------------------------------------------------------------------------- /views/manifest.ejs: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Indexable PWA", 3 | "short_name": "Indexable", 4 | "icons": [{ 5 | "src": "images/icon/128.png", 6 | "type": "image/png", 7 | "sizes": "128x128" 8 | }, { 9 | "src": "images/icon/152.png", 10 | "type": "image/png", 11 | "sizes": "152x152" 12 | }, { 13 | "src": "images/icon/144.png", 14 | "type": "image/png", 15 | "sizes": "144x144" 16 | }, { 17 | "src": "images/icon/192.png", 18 | "type": "image/png", 19 | "sizes": "192x192" 20 | }, 21 | { 22 | "src": "images/icon/256.png", 23 | "type": "image/png", 24 | "sizes": "256x256" 25 | }, 26 | { 27 | "src": "images/icon/512.png", 28 | "type": "image/png", 29 | "sizes": "512x512" 30 | }], 31 | "start_url": "/", 32 | "display": "standalone", 33 | "orientation": "portrait", 34 | "background_color": "#f5f5f5", 35 | "theme_color": "#80dbff" 36 | } 37 | -------------------------------------------------------------------------------- /sw-precache-config-client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | module.exports = { 18 | "staticFileGlobs": [ 19 | "public/javascripts/*", 20 | "public/stylesheets/*", 21 | "public/images/*", 22 | "bower_components/*" 23 | ], 24 | 25 | "stripPrefix": "public", 26 | 27 | "runtimeCaching": [ 28 | { 29 | "urlPattern": "/(.*)json", 30 | "handler": "fastest" 31 | } 32 | ], 33 | 34 | "navigateFallback": "/app-shell", 35 | 36 | "dynamicUrlToDependencies": { 37 | "/app-shell": [ 38 | "views/content.ejs", 39 | "views/footer.ejs", 40 | "views/header.ejs", 41 | "views/site-navigation.ejs" 42 | ] 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /sw-precache-config-hybrid.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | module.exports = { 18 | "staticFileGlobs": [ 19 | "public/javascripts/*", 20 | "public/stylesheets/*", 21 | "public/images/*", 22 | "bower_components/*" 23 | ], 24 | 25 | "stripPrefix": "public", 26 | 27 | "runtimeCaching": [ 28 | { 29 | "urlPattern": "/(.*)json", 30 | "handler": "fastest" 31 | } 32 | ], 33 | 34 | "navigateFallback": "/app-shell", 35 | 36 | "dynamicUrlToDependencies": { 37 | "/app-shell": [ 38 | "views/content.ejs", 39 | "views/footer.ejs", 40 | "views/header.ejs", 41 | "views/site-navigation.ejs" 42 | ] 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /CONTRIBUTING.TXT: -------------------------------------------------------------------------------- 1 | Want to contribute? Great! First, read this page (including the small print at the end). 2 | 3 | ### Before you contribute 4 | Before we can use your code, you must sign the 5 | [Google Individual Contributor License Agreement] 6 | (https://cla.developers.google.com/about/google-individual) 7 | (CLA), which you can do online. The CLA is necessary mainly because you own the 8 | copyright to your changes, even after your contribution becomes part of our 9 | codebase, so we need your permission to use and distribute your code. We also 10 | need to be sure of various other things—for instance that you'll tell us if you 11 | know that your code infringes on other people's patents. You don't have to sign 12 | the CLA until after you've submitted your code for review and a member has 13 | approved it, but you must do it before we can put your code into our codebase. 14 | Before you start working on a larger contribution, you should get in touch with 15 | us first through the issue tracker with your idea so that we can help out and 16 | possibly guide you. Coordinating up front makes it much easier to avoid 17 | frustration later on. 18 | 19 | ### Code reviews 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. 22 | 23 | ### The small print 24 | Contributions made by corporations are covered by a different agreement than 25 | the one above, the 26 | [Software Grant and Corporate Contributor License Agreement] 27 | (https://cla.developers.google.com/about/google-corporate). 28 | -------------------------------------------------------------------------------- /content/ocean.xml: -------------------------------------------------------------------------------- 1 | 18 | 19 | Ocean 20 | 21 | /content/ocean 22 | A page about oceans. 23 | /images/ocean.jpg 24 | 1024 25 | 683 26 | Robert Padovani 27 | 30 | 31 |

32 | Ocean dolor sit amet, consectetur adipiscing elit. Maecenas lacinia, ligula et porttitor feugiat, ligula quam tempor augue, a tempus dui sapien quis ante. 33 |

34 |

35 | Sed porta luctus placerat. Donec auctor dapibus diam, id vehicula justo feugiat sed. Aliquam finibus mollis porta. 36 |

37 |

38 | In eu tortor orci. Morbi justo sapien, porta non rutrum eu, imperdiet ut massa. Vestibulum varius vitae nulla et tempor. 39 |

40 | 41 | ]]>
42 |
43 | -------------------------------------------------------------------------------- /content/desert.xml: -------------------------------------------------------------------------------- 1 | 18 | 19 | Desert 20 | 21 | /content/desert 22 | A page about deserts. 23 | /images/desert.jpg 24 | 1024 25 | 768 26 | Oratori Don Bosko 27 | 30 | 31 |

32 | Desert dolor sit amet, consectetur adipiscing elit. Maecenas lacinia, ligula et porttitor feugiat, ligula quam tempor augue, a tempus dui sapien quis ante. 33 |

34 |

35 | Sed porta luctus placerat. Donec auctor dapibus diam, id vehicula justo feugiat sed. Aliquam finibus mollis porta. 36 |

37 |

38 | In eu tortor orci. Morbi justo sapien, porta non rutrum eu, imperdiet ut massa. Vestibulum varius vitae nulla et tempor. 39 |

40 | 41 | ]]>
42 |
43 | -------------------------------------------------------------------------------- /content/index.xml: -------------------------------------------------------------------------------- 1 | 18 | 19 | Fields (Home) 20 | 21 | / 22 | A page about fields. 23 | /images/fields.jpg 24 | 1024 25 | 681 26 | Mathias Appel 27 | 30 | 31 |

32 | Fields (home page) dolor sit amet, consectetur adipiscing elit. Maecenas lacinia, ligula et porttitor feugiat, ligula quam tempor augue, a tempus dui sapien quis ante. 33 |

34 |

35 | Sed porta luctus placerat. Donec auctor dapibus diam, id vehicula justo feugiat sed. Aliquam finibus mollis porta. 36 |

37 |

38 | In eu tortor orci. Morbi justo sapien, porta non rutrum eu, imperdiet ut massa. Vestibulum varius vitae nulla et tempor. 39 |

40 | 41 | ]]>
42 |
43 | -------------------------------------------------------------------------------- /content/mountains.xml: -------------------------------------------------------------------------------- 1 | 18 | 19 | Mountains 20 | 21 | /content/mountains 22 | A page about mountains. 23 | /images/mountains.jpg 24 | 1024 25 | 611 26 | JCMU 27 | 30 | 31 |

32 | Mountains dolor sit amet, consectetur adipiscing elit. Maecenas lacinia, ligula et porttitor feugiat, ligula quam tempor augue, a tempus dui sapien quis ante. 33 |

34 |

35 | Sed porta luctus placerat. Donec auctor dapibus diam, id vehicula justo feugiat sed. Aliquam finibus mollis porta. 36 |

37 |

38 | In eu tortor orci. Morbi justo sapien, porta non rutrum eu, imperdiet ut massa. Vestibulum varius vitae nulla et tempor. 39 |

40 | ]]>
41 |
42 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('generate:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var express = require('express'); 18 | var path = require('path'); 19 | var logger = require('morgan'); 20 | var cookieParser = require('cookie-parser'); 21 | var bodyParser = require('body-parser'); 22 | 23 | var allRoutes = require('./routes/all'); 24 | 25 | var app = express(); 26 | 27 | // View engine setup 28 | app.set('views', path.join(__dirname, 'views')); 29 | app.set('view engine', 'ejs'); 30 | 31 | app.use(logger('dev')); 32 | app.use(bodyParser.json()); 33 | app.use(bodyParser.urlencoded({ extended: false })); 34 | app.use(cookieParser()); 35 | 36 | app.use(express.static(path.join(__dirname, 'public'))); 37 | app.use('/bower_components', express.static(path.join(__dirname, 'bower_components'))); 38 | 39 | app.use('/', allRoutes); 40 | 41 | // Default 404 route and forward to error handler 42 | app.use(function(req, res, next) { 43 | var err = new Error('Not Found'); 44 | err.status = 404; 45 | next(err); 46 | }); 47 | 48 | // Development error handler will print stacktrace 49 | if (app.get('env') === 'development') { 50 | app.use(function(err, req, res, next) { 51 | res.status(err.status || 500); 52 | res.render('error', { 53 | message: err.message, 54 | error: err, 55 | page: { 56 | title: 'Error' 57 | } 58 | }); 59 | }); 60 | } 61 | 62 | // Production error handler no stacktraces leaked to user 63 | app.use(function(err, req, res, next) { 64 | res.status(err.status || 500); 65 | res.render('error', { 66 | message: err.message, 67 | error: {} 68 | }); 69 | }); 70 | 71 | module.exports = app; 72 | -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | *, html, body { 18 | margin: 0; 19 | padding: 0; 20 | } 21 | 22 | body { 23 | font: 14pt "Lucida Grande", Helvetica, Arial, sans-serif; 24 | background: whiteSmoke; 25 | } 26 | 27 | body.offline #content { 28 | opacity: 0.66; 29 | } 30 | 31 | a { 32 | color: #00B7FF; 33 | } 34 | 35 | h1 { 36 | padding: 0 5px; 37 | display: inline-block; 38 | line-height: 50pt; 39 | } 40 | 41 | h2 { 42 | padding: 10px 5px; 43 | } 44 | 45 | #wrapper { 46 | width: 800px; 47 | max-width: 100%; 48 | margin: auto; 49 | background: white; 50 | box-shadow: 0 0 15px grey; 51 | } 52 | 53 | nav { 54 | background: #333; 55 | height: 40px; 56 | } 57 | 58 | nav a { 59 | padding: 3px 2px; 60 | margin: 0 5px; 61 | color: white; 62 | text-decoration: none; 63 | line-height: 40px; 64 | } 65 | 66 | nav a:hover, nav a.selected { 67 | color: #00B7FF; 68 | } 69 | 70 | #content { 71 | padding: 5px 0; 72 | } 73 | 74 | p { 75 | padding: 10px 5px; 76 | } 77 | 78 | #title-container a, .amp-flag { 79 | float: right; 80 | line-height: 24px; 81 | 82 | background: #00B7FF; 83 | padding: 3px; 84 | border-radius: 5px; 85 | width: 50px; 86 | text-align: center; 87 | margin: 5px; 88 | color: white; 89 | text-decoration: none; 90 | } 91 | 92 | .server-flags { 93 | font-family: monospace; 94 | text-align: center; 95 | padding: 10px; 96 | color: grey; 97 | } 98 | 99 | .samples-list { 100 | font-family: monospace; 101 | text-align: center; 102 | padding: 10px; 103 | color: grey; 104 | } 105 | 106 | .image-copyright { 107 | text-align: center; 108 | color: grey; 109 | } 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Indexable PWA 2 | 3 | A sample ExpressJS PWA that explores and demonstrates the best 4 | practices for implementing a PWA in regards to indexability of content. 5 | 6 | Can be configured for 3 specific rendering modes: 7 | * Client 8 | * Server 9 | * Hybrid 10 | 11 | ## Local Development Setup 12 | 13 | Install this package via npm: 14 | 15 | `npm install` 16 | 17 | Install bower and the fetch polyfill to support Safari: 18 | 19 | ~~~~ 20 | npm install bower 21 | bower install fetch 22 | ~~~~ 23 | 24 | Run the server via: 25 | 26 | `node server.js` 27 | 28 | ## Deploying to App Engine 29 | 30 | Follow this link to setup your App Engine project for Node.JS: 31 | 32 | https://cloud.google.com/nodejs/ 33 | 34 | To deploy a particular configuration to App Engine specify the yaml file: 35 | 36 | `gcloud app deploy app.yaml` 37 | 38 | Use app.yaml to set environment variables to override the config.js file. 39 | 40 | Example .yaml file: 41 | 42 | ~~~~ 43 | runtime: nodejs 44 | vm: true 45 | env_variables: 46 | AMP_MODE: disabled 47 | RENDER_MODE: hybrid 48 | UPDATE_MODE: json 49 | ~~~~ 50 | 51 | ## Configuration Patterns 52 | 53 | Configure config.js: 54 | 55 | * ampMode: [enabled, disabled] 56 | * renderMode: [server, client, hybrid] 57 | * updateMode: [json, html, disabled] 58 | * json: causes AJAX requests to be fetched as JSON from the server and 59 | the DOM to be updated via reading the JSON. 60 | * html: causes AJAX requests to be fetched as HTML from the server, 61 | the HTML is parsed on the client and the DOM to be updated via reading 62 | the parsed HTML from the server. 63 | 64 | Recommended configuration patterns: 65 | 66 | ### Server Sample 67 | 68 | For server-side rendering: 69 | 70 | ~~~~ 71 | { 72 | "ampMode": "disabled", 73 | "renderMode": "server", 74 | "updateMode": "disabled", 75 | "googleSiteVerificationToken": "" 76 | } 77 | ~~~~ 78 | 79 | ### Client Sample 80 | 81 | For client-side rendering. 82 | 83 | updateMode can be configured as either 'json' or 'html'. 84 | 85 | ~~~~ 86 | { 87 | "ampMode": "disabled", 88 | "renderMode": "client", 89 | "updateMode": "json", 90 | "googleSiteVerificationToken": "" 91 | } 92 | ~~~~ 93 | 94 | ### Hybrid Sample 95 | 96 | For hybrid rendering. 97 | 98 | updateMode can be configured as either 'json' or 'html'. 99 | 100 | ~~~~ 101 | { 102 | "ampMode": "disabled", 103 | "renderMode": "hybrid", 104 | "updateMode": "json", 105 | "googleSiteVerificationToken": "" 106 | } 107 | ~~~~ 108 | -------------------------------------------------------------------------------- /public/additional-sw-scripts.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | console.log('Installing additional SW logic'); 18 | 19 | const CACHE_VERSION = 1; 20 | let CURRENT_CACHES = { 21 | offline: 'offline-v' + CACHE_VERSION 22 | }; 23 | const OFFLINE_URL = '/offline'; 24 | 25 | // Returns a request for a URL which is guaranteed to be freshly accessed 26 | // from the network and to have avoided the cache. 27 | // Newer versions of Chrome support this via the cache: reload key pair. 28 | // But if the request fails to include the cache key after being created then 29 | // we know that failed. In which case we'll append a timestamp to the URL to 30 | // ensure it's going to be a fresh request. 31 | function createCacheBustedRequest(url) { 32 | let request = new Request(url, {cache: 'reload'}); 33 | // See https://fetch.spec.whatwg.org/#concept-request-mode 34 | // This is not yet supported in Chrome as of M48, so we need to explicitly 35 | // check to see if the cache: 'reload' option had any effect. 36 | if ('cache' in request) { 37 | return request; 38 | } 39 | 40 | // If {cache: 'reload'} didn't have any effect, append a cache-busting URL 41 | // parameter instead. 42 | let bustedUrl = new URL(url, self.location.href); 43 | bustedUrl.search += (bustedUrl.search ? '&' : '') + 'cachebust=' + Date.now(); 44 | return new Request(bustedUrl); 45 | } 46 | 47 | self.addEventListener('install', function(event) { 48 | event.waitUntil( 49 | // We can't use cache.add() here, since we want OFFLINE_URL to be the 50 | // cache key, but the actual URL we request should always be fresh from 51 | // the network so we use the function "createCacheBustedRequest" to 52 | // guarantee this. 53 | fetch(createCacheBustedRequest(OFFLINE_URL)).then(function(response) { 54 | return caches.open(CURRENT_CACHES.offline).then(function(cache) { 55 | return cache.put(OFFLINE_URL, response); 56 | }); 57 | }) 58 | ); 59 | }); 60 | 61 | self.addEventListener('fetch', function(event) { 62 | // We only want to call event.respondWith() if this is a navigation request 63 | // for an HTML page. 64 | // request.mode of 'navigate' is unfortunately not supported in Chrome 65 | // versions older than 49, so we need to include a less precise fallback, 66 | // which checks for a GET request with an Accept: text/html header. 67 | if (event.request.mode === 'navigate' || 68 | (event.request.method === 'GET' && 69 | event.request.headers.get('accept').includes('text/html'))) { 70 | 71 | // If the resource is cached, return it from the cache. 72 | // Otherwise fetch it from the server and then cache it. 73 | // If that fails as well, then fall back to the offline page we 74 | // cached at the start. 75 | event.respondWith( 76 | caches.match(event.request).then(function(response) { 77 | return response || fetch(event.request).then(function(response) { 78 | return caches.open(CURRENT_CACHES.offline).then(function(cache) { 79 | return cache.put(event.request, response); 80 | }); 81 | }).catch(function(error) { 82 | // The catch is only triggered if fetch() throws an exception, 83 | // which will most likely happen due to the server being 84 | // unreachable. 85 | console.log('Fetch failed; serving offline page instead.', error); 86 | return caches.match(OFFLINE_URL); 87 | }); 88 | }) 89 | ); 90 | } 91 | }); 92 | -------------------------------------------------------------------------------- /views/header.ejs: -------------------------------------------------------------------------------- 1 | 2 | amp<% } %> lang="en"> 3 | 4 | 5 | <%- page.title %> 6 | 7 | 8 | 9 | <% if (ampRequested) { %> 10 | 11 | <% } else { %> 12 | 13 | 14 | <% if (ampEnabled) { %> 15 | 16 | <% } %> 17 | <% } %> 18 | 19 | 20 | 21 | 22 | 23 | 24 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | <% if (ampRequested) { %> 76 | 77 | 78 | 79 | 80 | <% } else { %> 81 | 82 | 83 | 84 | 87 | 88 | 89 | <% } %> 90 | 91 | 92 | <% if (ampRequested) { %> 93 | 96 | 97 | <% } %> 98 | 99 |
100 | -------------------------------------------------------------------------------- /routes/all.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var express = require('express'); 18 | var router = express.Router(); 19 | var fs = require('fs'); 20 | var xml2js = require('xml2js'); 21 | var xmlParser = new xml2js.Parser(); 22 | var swPrecache = require('sw-precache'); 23 | var fs = require('fs'); 24 | var path = require('path'); 25 | 26 | // Read the server's configuration from a local JSON file. 27 | // Allow for overrides from server environment variables. 28 | var serverConfig = JSON.parse(fs.readFileSync(path.join(__dirname, 29 | '/../config.json'), 'utf8')); 30 | 31 | if ('AMP_MODE' in process.env) { 32 | serverConfig.ampMode = process.env.AMP_MODE; 33 | } 34 | 35 | if ('RENDER_MODE' in process.env) { 36 | serverConfig.renderMode = process.env.RENDER_MODE; 37 | } 38 | 39 | if ('UPDATE_MODE' in process.env) { 40 | serverConfig.updateMode = process.env.UPDATE_MODE; 41 | } 42 | 43 | // Google Site Verification is necessary for some Google tools such as 44 | // Search Console. For convenience this is configurable. 45 | if ('GOOGLE_SITE_VERIFICATION_TOKEN' in process.env) { 46 | serverConfig.googleSiteVerificationToken = 47 | process.env.GOOGLE_SITE_VERIFICATION_TOKEN; 48 | } 49 | 50 | console.log('Server Configuration:', serverConfig); 51 | 52 | // Based on the render mode we use a different sw-precache configuration file 53 | // to generate the Service Worker. 54 | var swPrecacheConfig = require('./../sw-precache-config-' + 55 | serverConfig.renderMode + '.js'); 56 | swPrecache.write(path.join(__dirname, 57 | '/../public/generated-service-worker.js'), swPrecacheConfig, 58 | function(err) { 59 | if (err) console.log(err); 60 | }); 61 | 62 | // Cache the CSS in memory to embed into AMP documents where necessary: 63 | var globalCSS = ''; 64 | 65 | fs.readFile(path.join(__dirname, '/../public/stylesheets/style.css'), 66 | function(err, data) { 67 | if (err) console.log(err); 68 | 69 | globalCSS = data; 70 | console.log('CSS preloaded for AMP requests.'); 71 | }); 72 | 73 | function getPath(url) { 74 | return url.substring(0, url.lastIndexOf('.')); 75 | } 76 | 77 | function renderPage(req, res, next, templateData) { 78 | // SSL or bust! 79 | var siteUrl = 'https://' + req.get('host'); 80 | 81 | var typeRequest = req.params.typeRequest || 'html'; 82 | 83 | // 84 | if (typeRequest == 'amp' && serverConfig.ampMode != 'disabled') { 85 | // CSS in AMP must be embedded 86 | templateData.ampCSS = globalCSS; 87 | 88 | templateData.ampRequested = true; 89 | templateData.altUrl = getPath(req.path); 90 | templateData.canonicalUrl = siteUrl + templateData.page.canonical; 91 | templateData.serviceWorkerUrl = siteUrl + '/generated-service-worker.js'; 92 | } else { 93 | templateData.ampRequested = false; 94 | templateData.altUrl = getPath(req.path) + '.amp'; 95 | templateData.canonicalUrl = siteUrl + templateData.page.canonical; 96 | templateData.ampUrl = siteUrl + templateData.page.canonical + '.amp'; 97 | } 98 | 99 | // Attach the server config 100 | templateData.serverConfig = serverConfig; 101 | templateData.siteUrl = siteUrl; 102 | templateData.ampEnabled = (serverConfig.ampMode == 'enabled'); 103 | 104 | // If the request is for JSON then don't render the content in 105 | // the HTML template 106 | if (typeRequest == 'json') 107 | { 108 | res.send(JSON.stringify(templateData)); 109 | } else { 110 | res.render('content', templateData); 111 | } 112 | } 113 | 114 | // Render 404 page 115 | function render404(req, res, next) { 116 | res.status(404); 117 | 118 | renderPage(req, res, next, { 119 | page: { 120 | title: 'Page not found', 121 | canonical: 'http://localhost/', 122 | content: 'Page could not be found.' 123 | } 124 | }); 125 | } 126 | 127 | // Route for all content page requests 128 | router.get(['/(.:typeRequest)?', '/content/:id.:typeRequest?'], 129 | function(req, res, next) { 130 | // Specific content page requested, defaults to index 131 | var contentId = req.params.id || 'index'; 132 | 133 | // Specific content page requested, accepts 'json' or 'html', defaults to html 134 | var typeRequest = req.params.typeRequest || 'html'; 135 | 136 | console.log('Content page request, id:', contentId, '& type:', typeRequest); 137 | 138 | // If the server is set to client mode and the request is for the HTML 139 | // payload then we serve back the empty App Shell. 140 | if (serverConfig.renderMode == 'client' && typeRequest == 'html') { 141 | renderPage(req, res, next, { 142 | page: {} 143 | }); 144 | return; 145 | } 146 | 147 | // Find the conten XML file: 148 | var contentFilePath = path.join(__dirname, 149 | '/../content/' + contentId + '.xml'); 150 | 151 | fs.readFile(contentFilePath, function(err, data) { 152 | if (err) return render404(req, res, next); 153 | 154 | // XML content extraction for templating 155 | xmlParser.parseString(data, function (err, result) { 156 | if (err) return render404(req, res, next); 157 | 158 | // Map the XML content 159 | var templateData = { 160 | page: { 161 | title: result.document.title[0], 162 | canonical: result.document.canonical[0], 163 | content: result.document.content[0], 164 | description: result.document.description[0], 165 | metaImage: result.document.thumbnailPath[0], 166 | thumbnailPath: result.document.thumbnailPath[0], 167 | thumbnailWidth: result.document.thumbnailWidth[0], 168 | thumbnailHeight: result.document.thumbnailHeight[0], 169 | copyright: result.document.copyright[0] 170 | } 171 | }; 172 | 173 | // Render the page: 174 | renderPage(req, res, next, templateData); 175 | }); 176 | }); 177 | }); 178 | 179 | // Render robots.txt template 180 | router.get('/robots.txt', function(req, res, next) { 181 | res.render('robots', {}); 182 | }) 183 | 184 | // Render manifest.json template 185 | router.get('/manifest.json', function(req, res, next) { 186 | res.render('manifest', {}); 187 | }) 188 | 189 | // Render the content template with empty data to act as the 'App Shell' 190 | router.get('/app-shell', function(req, res, next) { 191 | renderPage(req, res, next, { 192 | page: {} 193 | }); 194 | }) 195 | 196 | // Render the content template with offline feedback to the user 197 | router.get('/offline', function(req, res, next) { 198 | renderPage(req, res, next, { 199 | page: { 200 | title: 'Offline', 201 | content: 'You are offline.' 202 | } 203 | }); 204 | }) 205 | 206 | module.exports = router; 207 | -------------------------------------------------------------------------------- /public/javascripts/core.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var serverConfig = serverConfig || {}; 18 | 19 | var pwaDemo = { 20 | /** @param {string} url */ 21 | updatePageContent: function(url) { 22 | switch(serverConfig.updateMode) { 23 | case 'disabled': 24 | console.log('Dynamic content update attempted but updateMode is off.'); 25 | break; 26 | case 'json': 27 | fetch(url + '.json').then(function(response) { 28 | return response.text(); 29 | }).then(function(data) { 30 | pwaDemo.processJSON(JSON.parse(data)); 31 | }).catch(function(error) { 32 | console.log('Fetch error: ', error); 33 | 34 | pwaDemo.showOfflinePage(); 35 | }) 36 | break; 37 | case 'html': 38 | fetch(url).then(function(response) { 39 | return response.text(); 40 | }).then(function(data) { 41 | pwaDemo.processHTML(data); 42 | }).catch(function(error) { 43 | console.log('Fetch error: ', error); 44 | 45 | pwaDemo.showOfflinePage(); 46 | }) 47 | break; 48 | } 49 | }, 50 | // Processes raw HTML content from the server into the same JSON format 51 | // that is otherwise delivered from the server when updateMode is set to 52 | // JSON. Then immediately the processJSON function is called to finish 53 | // the work. 54 | /** @param {string} rawHTML */ 55 | processHTML: function(rawHTML) { 56 | console.log('Updating page with HTML: ', rawHTML); 57 | 58 | var tempDom = document.createElement('tempDom'); 59 | tempDom.innerHTML = rawHTML; 60 | 61 | var contentTag = tempDom.querySelector('#content'); 62 | var contentHTML = contentTag.innerHTML; 63 | 64 | var schemaTag = tempDom.querySelector('#schema-data'); 65 | if (schemaTag == null) { 66 | throw "HTML missing Schema Markup"; 67 | } 68 | 69 | var schemaData = JSON.parse(schemaTag.text); 70 | 71 | var titleTag = tempDom.querySelector('title'); 72 | if (titleTag == null) throw "Title tag is missing"; 73 | 74 | var canonicalTag = tempDom.querySelector('link[rel=canonical]'); 75 | if (canonicalTag == null) throw "Canonical tag is missing"; 76 | 77 | var preparedJSON = { 78 | page: { 79 | title: titleTag.text, 80 | canonical: canonicalTag.text, 81 | content: contentHTML, 82 | description: schemaData.description, 83 | metaImage: schemaData.image.url, 84 | thumbnailPath: schemaData.image.url, 85 | thumbnailWidth: schemaData.image.width, 86 | thumbnailHeight: schemaData.image.height, 87 | copyright: '' 88 | }, 89 | siteUrl: '' 90 | } 91 | 92 | pwaDemo.processJSON(preparedJSON); 93 | }, 94 | // Reads the JSON from the server (or from the client processed HTML) 95 | /** @param {!Object} jsonData */ 96 | processJSON: function(jsonData) { 97 | console.log('Updating page with JSON: ', jsonData); 98 | 99 | var canonical = window.location.toString(); 100 | var contentTag = document.querySelector('#content'); 101 | 102 | contentTag.innerHTML = 'content' in jsonData.page ? 103 | jsonData.page.content : ''; 104 | 105 | var newSchemaMeta = { 106 | '@context': 'http://schema.org', 107 | '@type': 'NewsArticle', 108 | 'mainEntityOfPage': { 109 | '@type': 'WebPage', 110 | '@id': 'https://google.com/article' 111 | }, 112 | 'headline': jsonData.page.title, 113 | 'image': { 114 | '@type': 'ImageObject', 115 | 'url': jsonData.siteUrl + jsonData.page.thumbnailPath, 116 | 'width': jsonData.page.thumbnailWidth, 117 | 'height': jsonData.page.thumbnailHeight 118 | }, 119 | 'datePublished': '2015-02-05T08:00:00+08:00', 120 | 'dateModified': '2015-02-05T09:20:00+08:00', 121 | 'author': { 122 | '@type': 'Person', 123 | 'name': 'John Doe' 124 | }, 125 | 'publisher': { 126 | '@type': 'Organization', 127 | 'name': 'ACME Industries', 128 | 'logo': { 129 | '@type': 'ImageObject', 130 | 'url': jsonData.siteUrl + '/images/logo.jpg', 131 | 'width': 600, 132 | 'height': 60 133 | } 134 | }, 135 | 'description': jsonData.page.description 136 | } 137 | 138 | document.querySelector('html > head > title').innerText = 139 | jsonData.page.title; 140 | document.querySelector('h2').innerText = jsonData.page.title; 141 | document.querySelector('#schema-data').innerHTML = 142 | JSON.stringify(newSchemaMeta); 143 | 144 | document.querySelector('html > head > link[rel=canonical]').href = 145 | canonical; 146 | 147 | document.querySelector('html > head > meta[property=og\\:title]').content = 148 | jsonData.page.title; 149 | document.querySelector('html > head > meta[property=og\\:url]').content = 150 | jsonData.siteUrl + jsonData.page.canonical; 151 | document.querySelector('html > head > meta[property=og\\:image]').content = 152 | jsonData.siteUrl + jsonData.page.metaImage; 153 | 154 | document.querySelector('html > head > meta[name=twitter\\:title]').content = 155 | jsonData.page.title; 156 | document.querySelector( 157 | 'html > head > meta[name=twitter\\:description]').content = 158 | jsonData.page.description; 159 | document.querySelector( 160 | 'html > head > meta[name=twitter\\:image]').content = 161 | jsonData.siteUrl + jsonData.page.metaImage; 162 | 163 | document.querySelector('.image-copyright p').innerText = 164 | 'Image is copyright of ' + jsonData.page.copyright; 165 | }, 166 | isRenderedPageEmpty: function() { 167 | var contentTag = document.querySelector('#content'); 168 | return contentTag.innerHTML.trim() == ''; 169 | }, 170 | checkOnlineOfflineState: function() { 171 | if (navigator.onLine) { 172 | document.body.classList.remove('offline'); 173 | } else { 174 | document.body.classList.add('offline'); 175 | } 176 | }, 177 | showOfflinePage: function() { 178 | pwaDemo.processJSON({ 179 | page: { 180 | title: 'Offline', 181 | canonical: '/', 182 | content: 'You are offline.', 183 | description: '', 184 | metaImage: '', 185 | thumbnailPath: '', 186 | thumbnailWidth: '', 187 | thumbnailHeight: '', 188 | copyright: '' 189 | }, 190 | siteUrl: '' 191 | }); 192 | } 193 | } 194 | 195 | // Detect Service Worker API availability and install if possible 196 | if ('serviceWorker' in navigator) { 197 | navigator.serviceWorker.register('/generated-service-worker.js', { 198 | 'scope': '/' 199 | }).then(function(registration) { 200 | // Registration was successful 201 | console.log('ServiceWorker registration successful with scope: ', 202 | registration.scope); 203 | }).catch(function(err) { 204 | // Registration failed 205 | console.log('ServiceWorker registration failed: ', err); 206 | }); 207 | } 208 | 209 | document.addEventListener('click', function(e) { 210 | if (serverConfig.renderMode == 'server' || 211 | serverConfig.updateMode == 'disabled') { 212 | // Allow the links to be traversed as per normal. 213 | return; 214 | } 215 | 216 | if (e.target.classList.contains('external')) { 217 | // Links designed to be regularly traversed. 218 | return; 219 | } 220 | 221 | if (e.target.tagName == 'A') { 222 | e.preventDefault(); 223 | 224 | var newUrl = e.target.href; 225 | var linkDescription = e.target.text; 226 | 227 | // Push the request on the 228 | history.pushState(false, linkDescription, newUrl); 229 | 230 | pwaDemo.updatePageContent(newUrl); 231 | } 232 | }, false); 233 | 234 | window.addEventListener('load', function(e) { 235 | // Fade out the content of the page slightly when the user is offline 236 | pwaDemo.checkOnlineOfflineState(); 237 | 238 | if (serverConfig.renderMode == 'server') { 239 | return; 240 | } 241 | 242 | // Listen for browser navigation state changes and update dynamically if 243 | // render mode isn't server driven 244 | window.addEventListener('popstate', function(event) { 245 | pwaDemo.updatePageContent(window.location.toString()); 246 | }); 247 | 248 | if (serverConfig.renderMode == 'client') { 249 | // Fetch the page content separately from the initial URL request 250 | pwaDemo.updatePageContent(window.location.toString()); 251 | } 252 | 253 | if (serverConfig.renderMode == 'hybrid' && pwaDemo.isRenderedPageEmpty()) { 254 | // Fetch the page content separately from the initial URL request 255 | // if the hybrid model received an empty page -- i.e from the service worker 256 | pwaDemo.updatePageContent(window.location.toString()); 257 | } 258 | }, false); 259 | 260 | window.addEventListener('offline', function(e) { 261 | console.log('Transitioned offline.'); 262 | 263 | pwaDemo.checkOnlineOfflineState(); 264 | }); 265 | 266 | window.addEventListener('online', function(e) { 267 | console.log('Transitioned online.'); 268 | 269 | pwaDemo.checkOnlineOfflineState(); 270 | 271 | if (serverConfig.renderMode == 'client' || 272 | serverConfig.renderMode == 'hybrid') { 273 | // Dynamically fetch new content if the user goes online while on a page 274 | pwaDemo.updatePageContent(window.location.toString()); 275 | } 276 | }); 277 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /public/javascripts/amp-core.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | window.AMP_CONFIG={"canary":false,"no-auth-in-prerender":1,"amp-sticky-ad":1,"amp-live-list":1,"amp-ios-overflow-x":1,"amp-experiment":1,"v":"031471559432224"};/*AMP_CONFIG*/try{(function(){var process={env:{NODE_ENV:"production"}};var f,aa=function(a){return"undefined"!=typeof window&&window===a?a:"undefined"!=typeof global?global:a}(this);function ba(a,b){function c(){}c.prototype=b.prototype;a.prototype=new c;a.prototype.constructor=a;for(var d in b)if(aa.Object.defineProperties){var e=aa.Object.getOwnPropertyDescriptor(b,d);e&&aa.Object.defineProperty(a,d,e)}else a[d]=b[d]}var ca=""; 17 | function r(a){var b=a||self,c;if(b.AMP_MODE)c=b.AMP_MODE;else{c=b;if(c.context&&c.context.mode)c=c.context.mode;else{var d=location.originalHash||location.hash,e=Object.create(null);if(d){if(0==d.indexOf("?")||0==d.indexOf("#"))d=d.substr(1);for(var d=d.split("&"),g=0;ga.length)return!1;var c=a.length-b.length;return a.indexOf(b,c)==c}function ua(a){var b=x.urls.cdn;return b.length>a.length?!1:0==a.lastIndexOf(b,0)};var z,va,wa=/[?&]amp_js[^&]*/; 24 | function A(a){z||(z=self.document.createElement("a"),va=self.UrlCache||(self.UrlCache=Object.create(null)));var b=va[a];if(b)return b;z.href=a;z.protocol||(z.href=z.href);var c={href:z.href,protocol:z.protocol,host:z.host,hostname:z.hostname,port:"0"==z.port?"":z.port,pathname:z.pathname,search:z.search,hash:z.hash,origin:null};"/"!==c.pathname[0]&&(c.pathname="/"+c.pathname);if("http:"==c.protocol&&80==c.port||"https:"==c.protocol&&443==c.port)c.port="",c.host=c.hostname;c.origin=z.origin&&"null"!= 25 | z.origin?z.origin:"data:"!=c.protocol&&c.host?c.protocol+"//"+c.host:c.href;return va[a]=c}function xa(a,b,c){c=void 0===c?"source":c;t().assert(null!=a,"%s %s must be available",b,c);var d=A(a);t().assert("https:"==d.protocol||/^(\/\/)/.test(a)||"localhost"==d.hostname||ta(d.hostname),'%s %s must start with "https://" or "//" or be relative and served from either https or from localhost. Invalid value: %s',b,c,a)} 26 | function Ha(a){var b=Object.create(null);if(!a)return b;if(0==a.indexOf("?")||0==a.indexOf("#"))a=a.substr(1);a=a.split("&");for(var c=0;c ["+b+"]"):Ra(a,function(a){return a.hasAttribute(b)})}function cb(a){return Ta(a,function(a){return a.hasAttribute("placeholder")})} 31 | function db(a){null==B&&(B=Va(a));return B?ra(a.querySelectorAll(":scope > [placeholder]")):Sa(a,function(a){return a.hasAttribute("placeholder")})}function eb(a){var b="template";null==B&&(B=Va(a));if(B)return a.querySelector(":scope > "+b);b=b.toUpperCase();return Ra(a,function(a){return a.tagName==b})}function fb(a,b,c){var d,e;try{e=a.open(b,c,d)}catch(g){w().error("dom","Failed to open url on target: ",c,g)}e||"_top"==c||(e=a.open(b,"_top"))};(function(a,b,c,d){function e(a,b){for(var c=0,d=a.length;cea.call(Da,a)&&Da.push(a)),d&&(c=a[b+"Callback"])&&c.call(a))}if(!(d in b)){var y="__"+d+(1E5*Math.random()>> 34 | 0),L="attached",C="detached",Ea="extends",Q="ADDITION",Fa="MODIFICATION",Kb="REMOVAL",ga="DOMAttrModified",Mc="DOMContentLoaded",Oe="DOMSubtreeModified",Aa="<",Wa="=",Pe=/^[A-Z][A-Z0-9]*(?:-[A-Z0-9]+)+$/,Qe="ANNOTATION-XML COLOR-PROFILE FONT-FACE FONT-FACE-SRC FONT-FACE-URI FONT-FACE-FORMAT FONT-FACE-NAME MISSING-GLYPH".split(" "),fa=[],za=[],X="",ja=b.documentElement,ea=fa.indexOf||function(a){for(var b=this.length;b--&&this[b]!==a;);return b},Ob=c.prototype,Pb=Ob.hasOwnProperty,$a=Ob.isPrototypeOf, 35 | Oc=c.defineProperty,Qb=c.getOwnPropertyDescriptor,Pc=c.getOwnPropertyNames,Re=c.getPrototypeOf,Qc=c.setPrototypeOf,Rc=!!c.__proto__,Se=c.create||function Kc(a){return a?(Kc.prototype=a,new Kc):this},Me=Qc||(Rc?function(a,b){a.__proto__=b;return a}:Pc&&Qb?function(){function a(b,c){for(var d,e=Pc(c),g=0,h=e.length;g tag.").href).href,pageViewId:String(Math.floor(1E4*a.Math.random())),sourceUrl:Ja(a.location.href)}})};function E(a){return a.services.viewer.obj};function F(a){return a.services.viewport.obj};var Sb;function Tb(a,b){var c=Sb||(Sb=Object.create(null));if(b in c)a=c[b];else{var d=b;a=-1!=Ub(a).indexOf(d)?!0:a.AMP_CONFIG&&a.AMP_CONFIG.hasOwnProperty(d)?Math.random()=a.length?0:parseInt(a[1],10)}function M(a){return D(a,"platform",gc)};function ic(a){this.B=a.document;this.Eb=a.document.head;this.Fa={};this.ed={};this.cb=M(a);this.Fa[A(a.location.href).origin]=!0;var b=this.B.createElement("link").relList;this.zb=b&&b.supports?{preconnect:b.supports("preconnect"),preload:b.supports("preload")}:{};this.a=E(a);this.ba=K(a)} 58 | ic.prototype.url=function(a,b){if(jc(a)){a=A(a).origin;var c=Date.now(),d=this.Fa[a];if(d&&c=c)return 0;if(1<=c)return 1;for(var d=0,e=1,g=0,h=0;8>h;h++){var g=this.getPointX(c),k=(this.getPointX(c+b)-g)/b;if(Math.abs(g-a)b&&8>h;h++)g=d.delay&&(d.started=!0)}for(c=0;ca&&(a=10);var b=Date.now()+a;return!this.isPending()||-10>b-this.Zb?(this.cancel(),this.Zb=b,this.P=this.ba.delay(this.pd,a),!0):!1};pd.prototype.ra=function(){this.P=-1;this.Zb=0;this.Z=!0;this.yd();this.Z=!1}; 80 | pd.prototype.cancel=function(){this.isPending()&&(this.ba.cancel(this.P),this.P=-1)};function qd(a){return Jb(a,"access","amp-access")};function rd(a){var b=this;this.win=a;this.Gb=Date.now();this.Aa=[];this.resources_=this.a=null;this.Nb=!1;this.me=Wc(this.win.document).then(function(){});oc(a).then(function(){b.tick("ol");b.flush()})}f=rd.prototype;f.coreServicesAvailable=function(){var a=this;this.a=E(this.win);this.resources_=Xc(this.win);this.a.onVisibilityChanged(this.flush.bind(this));sd(this);var b=this.a.whenMessagingReady();return b?b.then(function(){a.Nb=!0;td(a);ud(a);a.flush()}):Promise.resolve()}; 81 | function sd(a){var b=!a.a.hasBeenVisible(),c=b?-1:a.Gb;b&&a.a.whenFirstVisible().then(function(){c=Date.now()});vd(a).then(function(){if(b){var d=-1=p||"A"<=p&&"Z">=p||"0"<=p&&"9">=p))break}t().assert(k=p||"A"<=p&&"Z">=p||"0"<=p&&"9">=p);k--);0<=k?(c=a.substring(0,k+1).trim(),h=a.substring(k+1).trim()):(h=a,c=void 0);d.push({mediaQuery:c,size:l?h:b?bc(h):ac(h)})}});return new Hd(d)} 98 | function Hd(a){t().assert(0a.top&&0<=a.top&&O(this.ownerDocument.defaultView).mutate(function(){b.Wa()&&b.Sc()}))};b.applySizesAndMediaQuery=function(){void 0===this.Ya&&(this.Ya=this.getAttribute("media")||null);this.Ya&&this.classList.toggle("-amp-hidden-by-media-query",!this.ownerDocument.defaultView.matchMedia(this.Ya).matches); 106 | if(void 0===this.lb){var a=this.getAttribute("sizes");this.lb=a?Gd(a):null}this.lb&&(this.style.width=this.lb.select(this.ownerDocument.defaultView));void 0===this.Va&&(this.Va=(a=this.getAttribute("heights"))?Gd(a,!0):null);this.Va&&"responsive"===this.layout_&&this.La&&(this.La.style.paddingTop=this.Va.select(this.ownerDocument.defaultView))};b.changeSize=function(a,b){this.La&&(this.La.style.paddingTop="0");void 0!==a&&(this.style.height=a+"px");void 0!==b&&(this.style.width=b+"px")};b.attachedCallback= 107 | function(){void 0===Id&&(Id="content"in self.document.createElement("template"));Id||void 0!==this.Ca||(this.Ca=!!Qa(this,"template"));if(!this.Ca){if(!this.everAttached){this.implementation_ instanceof S||this.cd(this.implementation_);this.isUpgraded()||(this.classList.add("amp-unresolved"),this.classList.add("-amp-unresolved"));try{var a=this.getAttribute("layout"),b=this.getAttribute("width"),e=this.getAttribute("height"),g=this.getAttribute("sizes"),h=this.getAttribute("heights"),k;if(a)a:{for(var l in Wb)if(Wb[l]== 108 | a){k=Wb[l];break a}k=void 0}else k=null;t().assert(void 0!==k,"Unknown layout: %s",a);var m=b&&"auto"!=b?$b(b):b;t().assert(void 0!==m,"Invalid width value: %s",b);var n=e?$b(e):null;t().assert(void 0!==n,"Invalid height value: %s",e);var q,p,u,v;if(!(v=k&&"fixed"!=k&&"fixed-height"!=k||m&&n)){var H=this.tagName,H=H.toUpperCase();v=void 0===Xb[H]}if(v)q=m,p=n;else{var G=this.tagName.toUpperCase();if(!Xb[G]){var y=this.ownerDocument,L=G.replace(/^AMP\-/,""),C=y.createElement(L);C.controls=!0;C.style.position= 109 | "absolute";C.style.visibility="hidden";y.body.appendChild(C);Xb[G]={width:(C.offsetWidth||1)+"px",height:(C.offsetHeight||1)+"px"};y.body.removeChild(C)}var Ea=Xb[G];q=m||"fixed-height"==k?m:Ea.width;p=n||Ea.height}u=k?k:q||p?!p||q&&"auto"!=q?p&&q&&(g||h)?"responsive":"fixed":"fixed-height":"container";"fixed"!=u&&"fixed-height"!=u&&"responsive"!=u||t().assert(p,"Expected height to be available: %s",e);"fixed-height"==u&&t().assert(!q||"auto"==q,'Expected width to be either absent or equal "auto" for fixed-height layout: %s', 110 | b);"fixed"!=u&&"responsive"!=u||t().assert(q&&"auto"!=q,'Expected width to be available and not equal to "auto": %s',b);"responsive"==u?t().assert(cc(q)==cc(p),"Length units should be the same for width and height: %s, %s",b,e):t().assert(null===h,'Unexpected "heights" attribute for none-responsive layout');this.classList.add("-amp-layout-"+u);Zb(u)&&this.classList.add("-amp-layout-size-defined");if("nodisplay"==u)this.style.display="none";else if("fixed"==u)this.style.width=q,this.style.height=p; 111 | else if("fixed-height"==u)this.style.height=p;else if("responsive"==u){var Q=this.ownerDocument.createElement("i-amp-sizer");Q.style.display="block";Q.style.paddingTop=dc(p)/dc(q)*100+"%";this.insertBefore(Q,this.firstChild);this.La=Q}else"fill"!=u&&"container"!=u&&"flex-item"==u&&(q&&(this.style.width=q),p&&(this.style.height=p));this.layout_=u;if("nodisplay"!=this.layout_&&!this.implementation_.isLayoutSupported(this.layout_))throw Error("Layout not supported for: "+this.layout_);this.implementation_.layout_= 112 | this.layout_;this.implementation_.firstAttachedCallback();this.isUpgraded()?this.dispatchCustomEvent("amp:attached"):this.dispatchCustomEvent("amp:stubbed")}catch(Fa){Gc(Fa,this)}this.everAttached=!0}this.resources_.add(this)}};b.cd=function(a){var b=this,e=a||this.implementation_;if(1==this.Pa){this.Pa=4;var g=e.upgradeCallback();g?"function"==typeof g.then?g.then(function(a){return b.yb(a)}).catch(function(a){b.Pa=3;ma(a)}):this.yb(g):this.yb(e)}};b.detachedCallback=function(){this.Ca||(this.resources_.remove(this), 113 | this.implementation_.detachedCallback())};b.dispatchCustomEvent=function(){};b.prerenderAllowed=function(){return this.implementation_.prerenderAllowed()};b.createPlaceholder=function(){return this.implementation_.createPlaceholderCallback()};b.renderOutsideViewport=function(){return this.implementation_.renderOutsideViewport()};b.getLayoutBox=function(){return this.resources_.getResourceForElement(this).getLayoutBox()};b.getIntersectionChangeEntry=function(){var a=this.implementation_.getIntersectionElementLayoutBox(), 114 | b=this.implementation_.getViewport().getRect(),e=a,g=Date.now();b.x=b.left;b.y=b.top;var h=-1*b.x,k=-1*b.y,h=0==h&&0==k?e:J(e.left+h,e.top+k,e.width,e.height);h.x=h.left;h.y=h.top;a:{var k=Math.max(b.left,e.left),l=Math.min(b.left+b.width,e.left+e.width);if(k<=l){var m=Math.max(b.top,e.top),e=Math.min(b.top+b.height,e.top+e.height);if(m<=e){e=J(k,m,l-k,e-m);break a}}e=null}e=e||J(0,0,0,0);e.x=e.left;e.y=e.top;return{time:g,rootBounds:b,boundingClientRect:h,intersectionRect:e}};b.isRelayoutNeeded= 115 | function(){return this.implementation_.isRelayoutNeeded()};b.layoutCallback=function(){var a=this;this.isBuilt();this.dispatchCustomEvent("amp:load:start");var b=this.implementation_.layoutCallback();this.preconnect(!0);this.classList.add("-amp-layout");return b.then(function(){a.readyState="complete";a.O++;a.Oa(!1,!0);a.Jb&&(a.implementation_.firstLayoutCompleted(),a.Jb=!1)},function(b){a.O++;a.Oa(!1,!0);throw b;})};b.viewportCallback=function(a){var b=this;this.W=a;0==this.O&&(a?K(this.ownerDocument.defaultView).delay(function(){0== 116 | b.O&&b.W&&b.Oa(!0)},100):this.Oa(!1));this.isBuilt()&&this.dd(a)};b.dd=function(a){this.implementation_.inViewport_=a;this.implementation_.viewportCallback(a)};b.pauseCallback=function(){this.isBuilt()&&this.implementation_.pauseCallback()};b.resumeCallback=function(){this.isBuilt()&&this.implementation_.resumeCallback()};b.unlayoutCallback=function(){if(!this.isBuilt())return!1;var a=this.implementation_.unlayoutCallback();a&&(this.O=0,this.Jb=!0);return a};b.unlayoutOnPause=function(){return this.implementation_.unlayoutOnPause()}; 117 | b.collapse=function(){this.implementation_.collapse()};b.collapsedCallback=function(a){this.implementation_.collapsedCallback(a)};b.enqueAction=function(a){this.isBuilt()?this.Bc(a,!1):this.fa.push(a)};b.sd=function(){var a=this;if(this.fa){var b=this.fa;this.fa=null;b.forEach(function(b){a.Bc(b,!0)})}};b.Bc=function(a,b){try{this.implementation_.executeAction(a,b)}catch(e){ma("Action execution failed:",e,a.target.tagName,a.method)}};b.getRealChildNodes=function(){return Ua(this,function(a){return!Md(a)})}; 118 | b.getRealChildren=function(){return Sa(this,function(a){return!Md(a)})};b.getPlaceholder=function(){return cb(this)};b.togglePlaceholder=function(a){if(a){var b=this.getPlaceholder();b&&b.classList.remove("amp-hidden")}else for(var e=db(this),b=0;bthis.layoutWidth_||0g;g++){var h=d.createElement("div");h.classList.add("-amp-loader-dot"); 120 | e.appendChild(h)}b.appendChild(e);this.appendChild(b);this.ha=b;this.Tb=e}};b.Oa=function(a,b){var e=this;if((this.Ub=a)||this.ha)a&&!this.Wa()?this.Ub=!1:O(this.ownerDocument.defaultView).mutate(function(){var a=e.Ub;a&&!e.Wa()&&(a=!1);a&&e.Sc();if(e.ha&&(e.ha.classList.toggle("amp-hidden",!a),e.Tb.classList.toggle("amp-active",a),!a&&b)){var c=e.ha;e.ha=null;e.Tb=null;e.resources_.deferMutate(e,function(){var a=c;a.parentElement&&a.parentElement.removeChild(a)})}})};b.getOverflowElement=function(){void 0=== 121 | this.L&&(this.L=bb(this,"overflow"))&&(this.L.hasAttribute("tabindex")||this.L.setAttribute("tabindex","0"),this.L.hasAttribute("role")||this.L.setAttribute("role","button"));return this.L};b.overflowCallback=function(a,b,e){var g=this;this.getOverflowElement();this.L?(this.L.classList.toggle("amp-visible",a),this.L.onclick=a?function(){g.resources_.changeSize(g,b,e);O(g.ownerDocument.defaultView).mutate(function(){g.overflowCallback(!1,b,e)})}:null):a&&t().warn("CustomElement","Cannot resize element and overflow is not available", 122 | this)};return b}function Ld(a,b,c){T[b]=c;a.document.registerElement(b,{prototype:Nd(a,b)})};function Pd(a){this.element=a;this.win=a.ownerDocument.defaultView;this.compileCallback()}Pd.prototype.compileCallback=function(){};Pd.prototype.render=function(){throw Error("Not implemented");};Pd.prototype.unwrap=function(a){for(var b=null,c=a.firstChild;null!=c;c=c.nextSibling)if(3==c.nodeType){if(c.textContent.trim()){b=null;break}}else if(8!=c.nodeType)if(1==c.nodeType)if(b){b=null;break}else b=c;else b=null;return b||a};function Qd(a){this.b=a;this.Na={};this.gc={};this.Ta=void 0} 123 | Qd.prototype.renderTemplate=function(a,b){return Rd(this,a).then(function(a){return Sd(a,b)})};Qd.prototype.renderTemplateArray=function(a,b){return 0==b.length?Promise.resolve([]):Rd(this,a).then(function(a){return b.map(function(b){return Sd(a,b)})})};Qd.prototype.findAndRenderTemplate=function(a,b){return this.renderTemplate(Td(a),b)};Qd.prototype.findAndRenderTemplateArray=function(a,b){return this.renderTemplateArray(Td(a),b)}; 124 | function Td(a){var b,c=a.getAttribute("template");b=c?a.ownerDocument.getElementById(c):eb(a);t().assert(b,"Template not found for %s",a);t().assert("TEMPLATE"==b.tagName,'Template element must be a "template" tag %s',b);return b} 125 | function Rd(a,b){var c=b.__AMP_IMPL_;if(c)return Promise.resolve(c);var c=t().assert(b.getAttribute("type"),"Type must be specified: %s",b),d=b.__AMP_WAIT_;if(d)return d;d=Ud(a,b,c).then(function(a){var c=b.__AMP_IMPL_=new a(b);delete b.__AMP_WAIT_;return c});return b.__AMP_WAIT_=d}function Ud(a,b,c){if(a.Na[c])return a.Na[c];Vd(a,b,c);var d;b=new Promise(function(a){d=a});a.Na[c]=b;a.gc[c]=d;return b} 126 | function Vd(a,b,c){if(!a.Ta){a.Ta=a.b.Object.create(null);for(var d=a.b.document.querySelectorAll("script[custom-template]"),e=0;e",b,c)}function Sd(a,b){a=a.render(b);var c=a.getElementsByTagName("a");for(b=0;b?',h,b):b.id&&"amp-"==b.id.substring(0,4)?(b.__AMP_ACTION_QUEUE__||(b.__AMP_ACTION_QUEUE__=[]),b.__AMP_ACTION_QUEUE__.push(c)):je("Target must be an AMP element or have an AMP ID",h,b)} 137 | function de(a,b,c,d){return t().assert(c,"Invalid action definition in %s: [%s] %s",b,a,d||"")}function ee(a,b,c,d,e){void 0!==e?de(a,b,c.type==d&&c.value==e,"; expected ["+e+"]"):de(a,b,c.type==d);return c}var ge=1,U=2,he=3;function fe(a){this.u=a;this.Fb=-1}fe.prototype.next=function(a){var b=ke(this,a||!1);this.Fb=b.index;return b};fe.prototype.peek=function(a){return ke(this,a||!1)}; 138 | function ke(a,b){var c=a.Fb+1;if(c>=a.u.length)return{type:ge,index:a.Fb};var d=a.u.charAt(c);if(-1!=" \t\n\r\f\v\u00a0\u2028\u2029".indexOf(d)){for(c++;c=a.u.length)return{type:ge,index:c};d=a.u.charAt(c)}if(b&&(le(d)||"."==d&&c+1=a};function me(a){if(!a.defaultPrevented){var b=a.target;if(b&&"FORM"==b.tagName){var c=b.getAttribute("action");t().assert(c,"form action attribute is required: %s",b);xa(c,b,"action");t().assert(!ua(c),"form action should not be on AMP CDN: %s",b);c=b.getAttribute("target");t().assert(c,"form target attribute is required: %s",b);t().assert("_blank"==c||"_top"==c,"form target=%s is invalid can only be _blank or _top: %s",c,b);var d=b.classList.contains("-amp-form"),e;(e=d?!b.hasAttribute("amp-novalidate"): 140 | !b.hasAttribute("novalidate"))&&b.checkValidity&&!b.checkValidity()&&a.preventDefault()}}};var ne=[/(^|\.)google\.(com?|[a-z]{2}|com?\.[a-z]{2})$/]; 141 | function oe(a){var b=this;this.win=a;this.Lb=this.win.parent&&this.win.parent!=this.win;this.Ac=D(this.win,"documentState",Yc);this.N=!0;this.ac=!1;this.hd=this.tb="visible";this.Y=1;this.la="natural";this.l=0;this.Vc=new I;this.Ra=new I;this.jd=new I;this.Fc=new I;this.vc=new I;this.$a=this.qa=null;this.ia=[];this.o={};this.V=this.ld=null;this.le=new Promise(function(a){b.ld=a});this.win.name&&0==this.win.name.indexOf("__AMP__")&&pe(this.win.name.substring(7),this.o);this.win.location.hash&&pe(this.win.location.hash, 142 | this.o);this.N=!parseInt(this.o.off,10);this.ac=parseInt(this.o.history,10)||this.ac;qe(this,this.o.visibilityState);this.Y=parseInt(this.o.prerenderSize,10)||this.Y;this.la=this.o.viewportType||this.la;var c=M(this.win);"natural"==this.la&&this.Lb&&c.isIos()&&(this.la="natural-ios-embed");c.isIos()&&"natural-ios-embed"!=this.la&&r(a).development&&(this.la="natural-ios-embed");this.l=parseInt(this.o.paddingTop,10)||this.l;this.Rd="1"===this.o.csi;this.na=(this.Lb||"1"===this.o.webview)&&!this.win.AMP_TEST_IFRAME; 143 | this.Bb=this.isVisible();this.Ac.onVisibilityChanged(this.Uc.bind(this));this.Vb=this.na?K(this.win).timeoutPromise(2E4,new Promise(function(a){b.Mc=a})).catch(function(a){throw re(a);}):null;this.Za=this.na?this.Vb.catch(function(a){Gc(re(a))}):null;var d,e;this.na?this.win.location.ancestorOrigins?(d=0=this.ja.length-1)){a=[];for(var b=this.ja.length-1;b>this.h;b--)this.ja[b]&&(a.push(this.ja[b]),this.ja[b]=void 0);this.ja.splice(this.h+1);if(0this.win.history.length-2&&(c=this.win.history.length-2,this.ca(c));c=void 0==b?c+1:b=b)return Promise.resolve(a.h);a.va=we(a.h-b);var c=De(a);a.win.history.go(-b);return c.then(function(){return Promise.resolve(a.h)})} 164 | f.Gc=function(a,b,c){a||(a={});var d=this.h+1;a["AMP.History"]=d;this.Td(a,b,c);d!=this.win.history.length-1&&(d=this.win.history.length-1,a["AMP.History"]=d,this.eb(a));this.ca(d)};f.zd=function(a,b,c){a||(a={});var d=Math.min(this.h,this.win.history.length-1);a["AMP.History"]=d;this.eb(a,b,c);this.ca(d)};f.ca=function(a){a=Math.min(a,this.win.history.length-1);this.h!=a&&(this.h=a,this.K&&this.K(a))}; 165 | function Ee(a){this.a=a;this.h=0;this.K=null;this.ie=this.a.onHistoryPoppedEvent(this.Hd.bind(this))}f=Ee.prototype;f.M=function(){this.ie()};f.setOnStackIndexUpdated=function(a){this.K=a};f.push=function(){this.ca(this.h+1);this.a.postPushHistory(this.h);return Promise.resolve(this.h)};f.pop=function(a){if(a>this.h)return Promise.resolve(this.h);this.a.postPopHistory(a);this.ca(a-1);return Promise.resolve(this.h)};f.Hd=function(a){this.ca(a.newStackIndex)}; 166 | f.ca=function(a){this.h!=a&&(this.h=a,this.K&&this.K(a))};function Fe(a){Ab(a,"history",function(){var b=ve(a),b=b.isOvertakeHistory()?new Ee(b):new Ae(a);return new xe(a,b)})};function Ge(a){P.call(this,a);this.Rb=oc;this.xa=!0;this.Yc=this.G=null}ba(Ge,P);f=Ge.prototype;f.isLayoutSupported=function(a){return Zb(a)}; 167 | f.Hb=function(){if(!this.G){this.xa=!0;this.element.hasAttribute("fallback")&&(this.xa=!1);this.G=new Image;this.element.id&&this.G.setAttribute("amp-img-id",this.element.id);this.propagateAttributes(["alt","referrerpolicy"],this.G);this.applyFillContent(this.G,!0);this.G.width=dc(this.element.getAttribute("width"));this.G.height=dc(this.element.getAttribute("height"));this.element.appendChild(this.G);var a;a=this.element;var b=a.getAttribute("srcset");if(b){a=b.match(/\s*(?:[\S]*)(?:\s+(?:-?(?:\d+(?:\.(?:\d+)?)?|\.\d+)[a-zA-Z]))?(?:\s*,)?/g); 168 | t().assert(0=a.getLayoutWidth())return Promise.resolve();var b=a.Yc.select(a.getLayoutWidth(),a.getDpr()).url;if(b==a.G.getAttribute("src"))return Promise.resolve();a.G.setAttribute("src",b);return a.Rb(a.G).then(function(){!a.xa&&a.G.classList.contains("-amp-ghost")&&a.getVsync().mutate(function(){a.G.classList.remove("-amp-ghost");a.toggleFallback(!1)})})}function Ie(a){a.getVsync().mutate(function(){a.G.classList.add("-amp-ghost");a.toggleFallback(!0)})};function Je(a){function b(a){P.apply(this,arguments)}ba(b,P);b.prototype.getPriority=function(){return 1};b.prototype.isLayoutSupported=function(a){return"fixed"==a};b.prototype.buildCallback=function(){this.element.setAttribute("aria-hidden","true")};b.prototype.layoutCallback=function(){var a=this;xc(this.element);var b=this.element.getAttribute("src");return this.win.services["url-replace"].obj.expand(this.assertSource(b)).then(function(b){var d=new Image;d.src=b;a.element.appendChild(d)})};b.prototype.assertSource= 171 | function(a){t().assert(/^(https\:\/\/|\/\/)/i.test(a),'The src attribute must start with "https://" or "//". Invalid value: '+a);return a};Ld(a,"amp-pixel",b)};function Ke(a){this.c=a;this.ad=Object.create(null)}Ke.prototype.addTransition=function(a,b,c){this.ad[a+"|"+b]=c};Ke.prototype.setState=function(a){var b=this.c;this.c=a;(a=this.ad[b+"|"+a])&&a()};function Xe(a,b){var c=this;this.win=a;this.Sd=b;this.s=[];this.Qc=new I;this.xc=function(a){a.target&&Ye(c,a.target)};this.wc=function(){K(a).delay(function(){Ye(c,c.win.document.activeElement)},500)};this.win.document.addEventListener("focus",this.xc,!0);this.win.addEventListener("blur",this.wc)}f=Xe.prototype;f.M=function(){this.win.document.removeEventListener("focus",this.xc,!0);this.win.removeEventListener("blur",this.wc)};f.onFocus=function(a){return this.Qc.add(a)}; 172 | function Ye(a,b){var c=Date.now();0==a.s.length||a.s[a.s.length-1].el!=b?a.s.push({el:b,time:c}):a.s[a.s.length-1].time=c;a.purgeBefore(c-a.Sd);a.Qc.fire(b)}f.getLast=function(){return 0==this.s.length?null:this.s[this.s.length-1].el};f.purgeBefore=function(a){for(var b=this.s.length-1,c=0;c=a){b=c-1;break}-1!=b&&this.s.splice(0,b+1)}; 173 | f.hasDescendantsOf=function(a){this.win.document.activeElement&&Ye(this,this.win.document.activeElement);for(var b=0;bc.right)return!1;if(b.bottomc.bottom)h=b.top-c.bottom,1==d&&(g=2);else return!0;return hd)a.clearInterval(e),b(),h||w().error("ie-media-bug","IE media never resolved")},10)})}function cf(a){var b="(min-width: "+a.innerWidth+"px)"+(" AND (max-width: "+a.innerWidth+"px)");try{return a.matchMedia(b).matches}catch(c){return w().error("ie-media-bug","IE matchMedia failed: ",c),!0}};function df(a){this.win=a;this.lc=this.Id.bind(this);this.mc=this.Jd.bind(this);this.nc=this.Kd.bind(this);this.nd=this.Nc.bind(this);this.od=this.Fd.bind(this);this.Db="ontouchstart"in a||void 0!==a.navigator.maxTouchPoints&&0=this.Oc&&lc(this.win.document,"mousemove",this.nc)};function ef(a,b,c,d){this.doc=a;this.j=b;this.l=c;this.qb=d;this.i=null;this.qd=0;this.w=[]}f=ef.prototype;f.setVisible=function(a){var b=this;this.i&&this.j.mutate(function(){wc(b.i,"visibility",a?"visible":"hidden")})}; 189 | f.setup=function(){var a=this.doc.styleSheets;if(a){for(var b=[],c=0;ch.offsetHeight&&(!!m&&0==parseInt(m,10)||!!p&&0==parseInt(p,10));v&&(c=!0);b[g.id]={fixed:l,transferrable:v,top:m,zIndex:k.getPropertyValue("z-index")}}else b[g.id]={fixed:!1,transferrable:!1,top:"",zIndex:""}}else b[g.id]={fixed:!1, 193 | transferrable:!1,top:"",zIndex:""}})},mutate:function(b){if(c&&a.qb){var e=lf(a);e.className!=a.doc.body.className&&(e.className=a.doc.body.className)}a.w.forEach(function(c,e){var k=b[c.id];if(k){var l=k,m=c.element,n=c.fixedNow;(c.fixedNow=l.fixed)?(m.style.top=l.top?"calc("+l.top+" + "+a.l+"px)":"",!n&&a.qb&&(l.transferrable?mf(a,c,e,l):kf(a,c))):n&&(m.style.top&&(m.style.top=""),kf(a,c))}})}},{}).catch(function(a){w().error("FixedLayer","Failed to mutate fixed elements:",a)})}; 194 | f.trySetupFixedSelectorsNoInline=function(a){try{for(var b=0;bc?g(Error("timeout")):(k=d,l(a)):e()}});l({})}):Promise.reject(Error("CANCELLED"))};f.ib=function(){this.P||(this.P=!0,qf(this))};function qf(a){a.a.isVisible()?a.Ud(a.pc):a.ra.schedule()} 204 | f.be=function(){this.P=!1;var a=this.D,b=this.fc,c=this.Wb;this.ab=this.Wb=null;this.D=this.Yb;this.fc=this.Xb;for(var d=0;db)){this.ua=b;if(!this.dc){this.dc=!0;var c=Date.now();K(this.b).delay(function(){a.j.measure(function(){a.hc(c,b)})},36)}this.aa.fire()}};f.hc=function(a,b){var c=this,d=this.ua=this.g.getScrollTop(),e=Date.now(),g=0;e!=a&&(g=(d-b)/(e-a));.03>Math.abs(g)?(vf(this,!1,g),this.dc=!1):K(this.b).delay(function(){return c.j.measure(c.hc.bind(c,e,d))},20)}; 216 | f.de=function(){var a=this;this.cc||(this.cc=!0,this.j.measure(function(){a.cc=!1;a.a.postScroll(a.getScrollTop())}))};f.Yd=function(){var a=this;this.Ha=null;var b=this.Ka;this.Ka=null;var c=this.getSize();this.i.update().then(function(){vf(a,!b||b.width!=c.width,0)})}; 217 | function wf(a,b){var c=this;this.win=a;this.cb=M(a);this.a=b;this.aa=new I;this.Ja=new I;this.win.addEventListener("scroll",function(){return c.aa.fire()});this.win.addEventListener("resize",function(){return c.Ja.fire()});this.win.document.defaultView&&La(this.win.document,function(){c.win.document.body.style.overflow="visible";Tb(c.win,"amp-ios-overflow-x")&&c.cb.isIos()&&"1"===c.a.getParam("webview")&&N(c.win.document.body,{overflowX:"hidden",overflowY:"visible"})})}f=wf.prototype;f.M=function(){}; 218 | f.requiresFixedLayerTransfer=function(){return!1};f.onScroll=function(a){this.aa.add(a)};f.onResize=function(a){this.Ja.add(a)};f.updateViewerViewport=function(){};f.updatePaddingTop=function(a){this.win.document.documentElement.style.paddingTop=a+"px"};f.updateLightboxMode=function(){};f.getSize=function(){var a=this.win.innerWidth,b=this.win.innerHeight;if(a&&b)return{width:a,height:b};var c=this.win.document.documentElement;return{width:c.clientWidth,height:c.clientHeight}}; 219 | f.getScrollTop=function(){return xf(this).scrollTop||this.win.pageYOffset};f.getScrollLeft=function(){return xf(this).scrollLeft||this.win.pageXOffset};f.getScrollWidth=function(){return xf(this).scrollWidth};f.getScrollHeight=function(){return xf(this).scrollHeight};f.getLayoutRect=function(a,b,c){var d=void 0!=c?c:this.getScrollTop(),e=void 0!=b?b:this.getScrollLeft();a=a.getBoundingClientRect();return J(Math.round(a.left+e),Math.round(a.top+d),Math.round(a.width),Math.round(a.height))}; 220 | f.setScrollTop=function(a){xf(this).scrollTop=a};function xf(a){var b=a.win.document;return b.scrollingElement?b.scrollingElement:b.body&&a.cb.isWebKit()?b.body:b.documentElement} 221 | function yf(a){var b=this;this.win=a;this.$=this.T=null;this.X={x:0,y:0};this.aa=new I;this.Ja=new I;this.l=0;Wc(this.win.document).then(function(){var a=b.win.document.body;N(b.win.document.documentElement,{overflowY:"auto",webkitOverflowScrolling:"touch"});N(a,{overflowX:"hidden",overflowY:"auto",webkitOverflowScrolling:"touch",position:"absolute",top:0,left:0,right:0,bottom:0});b.T=b.win.document.createElement("div");b.T.id="-amp-scrollpos";N(b.T,{position:"absolute",top:0,left:0,width:0,height:0, 222 | visibility:"hidden"});a.appendChild(b.T);b.$=b.win.document.createElement("div");b.$.id="-amp-scrollmove";N(b.$,{position:"absolute",top:0,left:0,width:0,height:0,visibility:"hidden"});a.appendChild(b.$);b.za=b.win.document.createElement("div");b.za.id="-amp-endpos";N(b.za,{width:0,height:0,visibility:"hidden"});a.appendChild(b.za);a.addEventListener("scroll",b.Ld.bind(b));nf(b.win)});this.win.addEventListener("resize",function(){return b.Ja.fire()})}f=yf.prototype;f.requiresFixedLayerTransfer=function(){return!0}; 223 | f.updateViewerViewport=function(){};f.updatePaddingTop=function(a){var b=this;Vc(this.win.document,function(c){b.l=a;c.body.style.borderTop=a+"px solid transparent"})};f.updateLightboxMode=function(a){Vc(this.win.document,function(b){b.body.style.borderStyle=a?"none":"solid"})};f.M=function(){};f.onScroll=function(a){this.aa.add(a)};f.onResize=function(a){this.Ja.add(a)};f.getSize=function(){return{width:this.win.innerWidth,height:this.win.innerHeight}};f.getScrollTop=function(){return Math.round(this.X.y)}; 224 | f.getScrollLeft=function(){return Math.round(this.X.x)};f.getScrollWidth=function(){return this.win.innerWidth};f.getScrollHeight=function(){return this.za?Math.round(this.za.getBoundingClientRect().top-this.T.getBoundingClientRect().top):0};f.getLayoutRect=function(a){a=a.getBoundingClientRect();return J(Math.round(a.left+this.X.x),Math.round(a.top+this.X.y),Math.round(a.width),Math.round(a.height))};f.setScrollTop=function(a){zf(this,a||1)}; 225 | f.Ld=function(a){this.T&&(this.T&&this.$&&0==-this.T.getBoundingClientRect().top+this.l&&(zf(this,1),a&&a.preventDefault()),a=this.T.getBoundingClientRect(),this.X.x!=-a.left||this.X.y!=-a.top)&&(this.X.x=-a.left,this.X.y=-a.top+this.l,this.aa.fire())};function zf(a,b){a.$&&(wc(a.$,"transform","translateY("+(b-a.l)+"px)"),a.$.scrollIntoView(!0))}function Af(a){return Ab(a,"viewport",function(){var b=ve(a),c;c="natural-ios-embed"==b.getViewportType()?new yf(a):new wf(a,b);return new sf(a,c,b)})};function Bf(a){var b=this;this.win=a;this.a=ve(a);this.N=this.a.isRuntimeOn();this.Lc=this.win.devicePixelRatio||1;this.ae=0;this.resources_=[];this.da=this.a.isVisible();this.Y=this.a.getPrerenderSize();this.Ua=!1;this.Dc=!0;this.V=-1;this.ta=!0;this.Ia=-1;this.Qb=this.Xa=0;this.ra=new pd(this.win,function(){return Cf(b)});this.R=new af;this.A=new af;this.qc=function(a){var d=0;if(!a.resource.isFixed())var d=b.m.getRect(),e=a.resource.getLayoutBox(),d=Math.floor((e.top-d.top)/d.height);Math.sign(d)!= 226 | b.getScrollDirection()&&(d*=2);d=Math.abs(d);return 10*a.priority+d};this.J=[];this.ma=[];this.S=[];this.Ib=!1;this.m=Af(this.win);this.j=rf(this.win);this.jc=new Xe(this.win,6E4);this.ic=!1;this.kd=new Ke(this.a.getVisibilityState());Df(this,this.kd);this.m.onChanged(function(a){b.Xa=Date.now();b.Qb=a.velocity;b.ta=b.ta||a.relayoutAll;b.schedulePass()});this.m.onScroll(function(){b.Xa=Date.now()});this.a.onVisibilityChanged(function(){-1==b.V&&b.a.isVisible()&&(b.V=Date.now());b.schedulePass()}); 227 | this.a.onRuntimeState(function(a){b.N=a;b.schedulePass(1)});this.jc.onFocus(function(a){Ef(b,a)});Vc(this.win.document,function(){b.Ua=!0;Ff(b);b.S=null;var a=bf(b.win);a?a.then(function(){b.ta=!0;b.schedulePass()}):b.ta=!0;b.schedulePass();Gf(b)});this.schedulePass()}f=Bf.prototype;f.get=function(){return this.resources_.slice(0)};f.isRuntimeOn=function(){return this.N}; 228 | f.getResourcesInViewport=function(a){a=a||!1;var b=this.m.getRect();return this.resources_.filter(function(c){return c.hasOwner()||!c.isDisplayed()||!c.overlaps(b)||a&&!c.prerenderAllowed()?!1:!0})};function Gf(a){var b=D(a.win,"input",df);b.onTouchDetected(function(b){Hf(a,"amp-mode-touch",b)},!0);b.onMouseDetected(function(b){Hf(a,"amp-mode-mouse",b)},!0);b.onKeyboardStateChanged(function(b){Hf(a,"amp-mode-keyboard-active",b)},!0)} 229 | function Hf(a,b,c){La(a.win.document,function(){a.j.mutate(function(){a.win.document.body.classList.toggle(b,c)})})}f.getMaxDpr=function(){return this.Lc};f.getDpr=function(){return this.Lc};f.getResourceForElement=function(a){return W(a)};f.getViewport=function(){return this.m};f.getScrollDirection=function(){return Math.sign(this.Qb)||1};f.add=function(a){var b=new Ze(++this.ae,a,this);a.id||(a.id="AMP_"+b.getId());this.resources_.push(b);If(this,b)}; 230 | function If(a,b,c){c=void 0===c?!1:c;a.N&&(a.Ua?(b.build(),a.schedulePass()):b.element.isBuilt()||c&&-1!=a.S.indexOf(b)||(a.S.push(b),Ff(a)))}function Ff(a){if(!a.Ib)try{a.Ib=!0;for(var b=0,c=0;cMath.abs(a.Qb)&&500=c.bottom- 237 | g)y=!0;else if(v.bottom<=c.top+e){k?q.push(p):a.J.push(p);continue}else H.bottom>=h||v.bottom>=h?y=!0:0>G||p.resource.overflowCallback(!0,p.newHeight,p.newWidth);y&&(0<=v.top&&(n=-1==n?v.top:Math.min(n,v.top)),p.resource.changeSize(p.newHeight,p.newWidth),p.resource.overflowCallback(!1,p.newHeight,p.newWidth));p.callback&&p.callback(y)}-1!=n&&Pf(a,n);0b.scrollHeight&&a.m.setScrollTop(b.scrollTop+(d-b.scrollHeight))}},{})}}function Pf(a,b){a.Ia=-1==a.Ia?b:Math.min(b,a.Ia)}function Ef(a,b){var c=Oa(b,function(a){return!!W(a)});if(c){b=W(c);var d=b.getPendingChangeSize();void 0!==d&&Of(a,b,d.height,d.width,!0)}} 239 | function Rf(a){var b=Date.now(),c=a.ta;a.ta=!1;var d=a.Ia;a.Ia=-1;for(var e=0,g=0,h=0;h=d)){var m=k.isDisplayed();k.measure();m&&!k.isDisplayed()&&(l||(l= 240 | []),l.push(k))}l&&a.j.mutate(function(){l.forEach(function(b){b.unload();Jf(a,b)})});var c=a.m.getRect(),n;n=a.da?ec(c,.25,2):0a.R.getLastDequeueTime()+5E3)for(var u=0,b=0;be?oc(a.b).then(function(){e=void 0===c?d[b]:d[c]-d[b];return isNaN(e)||Infinity==e||0>e?void 0:String(e)}):Promise.resolve(String(e))}function Yf(a,b){var c=a.b.performance&&a.b.performance.navigation;return c&&void 0!==c[b]?String(c[b]):Promise.resolve()} 263 | function Y(a,b,c){b.indexOf("RETURN");a.gb[b]=c;a.fb=void 0}Wf.prototype.expand=function(a,b){return Zf(this,a,b)}; 264 | function Zf(a,b,c,d){function e(a){null==a&&(a="");return encodeURIComponent(a)}a.Ic||a.Hb();var g=$f(a,c),h;b=b.replace(g,function(g,l,m){var n=[];"string"==typeof m&&(n=m.split(","));l=c&&l in c?c[l]:a.gb[l];var q;try{q="function"==typeof l?l.apply(null,n):l}catch(u){ma(u)}if(q&&q.then){var p=q.catch(function(a){ma(a)}).then(function(a){b=b.replace(g,e(a));d&&(d[g]=a)});h=h?h.then(function(){return p}):p;return g}d&&(d[g]=q);return e(q)});h&&(h=h.then(function(){return b}));return h||Promise.resolve(b)} 265 | Wf.prototype.collectVars=function(a,b){var c=Object.create(null);return Zf(this,a,b,c).then(function(){return c})};function $f(a,b){var c=b?Object.keys(b):null;if(c&&0g.readyState||(100>g.status||599a} 275 | function kg(a){return new Promise(function(b,c){if(200>a.status||300<=a.status){var d=t().createError("HTTP error "+a.status);ng(a.status)&&(d.retriable=!0);"application/json"==a.headers.get("Content-Type")?a.json().then(function(a){d.responseJson=a;c(d)},function(){c(d)}):c(d)}else b(a)})}function mg(a){this.ea=a;this.status=this.ea.status;this.headers=new og(a);this.bodyUsed=!1}function pg(a){a.bodyUsed=!0;return Promise.resolve(a.ea.responseText)}mg.prototype.text=function(){return pg(this)}; 276 | mg.prototype.json=function(){return pg(this).then(JSON.parse.bind(JSON))};mg.prototype.B=function(){this.bodyUsed=!0;t().assert(this.ea.responseXML,"responseXML should exist. Make sure to return Content-Type: text/html header.");return Promise.resolve(this.ea.responseXML)};mg.prototype.arrayBuffer=function(){t().assert(this.ea.response,"arrayBuffer response should exist.");this.bodyUsed=!0;return Promise.resolve(this.ea.response)};function og(a){this.ea=a}og.prototype.get=function(a){return this.ea.getResponseHeader(a)};function qg(a){ve(a);Af(a);Fe(a);rf(a);D(a,"resources",Bf);D(a,"url-replace",Wf);D(a,"xhr",fg);D(a,"templates",Qd);Tb(a,"form-submit")&&!a.__AMP_SUBMIT&&(a.__AMP_SUBMIT=!0,a.document.documentElement.addEventListener("submit",me,!0))} 277 | function rg(a){var b=self,c={registerElement:sg,registerServiceForDoc:tg};function d(a){if("function"==typeof a)a(b.AMP);else{var c=a.n,d=a.f,e=b.AMP,n=Yd(g,c);try{g.Sa=c,d(e),n.loaded=!0,n.resolve&&n.resolve(n.extension)}catch(q){throw n.error=q,n.reject&&n.reject(q),q;}finally{g.Sa=null}}}if(!b.AMP_TAG){b.AMP_TAG=!0;var e=b.AMP||[],g=D(b,"extensions",Wd);qg(b);b.AMP={win:b};b.AMP.config=x;b.AMP.BaseElement=P;b.AMP.BaseTemplate=Pd;b.AMP.registerElement=c.registerElement.bind(null,b,g);b.AMP.registerTemplate= 278 | function(a,c){var d=D(b,"templates",Qd);if(d.Na[a]){var e=d.gc[a];t().assert(e,"Duplicate template type: %s",a);delete d.gc[a];e(c)}else d.Na[a]=Promise.resolve(c)};b.AMP.registerServiceForDoc=c.registerServiceForDoc.bind(null,b,g);b.AMP.isExperimentOn=Tb.bind(null,b);b.AMP.toggleExperiment=Vb.bind(null,b);b.AMP.setTickFunction=function(){};a(b,g);b.AMP.push=function(a){La(b.document,function(){d(a)})};La(b.document,function(){for(var a=0;a*{display:none}.-amp-ghost{visibility:hidden!important}.-amp-element>[placeholder]{display:block}.-amp-element>[placeholder].amp-hidden,.-amp-element>[placeholder].hidden{visibility:hidden}.-amp-element:not(.amp-notsupported)>[fallback]{display:none}.-amp-layout-size-defined>[fallback],.-amp-layout-size-defined>[placeholder]{position:absolute!important;top:0!important;left:0!important;right:0!important;bottom:0!important;z-index:1!important}.-amp-notbuilt>[placeholder]{display:block!important}.-amp-hidden-by-media-query{display:none}.-amp-element-error{background:red!important;color:#fff!important;position:relative!important}.-amp-element-error:before{content:attr(error-message)}i-amp-scroll-container{position:absolute;top:0;left:0;right:0;bottom:0;display:block}i-amp-scroll-container.amp-active{overflow:auto}.-amp-loading-container{display:block!important;z-index:1}.-amp-notbuilt>.-amp-loading-container{display:block!important}.-amp-loading-container.amp-hidden{visibility:hidden}.-amp-loader{position:absolute;display:block;height:10px;top:50%;left:50%;-webkit-transform:translateX(-50%) translateY(-50%);transform:translateX(-50%) translateY(-50%);-webkit-transform-origin:50% 50%;transform-origin:50% 50%;white-space:nowrap}.-amp-loader.amp-active .-amp-loader-dot{-webkit-animation:a 2s infinite;animation:a 2s infinite}.-amp-loader-dot{position:relative;display:inline-block;height:10px;width:10px;margin:2px;border-radius:100%;background-color:rgba(0,0,0,.3);box-shadow:2px 2px 2px 1px rgba(0,0,0,.2);will-change:transform}.-amp-loader .-amp-loader-dot:nth-child(1){-webkit-animation-delay:0s;animation-delay:0s}.-amp-loader .-amp-loader-dot:nth-child(2){-webkit-animation-delay:.1s;animation-delay:.1s}.-amp-loader .-amp-loader-dot:nth-child(3){-webkit-animation-delay:.2s;animation-delay:.2s}@-webkit-keyframes a{0%,to{-webkit-transform:scale(.7);transform:scale(.7);background-color:rgba(0,0,0,.3)}50%{-webkit-transform:scale(.8);transform:scale(.8);background-color:rgba(0,0,0,.5)}}@keyframes a{0%,to{-webkit-transform:scale(.7);transform:scale(.7);background-color:rgba(0,0,0,.3)}50%{-webkit-transform:scale(.8);transform:scale(.8);background-color:rgba(0,0,0,.5)}}.-amp-element>[overflow]{cursor:pointer;z-index:2;visibility:hidden}.-amp-element>[overflow].amp-visible{visibility:visible}template{display:none!important}.amp-border-box,.amp-border-box *,.amp-border-box :after,.amp-border-box :before{box-sizing:border-box}amp-pixel{position:fixed!important;top:0!important;width:1px!important;height:1px!important;overflow:hidden!important;visibility:hidden}amp-ad iframe{border:0!important;margin:0!important;padding:0!important}amp-instagram{padding:48px 8px!important;background-color:#fff}amp-analytics{position:fixed!important;top:0!important;width:1px!important;height:1px!important;overflow:hidden!important;visibility:hidden}[amp-access][amp-access-hide],amp-experiment,amp-live-list>[update],amp-share-tracking,form [submit-error],form [submit-success]{display:none}\n/*# sourceURL=/css/amp.css*/", 281 | function(){try{qg(self);var a=ampdoc;Cb(a,"action",ce);Cb(a,"standard-actions",Vf);perf.coreServicesAvailable();yg();a=self;Ld(a,"amp-img",Ge);Je(a);bg(a);ug();Kd(self);a=self;"0"==E(a).getParam("p2r")&&M(a).isChrome()&&new wd(a.document,F(a));D(self,"clickhandler",yd);xg();Ec(document,!0)}catch(b){throw Ec(document),b;}finally{perf.tick("e_is"),perf.flush()}},!0,"amp-runtime")}catch(a){throw Ec(document),a;}self.console&&(console.info||console.log).call(console,"Powered by AMP \u26a1 HTML \u2013 Version 1471559432224"); 282 | self.document.documentElement.setAttribute("amp-version","1471559432224");})()}catch(e){setTimeout(function(){var s=document.body.style;s.opacity=1;s.visibility="visible";s.animation="none";s.WebkitAnimation="none;"},1000);throw e}; 283 | //# sourceMappingURL=v0.js.map 284 | --------------------------------------------------------------------------------