This is the implementation proposal for the AMP package.
Description
The AMP package will be a Frontity package that will allow the creation of Google AMP pages with the Frontity framework. We want to do this with a package, @frontity/amp
, instead of hardcoding it in the framework.
Thanks to the multisite feature of Frontity, themes can be reused for both AMP pages and React sites.
Glossary
-
AMP-aware
Some function/component/package smart enough to check for itself if the site is an AMP site or not, and adapt to the situation.
Context / Background
We already had AMP support in the old version of the framework and it worked great. About 90% of the theme code was reused among the React site and the AMP pages, including all the layout and CSS. AMP support was hardcoded in the framework core.
Goals
As a Frontity developer
I want to use the same React codebase to create the AMP version of my site
so that I don’t have to maintain two separate codebases.
Out of Scope
This is out of the scope of this Feature Discussion but will be acomplished in a future FD.
As a Frontity developer
I want to use AMP content translated with the WP AMP plugin
so that I don’t have to use Html2React for that
Existing Solution / Workaround
There’s no way to use AMP right now with Frontity in Decoupled mode.
In Embedded mode, people could exclude the URLs that end in /amp
and use the AMP plugin to do AMP. But, of course, the theme of that AMP version won’t be anything similar to the Frontity theme.
Implementation Proposal
To be able to generate valid AMP HTML, this package needs to achieve several tasks.
Change the entry point of the packages
Some packages may require a different entry point for AMP because sometimes they are going to contain code that is not needed in the React version and we need to provide tools to make the bundle size of the React version as slim as possible.
This means that instead of using src/index.js
, packages can use src/amp.js
or src/amp/index.js
as entry points when the site is an AMP site. This is of course optional, and the packages that don’t need it or don’t implement this can still use src/index.js
.
It will be managed by a new site setting on the frontity.settings.js
file:
export default {
name: "my-amp-site",
entryPoints: "amp",
state: {
//...
},
packages: [
//...
],
};
This feature requires a new FD. I will edit this IP as soon as it is created.
Don’t generate client bundles
We don’t need to generate bundles for the client because the Google AMP framework forbids the use of external JavaScript.
It will be managed by a new site setting on the frontity.settings.js
file:
export default {
name: "my-amp-site",
bundles: ["server"],
state: {
//...
},
packages: [
//...
],
};
This feature requires a new FD. I will edit this IP as soon as it is created.
Modify site settings with packages
The @frontity/amp
package needs to be able to change the entryPoints
and bundles
settings.
This will be managed by a settings
export in the frontity.config.js
file of this package.
// packages/amp/frontity.config.js
export const settings = ({ settings }) => {
settings.entryPoints = "amp";
settings.bundles = ["server"];
};
The settings
export of the frontity.config.js
file of each package will be executed right after getting the settings from the frontity.settings.js
file.
This feature requires a new FD. I will edit this IP as soon as it is created.
Package priorities
The @frontity/amp
package needs to be executed before any other package because it is going to change the entryPoints
setting and that will affect how the rest of the packages are imported.
This will be managed by a priority
export in the frontity.config.js
file of this package.
// packages/amp/frontity.config.js
export const priority = 2;
Users can override this setting in the frontity.settings.js
file:
export default {
name: "my-amp-site",
packages: [
{
name: "some-package",
priority: 5, // Override default priority for this package.
state: {
// ...
},
},
],
};
This feature requires a new FD. I will edit this IP as soon as it is created.
Overwrite the HTML template
The Google AMP framework needs a specific HTML template because a normal HTML document doesn’t validate.
For that reason, the @frontity/amp
package needs to be able to overwrite the hardcoded HTML template.
This can be done with a beforeSSR
function but needs the template
to be exposed in libraries.frontity
.
// packages/amp/src/index.js
import ampTemplate from "./templates/amp";
export default {
actions: {
amp: {
beforeSSR: ({ libraries }) => {
libraries.frontity.template = ampTemplate;
},
},
},
};
The template should be something like this: https://github.com/frontity/frontity/blob/31d34e4d16042ebc73b4c30a9f53dfae36c633a2/packages/core/src/server/templates/amp.ts
Render to Static Markup
The Google AMP framework doesn’t need the custom data-
attributes added by React, so instead of using renderToString
, the @frontity/amp
package needs to make Frontity use renderToStaticMarkup
.
This can be done with a beforeSSR
function. It’ll be necessary to create a separate server.js
file because we don’t want to add react-dom/server
to the client bundle.
// packages/amp/src/server.js
import { renderToStaticMarkup } from "react-dom/server";
export default {
actions: {
amp: {
beforeSSR: ({ libraries }) => {
libraries.frontity.render = renderToStaticMarkup;
},
},
},
};
It needs the render
function to be exposed in libraries.frontity
.
Move CSS to the <head>
The Google AMP framework requires that all the CSS is placed in a <style>
tag of the head.
For that reason, the @frontity/amp
package needs to use Emotion’s extractCritical
API and move the extracted CSS to the <head>
.
This can be done with a beforeSSR
function. It’ll be necessary to create a separate server.js
file because we don’t want to add create-emotion-server
and react-dom/server
to the client bundle.
// packages/amp/src/server.js
import { CacheProvider } from "@emotion/core";
import createEmotionServer from "create-emotion-server";
import createCache from "@emotion/cache";
import { renderToStaticMarkup } from "react-dom/server";
import ampTemplate from "./templates/amp";
export default {
actions: {
amp: {
beforeSSR: ({ libraries }) => {
// Create an Emotion instance for this SSR.
const cache = createCache();
const { extractCritical } = createEmotionServer(cache);
libraries.frontity.render = (App) => {
// Add the Provider to the App.
App = (
<CacheProvider value={cache}>
<App />
</CacheProvider>
);
// Run extract critical.
const { html, css, ids } = extractCritical(
libraries.frontity.render(App)
);
// Replace the template injecting the CSS in the `<head>`.
libraries.frontity.template = ampTemplate(css, ids);
return html;
};
},
},
},
};
We also need to be able to add the CacheProvider
to the client. That can be done with a beforeCSR
function.
// packages/amp/src/client.js
import createCache from "@emotion/cache";
export default {
actions: {
amp: {
beforeCSR: ({ libraries }) => {
const cache = createCache();
libraries.frontity.App = () => (
<CacheProvider value={cache}>
<libraries.frontity.App />
</CacheProvider>
);
},
},
},
};
For context, this is the way Emotion worked in Frontity at first. This is the final commit, including a fix for the Global
component: https://github.com/frontity/frontity/commit/e8c3430
After that, we removed extractCritical
because both labels and source maps don’t work with that approach. Now it’s up to the @frontity/amp
package add extractCritical
again for AMP sites. This is where I removed that approach: https://github.com/frontity/frontity/commit/d5af653
Looking at the new docs it seems to me that we don’t need to do a separate hydrate()
on the ids anymore, as they are present in the data-emotion-css
attribute of the style
tag:
<style data-emotion-css="${ids.join(' ')}">${css}</style>
More info about the extractCritical
approach on https://emotion.sh/docs/ssr#advanced-approach
Modify link
to point to the canonical URL
There are three standard ways to use AMP. This package needs to support all of them.
- Using a subdomain, like
https://amp.mydomain.com/some-post
.
- Using a query parameter, like
https://www.mydomain.com/some-post?amp=true
.
- Using an extra folder in the path, like
https://www.mydomain.com/some-post/amp
.
1 and 2 are fine because the slug used by @frontity/wp-source
will be some-post
, but 3 is not.
We need to make this action and derived state point to /some-post/
:
actions.source.fecth("/some-post/amp");
state.source.get("/some-post/amp");
Ideally, this should be done with hooks, but we don’t have hooks yet and they seem too much to make them a dependency of this feature right now.
We’ve been searching for a workaround but the only thing that seems feasible right now is to hardcode a replacement in the normalize function and change it once we have hooks.
export const normalize = (route: string): string => {
route = route.replace(/\/amp$/, "");
return paramsToRoute(routeToParams(route));
};
This will need to contain a bit more logic to avoid some known URLs that shouldn’t be changed, like:
- A page called amp:
/amp
.
- A category/tag called amp:
/category/amp
or /tag/amp
.
I wouldn’t try to make it perfect though, taking into account that this will be replaced by a hook in the (hopefully) near future.
AMP processors
A theme that needs to supports AMP should contain AMP-aware components, but the HTML that is inside of post.content.rendered
is outside of the scope of the theme because it’s managed by Html2React.
The package @frontity/amp
should use processors to translate the HTML tags that need changes in the AMP framework, like images, iframes, and so on.
We already did a lot of related work in the old version of the framework, where many of the components used by processors were AMP-aware, like this one: https://github.com/wp-pwa/h2r/blob/dev/src/components/LazyIframe/index.js#L62-L87
This time we don’t need to check if we are in an AMP render, as this processors will only be used in an AMP render because we know that the @frontity/amp
was the one that added them.
// packages/amp/src/index.js
import amp from "./processors";
export default {
state: {
amp: {},
},
libraries: {
processors: [...amp],
},
};
Alternatively, we will be able to use the AMP Plugin to do this in the future once this PR is finished and merged: https://github.com/ampproject/amp-wp/pull/2827
AMP-aware components
The components that we export in @frontity/components
need to be aware of an AMP render and act accordingly.
For example, the Image
component needs to return <amp-img>
instead of <img>
and so on.
const Image = ({ state, src }) => {
if (state.amp) {
return <amp-img src={src} />;
} else {
return <img src={src} />;
}
};
Make official Frontity packages AMP-aware
There are some packages that need to be modified to make them work in AMP sites. Right now only:
@frontity/comscore-analytics
@frontity/google-analytics
@frontity/google-tag-manager-analytics
But in the future other packages, like ads packages.
Test Plan / Acceptance Criteria
Apart from normal e2e tests, we need to make sure that the HTML generated is always valid AMP HTML.
As far as I know, Cypress injects jQuery in the HTML, so we cannot validate the HTML directly, but we can do a request of the HTML from Cypress and validate the HTML using https://www.npmjs.com/package/amphtml-validator. Or maybe we don’t even need Cypress for this and we can just use Jest. That is going to depend on how our e2e system works.
It’d also be cool to test against a WordPress instance loaded with this content
https://github.com/WPTT/theme-unit-test which covers a lot of content and blocks, to make sure they all validate correctly with the amphtml-validator
package.
cc: @mmczaplinski maybe you want to have this new example in mind while designing the e2e system
Dependencies
We need new Feature Discussions for:
- New setting to change the entry point of the packages of a site.
- New setting to change what bundles are generated (client and/or server).
- Allow packages to modify the settings of a site.
- Package priorities.
Individual Tasks
This is a non-exhaustive list of tasks that need to be accomplished once the dependencies have been created.
- Expose in
libraries.frontity
:
- The HTML
template
.
- The
render
function (currently renderToString
).
- The
App
.
- Create the
@frontity/amp
package.
- Add
frontity.config.js
file for:
- Setting the
entryPoints
to amp
.
- Setting the
bundles
to ["server"]
.
- Setting
priority
to 2.
- Overwrite the HTML template.
- Move all the CSS to the
<head>
.
- Change the render to be
renderToStaticMarkup
.
- Create the AMP processors to translate the post content to AMP.
- Make all the components and hooks of
@frontity/components
and @frontity/hooks
AMP-aware.
- Remove
/amp
from link
in actions.source.fetch
and state.source.get
to point to the canonical URL.
Beta version
If we want to release a beta version to test this out as soon as possible, we can avoid the release of all the dependencies and replace them with workarounds:
Setting to change the entry point of the packages of a site
No package that we know of is using the /src/amp.js
entry point so far. Also, this feature is required to optimize the React bundle size so it’s not critical.
Setting to change which bundles are generated (client and/or server)
Frontity can generate the client bundle for AMP. It won’t be used, but everything should work just fine.
Allow packages to modify the settings of a site
We don’t need this until the previous two features have been implemented.
Package priorities
We can add the package at the beginning of the packages
array in the frontity.settings.js
file and it will have the same effect.
We can also skip modifying/creating some of the processors and components and the frontity.config.js
file for the beta.
Documentation
-
I think for this feature it would be great to have a new section in our docs that explains how to use this package:
- How to create a second site in your project
- How to use
match
to use the site for some URLs, for example, those that end with /amp
.
- How to make AMP-aware components.
- How to use Html2React to add custom AMP components.
- How to use different entry points to avoid including extra KBs in the client bundles.
-
A code example of a project using two sites, one configured for React and the other for AMP.
-
Make both mars-theme
and twentytwenty-theme
AMP-aware.
Open Questions
Expose SSR/CSR tools in ctx.frontity
or libraries
I’m not 100% convinced yet on exposing the objects/functions necessary for the SSR/CSR on the Koa context. Another option could be to use libraries
for this.
For example, using the Koa context to change the template and the App
:
export default {
actions: {
amp: {
beforeSSR: () => ({ ctx }) => {
ctx.frontity.template = myOwnTemplate;
ctx.frontity.App = (props) => (
<MyProvider>
<ctx.frontity.App {...props} />
</MyProvider>
);
},
},
},
};
Or just exposing the same under libraries
. It could be libraries.frontity
or something more specific like libraries.rendering
.
export default {
actions: {
amp: {
beforeSSR: ({ libraries }) => {
libraries.frontity.template = myOwnTemplate;
libraries.frontity.App = (props) => (
<MyProvider>
<libraries.frontity.App {...props} />
</MyProvider>
);
},
},
},
};
Same for beforeCSR
, where having a “Koa context” feels more weird.
EDIT: I have decided to go with libraries.frontity
. I have updated the implementation proposal to reflect this decision.
Related Feature Discussions
These Feature Discussions also make use of the new frontity.config.js
file.
References