Prerender routes in Express server for Angular - Part I

ayyash

Ayyash

Posted on September 19, 2022

Prerender routes in Express server for Angular - Part I

There are many reasons to prerender, and many not to. Each application finds its own use case to prerender, while static site generation is all about prerendering, a huge website like stackoverflow avoids it. Angular recently added universal builder for prerendering, but in this series, I want to investigate other ways.

There are two methods I want to inspect in doing our own prerendering.

  1. Good ol' Express server
  2. 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.

Some notes about the StackBlitz project

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));
Enter fullscreen mode Exit fullscreen mode

Running 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, including app.server.module
  • Building creates client files under host/client and the SSR under host/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 Angular ngExpressEngine exported from the app.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);
  }
};
Enter fullscreen mode Exit fullscreen mode

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();
    }
});
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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});
    // ...
}
Enter fullscreen mode Exit fullscreen mode

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.

Express 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
}
Enter fullscreen mode Exit fullscreen mode

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.

Single 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.

RESOURCES

RELATED POSTS

Alternative way to localize in Angular

Loading external configurations in Angular Universal

💖 💪 🙅 🚩
ayyash
Ayyash

Posted on September 19, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related