Prerender routes in Express server for Angular - Part I
Tags: angular, javascript, webdev, tutorial
Published: 2022-09-19 19:49:03

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](https://garage.sekrab.com/posts/alternative-way-to-localize-in-angular), which we spoke of previously.

#### Some notes about the StackBlitz project

> I created a simple [StackBlitz project](https://stackblitz.com/edit/angular-express-prerender) 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](https://garage.sekrab.com/posts/loading-external-configurations-in-angular-universal) 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:

```js
const fetch = (...args) => import('node-fetch')
	.then(({ default: fetch }) => fetch(...args));
```

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:

```js
// 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:

```js
// 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`.

```js
// 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.

```js
// 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.

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):

```js
// 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.

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

-   [Angular - Prerendering](https://angular.io/guide/prerendering)
-   [StackBlitz project](https://stackblitz.com/edit/angular-express-prerender)
-   [Express API](https://expressjs.com/en/4x/api.html)
-   [NodeJs FileSystem](https://nodejs.org/api/fs.html)
-   [MDN Fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch)
-   [Node Fetch library](https://www.npmjs.com/package/node-fetch)

### RELATED POSTS

[Alternative way to localize in Angular](https://garage.sekrab.com/posts/alternative-way-to-localize-in-angular)

[Loading external configurations in Angular Universal]
(https://garage.sekrab.com/posts/loading-external-configurations-in-angular-universal)

{% embed https://garage.sekrab.com/posts/prerender-routes-in-express-server-for-angular-part-i %}