There are two methods I want to inspect in doing our own prerendering.
- Good ol’ Express server
- Spin-off of Angular prerender builder
There is also using Angular RenderModule
on the server, but this is redundant.
Today we are going to build our Express server to prerender pages, and next week we will cover the different aspects of creating a single build multilingual Angular app, which we spoke of previously.
I created a simple StackBlitz project to build an SSR application, but unfortunately it failed at creating the index.html
in client. If you run npm run build:ssr
it might get stuck on index.html
. Cancel the step, continue, and patch the index yourself. I did patch the file in StackBlitz, but that meant that generating a prerendered index file for the root did not produce the right results. Whatever, StackBlitz!
This project shows a simple Express server, which we talked about previously in Isolating the server.
The Node version provided does not support fetch
, that’s why we use node-fetch
library, which is not commonjs
, so the solution (as per the documentation) is to import it like this:
const fetch = (...args) => import('node-fetch')
.then(({ default: fetch }) => fetch(...args));
anchorRunning a local Express server
The easiest and most straightforward way is to set up a local Express server and to use a simple fetch
in Node. fetch
is available from Node version 17, until then, you can use node-fetch
library.
The current setup is as follows:
src
folder has the Angular modules, includingapp.server.module
- Building creates client files under
host/client
and the SSR underhost/server/main.js
host/server.js
has an isolated Express server that runs Angular on a local port.host/server/routes.js
has the routes that import AngularngExpressEngine
exported from theapp.server.module
- Our new fetch file is under
host/prerender/fetch.js
So first let’s create the prerendering fetch module:
// host/prerender/fetch.js file
async function renderToHtml(route, port) {
// run url in localhost
// do something with returned text
// return
}
// export some function here:
module.exports = async (port) => {
// generate /client/static/route/index.html
// my static routes, example routes
const routes = ['', 'projects', 'projects/1'];
for (const route of routes) {
await renderToHtml(route, port);
}
};
We adapt our server to do something in case an environment variable is set, like this:
// host/server.js
// ...
// just when you start listening:
const port = process.env.PORT || 1212;
// assign a server to be able to close later
// turn function to async to allow an await statement
const server = app.listen(port, async function (err) {
console.log('started to listen to port: ' + port);
if (err) {
console.log(err);
return;
}
// if process.env.PRERENDER, then run this and close
if (process.env.PRERENDER) {
const prerender = require('./prerender/fetch');
// await fetch before you close here
// pass the port to reuse it
await prerender(port);
server.close();
}
});
To run in prerender mode, we create a quick npm
script, in the root of host folder.
"prerender": "SET PRERENDER=true && node server"
Or in other than Windows (like StackBlitz, find the script in root packages, with cd host
first)
"prerender": "PRERENDER=true node server”
The function renderToHtml
, should do the following:
- fetch the route in SSR environment
- save the output string into a
index.html
file - place the file in a path matching the route
- save it in a location easy to find, not only for Express but also for Firebase, Netlify, and Surge. So the destination should be inside
client
folder (the public folder of cloud hosts).
For that, we shall be using Node’s fs/promises
bundle, this allows us to close the port when done. I choose for now to place them in /client/static
folder. In Express, it’s easy to manage that. In other hosts, like Netlify, it’s easier to just place them on root /client
.
// host/prerender/fetch.js
const fs = require('fs/promises');
// this should be part of a config passed down from server listener
// client/static for Express, or simply client for cloud hosts
const prerenderOut = './client/static/';
async function renderToHtml(route, port, outFolder) {
// fetch it
const response = await fetch(`http://localhost:${port}/${route}`);
if (response.ok) {
const text = await response.text();
// the output folder is ./client/static/{route}, relative to root server file
const d = outFolder + route;
// mkdir recursive, creates the folder structure
await fs.mkdir(d, {recursive: true});
// create index.html, and write text to it.
await fs.writeFile(d + '/index.html', text);
// loggin success
console.log('ok', route, text.length);
} else {
// log errores
console.log('not ok', route, response.status);
}
}
module.exports = async (port) => {
// generate /client/static/route/index.html
// my static routes, example routes (you could run an API call to get all paths)
const routes = ['', 'projects', 'projects/1'];
for (const route of routes) {
await renderToHtml(route, port, prerenderOut);
}
}
One thing to enhance is remove the static folder before we begin creating it, to make sure we get a fresh copy every time we build it. In cloud hosts, it would be a bit different.
// prerender/fetch.js
module.exports = async (port) => {
// ...
// remove static folder first
await fs.rm(prerenderOut, {recursive: true, force: true});
// ...
}
Run the script, and watch the files be created. Note: the cool thing about Angular universal packages, is that it will also create inline critical CSS for every path.
anchorExpress routes
To serve those static files in Express, a new static adapter is created, exposing the contents of the /client/static
to the root, so in routes.js
file (which contains the necessary routes to server Angular SSR in Express):
// host/server/routes.js
// this should be part of a config file passed down from server listener
const rootPath = path.normalize(__dirname + '/../');
module.exports = function (app) {
// expose static folder
app.use('/', express.static(rootPath + 'client/static'));
// ... other routes
}
To test this, first we move into the host folder, and run node server
. Then browse to localhost:1212
, and to differentiate static files from Angular served files, we do the following:
- Change the title of the static files to something we can recognize, like “Static … “
- Disable JavaScript in the browse
Now if we see the title changed, then the static page is being served. Of course, JavaScript will hydrate when enabled, and Angular client-side will take over.
anchorSingle multilingual build
Sinking further in the sin of Twisting Angular Localization, let’s create static files for different languages, in the same build, to do that, take a break, and come back next week. 😴
Thank you for reading this short intro, let me know if you started seeing doubles.