This was a bit lengthier than I anticipated, but overall I do believe that I managed to research the proper way of handling this.
The Challenge Ahead
Frontity mission is to deliver top-notch and best in class quality for websites, running in what is considered modern browsers, the line was drawn at the presence of the Proxy
class. That means, the browsers that do not have this exposed on window can not benefit from Frontity’s functionalities at large, because of the tight relationship between how internally Frontity works.
But we do not stop there, we serve modern bundled code available as JavaScript modules or ESModules. That means that at each build time, we generate two sets of scripts that need to be sent to the browser. Depending on the browser, it will pick-up either the script or the module, to hydrate the already rendered application.
That means that navigating through links, pages or urls on browsers that do not support the Proxy
class, works seamlessly. What doesn’t work, are the framework and theme scripts. And this is the challenge in providing a fallback for older browsers.
Prerequisites and common ground
Before we dive in in the changes needed to frontity
we need to settled some grounds.
Add the needed compatibility <meta>
tag
In order for older Internet Explorer version to render the websites in compatibility mode, we need to instruct them as such. We can achieve that by adding this <meta>
tag to the template.
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
This will ensure the rendering of the server rendered content is rendered correctly, on Internet Explorer 11. This is the least painful change that we can to do in frontity
to properly render the pages.
The current runtime throws
Currently, in Internet Explorer 11, the generated scripts cannot run, since we use module by default. That means we need to find another way or a proper way of making sure Frontity can run some scripts.
Understanding the current bundling target
Frontity bundles the JavaScript in two formats for the browsers:
-
module
This one is aimed at the browsers that know how to parse the newer JavaScript syntax. This is the main targeted browser market.
-
es5
This is an acronym referring to EcmaScript5 version of JavaScript. This version is the earliest version that we’d like to support, since our state management architecture it’s built on top of Proxy
functionality.
These two bundles are served differently with the module/nomodule pattern. You can read more about it here.
What about the previously defined solutions
Most of them are not enough to get the SSR Fallback to have some sort of functionality. So, they are out of the scope of the following researched ideas – since technically they would need to be instrumented in a way to be compatible with legacy browsers.
Researched avenues
Tackling this particular issue it’s not that straightforward, since the architecture of frontity
is targeted to treat as first class citizen the modern browser or better yet the modern JavaScript syntax. With this in mind, I’ve tried to not steer far away from it and remain true to it.
A. Lowering the modern browser target
Proposal
Lower the browserlist to include ie >= 11
.
Results
This would basically mean, that the es5
format to include polyfills for the missing APIs including Proxy
. The latter is not perfect as it can not offer the same functionality, on which the state manager works, and it’ll not work seamless – since Proxy
it’s impossible to get a perfect polyfill.
For example we use the .has
trap. That it’s not implemented in any polyfill.
Verdict
The generated sources would be compiled with "use strict";
.
Since we use sourceType: "unambiguous"
for our babel options, and generate the entry point as a module, we’ll have to generate different entry points for each target only to make sure that Internet Explorer can run the scripts without throwing.
B. Create a fallback entry point
Proposal
If we can not use the client entry point, we could offer a fallback
client side entry, that will basically only call the actions defined on the theme namespace.
Results
This took way longer to figure out if it’s achievable or not. Mainly trying to come up with the least invasive method. And I believe I found it.
1. Create a fallback.ts
entry
Right now, we have a dynamically generated entry-point for the client
that based on the packages, imports the needed things and bootstraps the store and hydrates the App
. This is all good, but this is where the problem starts cause the bootstrap picks-up the whole state management based on Proxy
and its all down from here. By having a separate fallback
entry point, we can omit those packages and let the user deal as they wish with the client. This fallback is defined as a relative path to a file to be included in the compilation pipeline.
// frontity.settings.js
export default {
name: 'my-site',
fallback: './fallback.ts'
}
// ./fallback.ts
const stateElement = document.getElementById("__FRONTITY_CONNECT_STATE__");
const state = JSON.parse(stateElement.innerHTML);
// Here you can do whatever you'd like with access to state.
2. Define a on-the-fly entry point for the fallback
I imagine this would be based on a state flag or something that we can surface over from the settings. But let’s say we have the green light to generate a fallback entry point. This will be done in two places.
Firstly, based on the flag, we’ll have to create the dynamic entry point, inside webpack/entry.ts
. While there, also, push a new frontity-fallback
entry mapping, so webpack will pick-it up.
Second, we need to dynamically include it in the scripts collections of the ChunkExtractor
:
// ./server-side-rendering.ts
const extractor = new ChunkExtractor({
stats,
entrypoints: [settings.fallback && "frontity-fallback", settings.name],
});
This will make it so that we will end-up with 3 scripts in the dom:
<script async data-chunk="frontity-fallback" src="/static/frontity-fallback.es5.js"></script>
<script async data-chunk="mars-theme" src="/static/mars-theme.es5.js"></script>
<script async data-chunk="list" src="/static/list.es5.js"></script>
Verdict
I believe this is the most streamlined solution that I could come up with. Doesn’t offer the interaction between packages and client, but at least one can read the state, and fire-up some functionality for it.
Recorded a video as well:
C. Use the libraries.frontity.scripts
Proposal
For minimal functionality, one can push scripts through libraries.frontity.scripts
.
Results
These scripts are not transpiled, compiled or amended in any way. With that being sad something like this:
{
actions: {
theme: {
beforeSSR({ libraries }) {
libraries.frontity.scripts.push(`
<script>
window.gtag('pageview');
</script>
`);
}
}
}
}
Verdict
This is highly error prone. I can see one getting nowhere for wanting to reuse pieces of code, sharing functionality or simply wanting to track custom events.