提交 79e717a7 编写于 作者: S Sebastian Benz 提交者: Joe Haddad

Add AMP First example (#9109)

* Add AMP First example

The sample demonstrates how to build an AMP First Site with AMP. If
follows all the best-practices recommended by Lighthouse and will
result in a fully-qualified PWA.

Live demo of the page: https://my-next-app.sebastianbenz.now.sh/#development=1

* small improvements to AmpScript and AmpState

* remove gitignore

* Update example

* Move styles down in Layout

* Apply lint fix and tweak prop-types

* Add warning for host header usage
上级 7bb7d706
# AMP First Boilerplate ⚡
This example sets up the boilerplate for an AMP First Site. You can see a live version [here](https://my-next-app.sebastianbenz.now.sh). The boilerplate includes the following features:
- AMP Extension helpers (`amp-state`, `amp-script`, ...)
- AMP Serviceworker integration
- App manifest
- Offline page
## Getting started
To run the app in development, run the following command in the project director:
```shell
$ yarn dev
```
Open [http://localhost:3000](http://localhost:3000) to view it in the browser. The page will reload if you make edits. You will also see AMP validation errors in the console.
## Todo
Things you need to do after installing the boilerplate:
1. Update your app manifest in [`public/manifest.json`](public/manifest.json) with your app-specific data (`theme_color`, `background_color`, `name`, `short_name`).
2. Update the `THEME_COLOR` variable defined in [`components/Layout.js`](components/Layout.js).
3. Update favicon and icons in [`public/favicon.ico`](public/favicon.ico) and [`public/static/images/icons-*.png`](public/static/images).
4. Set the default language in [`pages/_document.js`](pages/_document.js).
5. Review the AMP Serviceworker implementation in [`public/serviceworker.js`](public/serviceworker.js).
## Tips & Tricks
- **Using AMP Components:** you can import AMP components using `next/head`. Checkout `components/amp/AmpCustomElement` for a simple way to import AMP components. Once the component is imported, you can use it like any other HTML element.
- **amp-bind & amp-state:** it's no problem to use `amp-bind` and `amp-state` with Next.js. There are two things to be aware of:
1. The `[...]` binding syntax, e.g. `[text]="myStateVariable"`, is not supported in JSX. Use `data-amp-bind-text="myStateVariable"` instead.
2. Initializing `amp-state` via JSON string is not supported in JSX:
```html
<amp-state id="myState">
<script type="application/json">
{
"foo": "bar"
}
</script>
</amp-state>
```
Instead you need to use `dangerouslySetInnerHTML` to initialize the string. can use the [`/components/amp/AmpState.js`](components/amp/AmpState.js) component to see how it works. The same approach works for `amp-access` and `amp-consent` as well:
```html
<AmpState id="message" value={ message: 'Hello World' }/>
```
- **amp-list & amp-mustache:** mustache templates conflict with JSX and it's template literals need to be escaped. A simple approach is to escape them via back ticks: `` src={`{{imageUrl}}`} ``.
- **amp-script:** you can use [amp-script](https://amp.dev/documentation/components/amp-script/) to add custom JavaScript to your AMP pages. The boilerplate includes a helper [`components/amp/AmpScript.js`](components/amp/AmpScript.js) to simplify using amp-script. The helper also supports embedding inline scripts. Good to know: Next.js uses [AMP Optimizer](https://github.com/ampproject/amp-toolbox/tree/master/packages/optimizer) under the hood, which automatically adds the needed script hashes for [inline amp-scripts](https://amp.dev/documentation/components/amp-script/#load-javascript-from-a-local-element).
## Deployment
For production builds, you need to run (the app will be build into the `.next` folder):
```shell
$ yarn build
```
To start the application in production mode, run:
```shell
$ yarn start
```
import PropTypes from 'prop-types'
import NextHead from 'next/head'
import { AmpIncludeAmpInstallServiceworker } from './amp/AmpCustomElement'
// Your app's theme color
const THEME_COLOR = '#005af0'
/**
* A sample page layout installing the AMP Serviceworker by default.
*
* @param {Props} props
*/
const Layout = props => (
<>
<NextHead>
<title>{props.title || ''}</title>
<meta name="description" content={props.description || ''} />
<meta name="theme-color" content={THEME_COLOR} />
<link rel="icon" sizes="192x192" href="/static/images/icons-192.png" />
<link rel="apple-touch-icon" href="/static/images/icons-192.png" />
<link rel="icon" href="/static/favicon.ico" />
<link rel="manifest" href="/manifest.json" />
</NextHead>
{props.children}
<AmpIncludeAmpInstallServiceworker />
<amp-install-serviceworker
src="/serviceworker.js"
data-iframe-src="/install-sw.html"
layout="nodisplay"
/>
<style jsx global>{`
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol';
min-height: 100vh;
scroll-behavior: smooth;
text-rendering: optimizeSpeed;
line-height: 1.5;
}
`}</style>
</>
)
Layout.propTypes = {
title: PropTypes.string,
description: PropTypes.string,
}
export default Layout
/**
* @file An AMP Component import helper. This file is auto-generated using
* https://www.npmjs.com/package/@ampproject/toolbox-validator-rules.
*/
import React from 'react'
import Head from 'next/head'
export function AmpIncludeCustomElement(props) {
return (
<Head>
<script
async
custom-element={props.name}
src={
'https://cdn.ampproject.org/v0/' +
props.name +
'-' +
props.version +
'.js'
}
key={props.name}
/>
</Head>
)
}
export function AmpIncludeCustomTemplate(props) {
return (
<Head>
<script
async
custom-template={props.name}
src={
'https://cdn.ampproject.org/v0/' +
props.name +
'-' +
props.version +
'.js'
}
key={props.name}
/>
</Head>
)
}
export function AmpIncludeAmp3dGltf() {
return <AmpIncludeCustomElement name="amp-3d-gltf" version="0.1" />
}
export function AmpIncludeAmp3qPlayer() {
return <AmpIncludeCustomElement name="amp-3q-player" version="0.1" />
}
export function AmpIncludeAmpAccessLaterpay() {
return <AmpIncludeCustomElement name="amp-access-laterpay" version="0.2" />
}
export function AmpIncludeAmpAccessPoool() {
return <AmpIncludeCustomElement name="amp-access-poool" version="0.1" />
}
export function AmpIncludeAmpAccessScroll() {
return <AmpIncludeCustomElement name="amp-access-scroll" version="0.1" />
}
export function AmpIncludeAmpAccess() {
return <AmpIncludeCustomElement name="amp-access" version="0.1" />
}
export function AmpIncludeAmpAccordion() {
return <AmpIncludeCustomElement name="amp-accordion" version="0.1" />
}
export function AmpIncludeAmpActionMacro() {
return <AmpIncludeCustomElement name="amp-action-macro" version="0.1" />
}
export function AmpIncludeAmpAdCustom() {
return <AmpIncludeCustomElement name="amp-ad-custom" version="0.1" />
}
export function AmpIncludeAmpAd() {
return <AmpIncludeCustomElement name="amp-ad" version="0.1" />
}
export function AmpIncludeAmpAddthis() {
return <AmpIncludeCustomElement name="amp-addthis" version="0.1" />
}
export function AmpIncludeAmpAnalytics() {
return <AmpIncludeCustomElement name="amp-analytics" version="0.1" />
}
export function AmpIncludeAmpAnim() {
return <AmpIncludeCustomElement name="amp-anim" version="0.1" />
}
export function AmpIncludeAmpAnimation() {
return <AmpIncludeCustomElement name="amp-animation" version="0.1" />
}
export function AmpIncludeAmpApesterMedia() {
return <AmpIncludeCustomElement name="amp-apester-media" version="0.1" />
}
export function AmpIncludeAmpAppBanner() {
return <AmpIncludeCustomElement name="amp-app-banner" version="0.1" />
}
export function AmpIncludeAmpAudio() {
return <AmpIncludeCustomElement name="amp-audio" version="0.1" />
}
export function AmpIncludeAmpAutoAds() {
return <AmpIncludeCustomElement name="amp-auto-ads" version="0.1" />
}
export function AmpIncludeAmpAutocomplete() {
return <AmpIncludeCustomElement name="amp-autocomplete" version="0.1" />
}
export function AmpIncludeAmpBaseCarousel() {
return <AmpIncludeCustomElement name="amp-base-carousel" version="0.1" />
}
export function AmpIncludeAmpBeopinion() {
return <AmpIncludeCustomElement name="amp-beopinion" version="0.1" />
}
export function AmpIncludeAmpBind() {
return <AmpIncludeCustomElement name="amp-bind" version="0.1" />
}
export function AmpIncludeAmpBodymovinAnimation() {
return (
<AmpIncludeCustomElement name="amp-bodymovin-animation" version="0.1" />
)
}
export function AmpIncludeAmpBridPlayer() {
return <AmpIncludeCustomElement name="amp-brid-player" version="0.1" />
}
export function AmpIncludeAmpBrightcove() {
return <AmpIncludeCustomElement name="amp-brightcove" version="0.1" />
}
export function AmpIncludeAmpBysideContent() {
return <AmpIncludeCustomElement name="amp-byside-content" version="0.1" />
}
export function AmpIncludeAmpCallTracking() {
return <AmpIncludeCustomElement name="amp-call-tracking" version="0.1" />
}
export function AmpIncludeAmpCarousel() {
return <AmpIncludeCustomElement name="amp-carousel" version="0.2" />
}
export function AmpIncludeAmpConnatixPlayer() {
return <AmpIncludeCustomElement name="amp-connatix-player" version="0.1" />
}
export function AmpIncludeAmpConsent() {
return <AmpIncludeCustomElement name="amp-consent" version="0.1" />
}
export function AmpIncludeAmpDailymotion() {
return <AmpIncludeCustomElement name="amp-dailymotion" version="0.1" />
}
export function AmpIncludeAmpDateCountdown() {
return <AmpIncludeCustomElement name="amp-date-countdown" version="0.1" />
}
export function AmpIncludeAmpDateDisplay() {
return <AmpIncludeCustomElement name="amp-date-display" version="0.1" />
}
export function AmpIncludeAmpDatePicker() {
return <AmpIncludeCustomElement name="amp-date-picker" version="0.1" />
}
export function AmpIncludeAmpDelightPlayer() {
return <AmpIncludeCustomElement name="amp-delight-player" version="0.1" />
}
export function AmpIncludeAmpDynamicCssClasses() {
return (
<AmpIncludeCustomElement name="amp-dynamic-css-classes" version="0.1" />
)
}
export function AmpIncludeAmpEmbedlyCard() {
return <AmpIncludeCustomElement name="amp-embedly-card" version="0.1" />
}
export function AmpIncludeAmpExperiment() {
return <AmpIncludeCustomElement name="amp-experiment" version="1.0" />
}
export function AmpIncludeAmpFacebookComments() {
return <AmpIncludeCustomElement name="amp-facebook-comments" version="0.1" />
}
export function AmpIncludeAmpFacebookLike() {
return <AmpIncludeCustomElement name="amp-facebook-like" version="0.1" />
}
export function AmpIncludeAmpFacebookPage() {
return <AmpIncludeCustomElement name="amp-facebook-page" version="0.1" />
}
export function AmpIncludeAmpFacebook() {
return <AmpIncludeCustomElement name="amp-facebook" version="0.1" />
}
export function AmpIncludeAmpFitText() {
return <AmpIncludeCustomElement name="amp-fit-text" version="0.1" />
}
export function AmpIncludeAmpFont() {
return <AmpIncludeCustomElement name="amp-font" version="0.1" />
}
export function AmpIncludeAmpForm() {
return <AmpIncludeCustomElement name="amp-form" version="0.1" />
}
export function AmpIncludeAmpFxCollection() {
return <AmpIncludeCustomElement name="amp-fx-collection" version="0.1" />
}
export function AmpIncludeAmpFxFlyingCarpet() {
return <AmpIncludeCustomElement name="amp-fx-flying-carpet" version="0.1" />
}
export function AmpIncludeAmpGeo() {
return <AmpIncludeCustomElement name="amp-geo" version="0.1" />
}
export function AmpIncludeAmpGfycat() {
return <AmpIncludeCustomElement name="amp-gfycat" version="0.1" />
}
export function AmpIncludeAmpGist() {
return <AmpIncludeCustomElement name="amp-gist" version="0.1" />
}
export function AmpIncludeAmpGoogleDocumentEmbed() {
return (
<AmpIncludeCustomElement name="amp-google-document-embed" version="0.1" />
)
}
export function AmpIncludeAmpHulu() {
return <AmpIncludeCustomElement name="amp-hulu" version="0.1" />
}
export function AmpIncludeAmpIframe() {
return <AmpIncludeCustomElement name="amp-iframe" version="0.1" />
}
export function AmpIncludeAmpImaVideo() {
return <AmpIncludeCustomElement name="amp-ima-video" version="0.1" />
}
export function AmpIncludeAmpImageLightbox() {
return <AmpIncludeCustomElement name="amp-image-lightbox" version="0.1" />
}
export function AmpIncludeAmpImageSlider() {
return <AmpIncludeCustomElement name="amp-image-slider" version="0.1" />
}
export function AmpIncludeAmpImgur() {
return <AmpIncludeCustomElement name="amp-imgur" version="0.1" />
}
export function AmpIncludeAmpInputmask() {
return <AmpIncludeCustomElement name="amp-inputmask" version="0.1" />
}
export function AmpIncludeAmpInstagram() {
return <AmpIncludeCustomElement name="amp-instagram" version="0.1" />
}
export function AmpIncludeAmpInstallServiceworker() {
return (
<AmpIncludeCustomElement name="amp-install-serviceworker" version="0.1" />
)
}
export function AmpIncludeAmpIzlesene() {
return <AmpIncludeCustomElement name="amp-izlesene" version="0.1" />
}
export function AmpIncludeAmpJwplayer() {
return <AmpIncludeCustomElement name="amp-jwplayer" version="0.1" />
}
export function AmpIncludeAmpKalturaPlayer() {
return <AmpIncludeCustomElement name="amp-kaltura-player" version="0.1" />
}
export function AmpIncludeAmpLightboxGallery() {
return <AmpIncludeCustomElement name="amp-lightbox-gallery" version="0.1" />
}
export function AmpIncludeAmpLightbox() {
return <AmpIncludeCustomElement name="amp-lightbox" version="0.1" />
}
export function AmpIncludeAmpLinkRewriter() {
return <AmpIncludeCustomElement name="amp-link-rewriter" version="0.1" />
}
export function AmpIncludeAmpList() {
return (
<>
<AmpIncludeAmpMustache />
<AmpIncludeCustomElement name="amp-list" version="0.1" />
</>
)
}
export function AmpIncludeAmpLiveList() {
return <AmpIncludeCustomElement name="amp-live-list" version="0.1" />
}
export function AmpIncludeAmpMathml() {
return <AmpIncludeCustomElement name="amp-mathml" version="0.1" />
}
export function AmpIncludeAmpMegaphone() {
return <AmpIncludeCustomElement name="amp-megaphone" version="0.1" />
}
export function AmpIncludeAmpMinuteMediaPlayer() {
return (
<AmpIncludeCustomElement name="amp-minute-media-player" version="0.1" />
)
}
export function AmpIncludeAmpMowplayer() {
return <AmpIncludeCustomElement name="amp-mowplayer" version="0.1" />
}
export function AmpIncludeAmpMustache() {
return <AmpIncludeCustomTemplate name="amp-mustache" version="0.2" />
}
export function AmpIncludeAmpNextPage() {
return <AmpIncludeCustomElement name="amp-next-page" version="0.1" />
}
export function AmpIncludeAmpNexxtvPlayer() {
return <AmpIncludeCustomElement name="amp-nexxtv-player" version="0.1" />
}
export function AmpIncludeAmpO2Player() {
return <AmpIncludeCustomElement name="amp-o2-player" version="0.1" />
}
export function AmpIncludeAmpOoyalaPlayer() {
return <AmpIncludeCustomElement name="amp-ooyala-player" version="0.1" />
}
export function AmpIncludeAmpOrientationObserver() {
return (
<AmpIncludeCustomElement name="amp-orientation-observer" version="0.1" />
)
}
export function AmpIncludeAmpPanZoom() {
return <AmpIncludeCustomElement name="amp-pan-zoom" version="0.1" />
}
export function AmpIncludeAmpPinterest() {
return <AmpIncludeCustomElement name="amp-pinterest" version="0.1" />
}
export function AmpIncludeAmpPlaybuzz() {
return <AmpIncludeCustomElement name="amp-playbuzz" version="0.1" />
}
export function AmpIncludeAmpPositionObserver() {
return <AmpIncludeCustomElement name="amp-position-observer" version="0.1" />
}
export function AmpIncludeAmpPowrPlayer() {
return <AmpIncludeCustomElement name="amp-powr-player" version="0.1" />
}
export function AmpIncludeAmpReachPlayer() {
return <AmpIncludeCustomElement name="amp-reach-player" version="0.1" />
}
export function AmpIncludeAmpRecaptchaInput() {
return <AmpIncludeCustomElement name="amp-recaptcha-input" version="0.1" />
}
export function AmpIncludeAmpReddit() {
return <AmpIncludeCustomElement name="amp-reddit" version="0.1" />
}
export function AmpIncludeAmpRiddleQuiz() {
return <AmpIncludeCustomElement name="amp-riddle-quiz" version="0.1" />
}
export function AmpIncludeAmpScript() {
return <AmpIncludeCustomElement name="amp-script" version="0.1" />
}
export function AmpIncludeAmpSelector() {
return <AmpIncludeCustomElement name="amp-selector" version="0.1" />
}
export function AmpIncludeAmpSidebar() {
return <AmpIncludeCustomElement name="amp-sidebar" version="0.1" />
}
export function AmpIncludeAmpSkimlinks() {
return <AmpIncludeCustomElement name="amp-skimlinks" version="0.1" />
}
export function AmpIncludeAmpSlides() {
return <AmpIncludeCustomElement name="amp-slides" version="0.1" />
}
export function AmpIncludeAmpSmartlinks() {
return <AmpIncludeCustomElement name="amp-smartlinks" version="0.1" />
}
export function AmpIncludeAmpSocialShare() {
return <AmpIncludeCustomElement name="amp-social-share" version="0.1" />
}
export function AmpIncludeAmpSoundcloud() {
return <AmpIncludeCustomElement name="amp-soundcloud" version="0.1" />
}
export function AmpIncludeAmpSpringboardPlayer() {
return <AmpIncludeCustomElement name="amp-springboard-player" version="0.1" />
}
export function AmpIncludeAmpStickyAd() {
return <AmpIncludeCustomElement name="amp-sticky-ad" version="1.0" />
}
export function AmpIncludeAmpStoryAutoAds() {
return <AmpIncludeCustomElement name="amp-story-auto-ads" version="0.1" />
}
export function AmpIncludeAmpStory() {
return <AmpIncludeCustomElement name="amp-story" version="1.0" />
}
export function AmpIncludeAmpSubscriptions() {
return <AmpIncludeCustomElement name="amp-subscriptions" version="0.1" />
}
export function AmpIncludeAmpSubscriptionsGoogle() {
return (
<AmpIncludeCustomElement name="amp-subscriptions-google" version="0.1" />
)
}
export function AmpIncludeAmpTimeago() {
return <AmpIncludeCustomElement name="amp-timeago" version="0.1" />
}
export function AmpIncludeAmpTruncateText() {
return <AmpIncludeCustomElement name="amp-truncate-text" version="0.1" />
}
export function AmpIncludeAmpTwitter() {
return <AmpIncludeCustomElement name="amp-twitter" version="0.1" />
}
export function AmpIncludeAmpUserLocation() {
return <AmpIncludeCustomElement name="amp-user-location" version="0.1" />
}
export function AmpIncludeAmpUserNotification() {
return <AmpIncludeCustomElement name="amp-user-notification" version="0.1" />
}
export function AmpIncludeAmpVideoDocking() {
return <AmpIncludeCustomElement name="amp-video-docking" version="0.1" />
}
export function AmpIncludeAmpVideoIframe() {
return <AmpIncludeCustomElement name="amp-video-iframe" version="0.1" />
}
export function AmpIncludeAmpVideo() {
return <AmpIncludeCustomElement name="amp-video" version="0.1" />
}
export function AmpIncludeAmpVimeo() {
return <AmpIncludeCustomElement name="amp-vimeo" version="0.1" />
}
export function AmpIncludeAmpVine() {
return <AmpIncludeCustomElement name="amp-vine" version="0.1" />
}
export function AmpIncludeAmpViqeoPlayer() {
return <AmpIncludeCustomElement name="amp-viqeo-player" version="0.1" />
}
export function AmpIncludeAmpVk() {
return <AmpIncludeCustomElement name="amp-vk" version="0.1" />
}
export function AmpIncludeAmpWebPush() {
return <AmpIncludeCustomElement name="amp-web-push" version="0.1" />
}
export function AmpIncludeAmpWistiaPlayer() {
return <AmpIncludeCustomElement name="amp-wistia-player" version="0.1" />
}
export function AmpIncludeAmpYotpo() {
return <AmpIncludeCustomElement name="amp-yotpo" version="0.1" />
}
export function AmpIncludeAmpYoutube() {
return <AmpIncludeCustomElement name="amp-youtube" version="0.1" />
}
import PropTypes from 'prop-types'
import React from 'react'
import { AmpIncludeAmpScript } from './AmpCustomElement'
/**
* Embeds an AMP Script by either linking to a JS `src` file or embedding inline
* AMP Script via the `script` property. The inline script hash will automatically
* be generated by AMP Optimizer.
*
* @param {Props} props
*/
export default function AmpScript(props) {
return (
<>
<AmpIncludeAmpScript />
<amp-script
layout={props.layout}
width={props.width}
height={props.height}
script={props.id}
src={props.src}
>
{props.children}
</amp-script>
{props.script && (
<script
id={props.id}
type="text/plain"
target="amp-script"
dangerouslySetInnerHTML={{
__html: generateInlineScript(props.script),
}}
/>
)}
</>
)
}
function generateInlineScript(script) {
if (typeof script === 'function') {
return `${script.toString()}()`
}
return String(script)
}
AmpScript.propTypes = {
id: PropTypes.string,
children: PropTypes.node.isRequired,
layout: PropTypes.string.isRequired,
width: PropTypes.string,
height: PropTypes.string,
script: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
src: PropTypes.string,
}
import PropTypes from 'prop-types'
import React from 'react'
import { AmpIncludeAmpBind } from './AmpCustomElement'
/**
* Renders an amp-state element, by either adding local state via `value`
* or remote state via the `src` property.
*
* @param {Props} props
*/
export default function AmpState(props) {
return (
<>
<AmpIncludeAmpBind />
<amp-state id={props.id} src={props.src}>
{props.children && (
<script
type="application/json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(props.children),
}}
/>
)}
</amp-state>
</>
)
}
AmpState.propTypes = {
id: PropTypes.string.isRequired,
children: PropTypes.any,
src: PropTypes.string,
}
{
"name": "amp-first",
"version": "1.0.0",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "latest",
"react": "^16.10.2",
"react-dom": "^16.10.2"
},
"license": "ISC"
}
// _document is only rendered on the server side and not on the client side
// Event handlers like onClick can't be added to this file
import Document, { Html, Head, Main, NextScript } from 'next/document'
class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx)
return { ...initialProps }
}
render() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument
import React from 'react'
import Layout from '../components/Layout'
import AmpState from '../components/amp/AmpState'
import AmpScript from '../components/amp/AmpScript'
import {
AmpIncludeAmpList,
AmpIncludeAmpCarousel,
} from '../components/amp/AmpCustomElement'
export const config = { amp: true }
const Home = props => (
<>
<Layout
title="Welcome to AMP"
description="Learn how to build an AMP First with Next.js."
>
<main>
<h1 className="title">Welcome to AMP </h1>
<p className="description">
To get started, edit <code>pages/index.js</code> and save to reload.
</p>
<section className="hero">
<a href="https://nextjs.org/learn/basics/getting-started">
<h3>Getting Started &rarr;</h3>
<p>Learn more about Next</p>
</a>
<a href="https://nextjs.org/docs#amp-support">
<h3>AMP Support in Next.js &rarr;</h3>
<p>Learn how to build AMP sites with Next.js</p>
</a>
<a href="https://amp.dev/documentation/components/?format=websites">
<h3>AMP Components &rarr;</h3>
<p>See which components are available.</p>
</a>
</section>
<section>
<h3>Using AMP Components</h3>
<p>
You can import AMP components using <code>next/head</code>. Checkout{' '}
<code>components/amp/AmpCustomElement</code> for a simple way to
import AMP components. Once the component is imported, you can use
it like any other HTML element.
</p>
<AmpIncludeAmpCarousel />
<amp-carousel
type="slides"
width="800"
height="300"
layout="responsive"
>
<amp-img
src="https://unsplash.it/800/300?id=123"
layout="fill"
alt="demo image"
/>
<amp-img
src="https://unsplash.it/800/300?id=124"
layout="fill"
alt="demo image"
/>
<amp-img
src="https://unsplash.it/800/300?id=125"
layout="fill"
alt="demo image"
/>
</amp-carousel>
</section>
<section>
<h3>amp-bind & amp-state</h3>
<p>
It's no problem to use <code>amp-bind</code> and{' '}
<code>amp-state</code> with Next.js. There are two things to be
aware of:
<ol>
<li>
The <code>[...]</code> binding syntax{' '}
<code>[text]="myStateVariable"</code>is not supported in JSX.
Use <code>data-amp-bind-text="myStateVariable"</code> instead.
</li>
<li>
Initializing <code>amp-state</code> via JSON string is not
supported in JSX:
<pre>{`<amp-state id="myState">
<script type="application/json">
{
"message": "Hello World"
}
</script>
</amp-state>`}</pre>
Instead you need to use <code>dangerouslySetInnerHTML</code> to
initialize the string. can use the{' '}
<code>/components/amp/AmpState.js</code> component to see how it
works. The same approach works for <code>amp-access</code> and{' '}
<code>amp-consent</code> as well
</li>
</ol>
Demo:
</p>
<AmpState id="myState">
{{
message: 'Hello World',
}}
</AmpState>
<button
on="tap:AMP.setState({
greeting: myState.message
})"
>
Click
</button>
<span data-amp-bind-text="greeting" />
</section>
<section>
<h3>amp-list & amp-mustache</h3>
<p>
Mustache templates conflict with JSX and it's template literals need
to be escaped. A simple approach is to escape them via back ticks:{' '}
<code>src=&#123;`&#123;&#123;imageUrl&#125;&#125;`&#125;</code>.
</p>
<AmpIncludeAmpList />
<amp-list
src="https://amp.dev/documentation/examples/api/photo-stream"
layout="fixed-height"
height="64"
binding="no"
>
<template type="amp-mustache">
<amp-img
src={`{{imageUrl}}`}
width="64"
height="64"
alt="demo image"
/>
</template>
</amp-list>
</section>
<section>
<h3>amp-script</h3>
<p>
Checkout the{' '}
<a href="https://amp.dev/documentation/components/amp-script/">
amp-script
</a>{' '}
helper here: <code>components/amp/AmpScript.js</code> for embedding
custom JavaScript.
</p>
<AmpScript
layout="container"
src={`${props.host}/static/amp-script/hello.js`}
>
<button>Hello amp-script!</button>
</AmpScript>
<p>
The helper also supports embedding inline scripts. Good to know:
Next.js uses{' '}
<a href="https://github.com/ampproject/amp-toolbox/tree/master/packages/optimizer">
AMP Optimizer
</a>{' '}
under the hood, which automatically adds the needed script hashes
for{' '}
<a href="https://amp.dev/documentation/components/amp-script/#load-javascript-from-a-local-element">
inline amp-scripts
</a>
.
</p>
<AmpScript
id="hello-world"
layout="fixed-height"
height="64"
script={() => {
const btn = document.querySelector('button')
btn.addEventListener('click', () => {
document.body.textContent = 'Hello World!'
})
}}
>
<button>Hello amp-script!</button>
</AmpScript>
</section>
</main>
</Layout>
<style jsx>{`
code,
pre {
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo,
Courier, monospace;
background: #f2f2f2;
padding: 2px 3px;
font-size: 13px;
}
main {
margin: 0 auto;
max-width: 800px;
}
main > * + * {
margin: 4rem 0.5rem;
}
.title {
text-align: center;
padding-top: 4rem;
}
.hero {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
grid-gap: 1rem;
}
.hero > a {
display: block;
padding: 1rem;
text-align: left;
text-decoration: none;
background-color: #005af0;
}
.hero h3 {
margin: 0;
color: #067df7;
color: #fff;
}
.hero p {
margin: 0;
color: #fff;
}
`}</style>
</>
)
// amp-script requires absolute URLs, so we create a property `host` which we can use to calculate the script URL.
Home.getInitialProps = async ({ req }) => {
// WARNING: This is a generally unsafe application unless you're deploying to a managed platform like ZEIT Now.
// Be sure your load balancer is configured to not allow spoofed host headers.
return { host: `${getProtocol(req)}://${req.headers.host}` }
}
function getProtocol(req) {
if (req.connection.encrypted) {
return 'https'
}
const forwardedProto = req.headers['x-forwarded-proto']
if (forwardedProto) {
return forwardedProto.split(/\s*,\s*/)[0]
}
return 'http'
}
export default Home
import React from 'react'
import Layout from '../components/Layout'
export const config = { amp: true }
const Home = () => (
<Layout title="Offline" description="No internet connection.">
<h1>Offline</h1>
<p>Please try again later.</p>
</Layout>
)
export default Home
<!DOCTYPE html>
<title>installing service worker</title>
<script type="text/javascript">
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
}
</script>
{
"short_name": "My page",
"name": "My fantastic page",
"icons": [
{
"src": "/static/images/icons-192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "/static/images/icons-512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"display": "standalone",
"scope": "/",
"theme_color": "#005af0",
"background_color": "#ffffff"
}
/* global importScripts, AMP_SW */
importScripts('https://cdn.ampproject.org/sw/amp-sw.js')
/*
This configures the AMP service worker to enhance network resiliency and
optimizes asset caching. This configuration will:
- Cache AMP scripts with a stale-while-revalidate strategy for a longer duration
than the default http response headers indicate.
- Cache valid visited AMP documents, and serve only in case of flaky network conditions.
- Cache and serve an offline page.
- Serve static assets with a cache first strategy.
Checkout https://github.com/ampproject/amp-sw/ to learn more about how to configure
asset caching and link prefetching.
*/
AMP_SW.init({
assetCachingOptions: [
{
regexp: /\.(png|jpg|woff2|woff|css|js)/,
cachingStrategy: 'CACHE_FIRST', // options are NETWORK_FIRST | CACHE_FIRST | STALE_WHILE_REVALIDATE
},
],
offlinePageOptions: {
url: '/offline',
assets: [],
},
})
const btn = document.querySelector('button')
btn.addEventListener('click', () => {
document.body.textContent = 'Hello World!'
})
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册