Prerendering in Angular - Part II
Tags: javascript, webdev, angular, node
Published: 2022-09-25 12:37:02

Today we will continue with our previous efforts to [Replace i18n Package in Angular](https://garage.sekrab.com/posts/alternative-way-to-localize-in-angular), and adapt our Express server to generate multiple static pages per language. The plan is:

1.  Adapting to have multilingual support, URL driven
2.  Testing the APP_BASE_URL support
3.  Adapting to make it run for cookie driven apps
4.  Look into Firebase Netlify and Surge support

## Multilingual URL driven apps

Planning with the end in mind, we need to create the same routes with different language folders, so `/client/projects/index.html` needs to sit inside `/client/en/projects/index.html`. To do that, we need to adapt our fetch module, to loop through all supported languages.

Note: if you have multiple builds (as per the default Angular behavior), the output directory would be swapped. For example: `/en/static/projects/index.html`

```js
// host/prerender/fetch.js
// example of supported languages which probably should be in a config file
const languages = ['en', 'ar', 'tr'];

// adapt by accepting language
async function renderToHtml(lang, route, port, outFolder) {
  // adapt to call fetch with the language prefixed in URL
  const response = await fetch(`http://localhost:${port}/${lang}/${route}`);
  // ...

  // adapt to place in /client/static/en/route/index.html
    const d = `${outFolder}${lang}/${route}`;
	// ...
}

module.exports = async (port) => {
	// ...
  // adapt to loop through languages
  for (const lang of languages) {
    for (const route of routes) {
      await renderToHtml(lang, route, port, prerenderOut);
    }
  }
};
```

The Express rule we had does not need to change, as long as our `base href` value is set to the right language, which is the behavior expected with [URL driven apps](https://garage.sekrab.com/posts/serving-the-same-angular-build-with-different-urls).

```js
module.exports = function (app) {
	// this rule holds, as long as the base href is set to the right language folder
	// for example "/en/"
  app.use('/', express.static(rootPath + 'client/static'));
	// ... other routes
}
```

Run the server, browse to `http://localhost:port/en/projects`, disable JavaScript, and test. You should be seeing the static version of the file.

The [fetch script is on StackBlitz](https://stackblitz.com/edit/angular-express-prerender?file=host%2Fprerender%2Ffetch-url.js).

## APP_BASE_HREF token

This is exactly like URL driven applications, with the exception that the `base href` is set to root `/`. No adaptations needed. Read about this solution in [Using Angular APP_BASE_HREF](https://garage.sekrab.com/posts/serving-the-same-angular-build-with-different-urls).

## Cookie driven apps

Single build apps based on a cookie value are not recommended. Nevertheless, let's dig into it. Two things need to be adapted:

-   set the cookie ahead of the prerendering `fetch` (not allowed)
-   check for physical file before serving our site

In fetch, we are not allowed to set the cookie header, but we can resolve to setting a different header. Then in language middleware we check for that header value along with other factors.

```js
// prerender/fetch.js
// adapt the function to set custom header 
async function renderToHtml(lang, route, port, config) {
	const response = await fetch(`http://localhost:${port}/${route}`, 
		{
			headers: {'cr-lang': lang}
		});

	// ... the rest, save in different language folder as usual
	// ... the output index files all have base href set to '/'
}
```

We just need to catch the header in the middleware to know which index to serve

```js
// language.js middleware, adapt the value to read "custom header" 
// before resolving to cookie then to default
res.locals.lang = req.get('cr-lang') || req.cookies['cr-lang'] || 'en';
```

The above with the language loop generates static files inside language folders, like in URL driven apps, with the exception that the generated `base href`, is set to root `/`. Now to serve these files, the Express route needs to physically match them, this can be done with a middleware (like [serve-static middleware](https://expressjs.com/en/resources/middleware/serve-static.html)), but here we'll keep it simple:

```js
// express routes for cookie driven apps
// ...
const { existsSync } = require('fs');

app.get('/*', (req, res) => {
    // first find the physical page, relative to __dirname and routes file
		// res.locals.lang is assumed to be set by middleware cookie reader
		// strip out the URL params if used in Angular ";param=something"
	   
    const static =  rootPath + 'client/static/' + res.locals.lang + req.path.split(';')[0] + '/index.html';
    if (existsSync(static)) {
      res.sendFile(static);
      return;
    }
		// then render Angular SSR (a previous article)
    res.render(rootPath + `index/index.${res.locals.lang}.html`, {
      req,
      res
    });

});
```

Unfortunately, in StackBlitz I could not set cookies nor detect custom headers to test the prerender function, but I confirm that it works locally.

The [cookie based app is on StackBlitz](https://stackblitz.com/edit/angular-express-prerender?file=host%2Fserver%2Flanguage.js,host%2Fprerender%2Ffetch-cookies.js).

## Cloud hosting

The main difference in public hosts is the lack of the powerful Express static middleware, but the solution is to simply save the files as they are, under the `client` (public) folder. So the only difference if you are hosting on Firebase, Netlify and Surge, is having the `prerenderOut` folder set to `client`. But, you still have to create an SSR, at least locally, to be able to generate the prerendered files. So there is no way around having a local Express server to run it. Well, there is one way. It would make the effort smaller. An Angular builder that uses `RenderModule`. This will have to wait till the next episode. 😴

Thanks for reading on, let me know if you spot any marks that need to be sanded.

### About serverless functions

If you are hosting your SSR on the cloud under a serverless function, like Firebase, this is as good as an Express server. You can expose your static folder with Express static middleware, and run firebase emulators locally:

`firebase emulators:start`

then you run a separate express script to simply fetch the already running firebase app (usually at port `5000`). Like this:

```js
// simple script /prerender/serverless.js to run while firebase emulators are running
const config = require('../server/config');
const prerender = require('./fetch-url');

async function act(port) {
  await prerender(port, config);
  console.log('Done prerendering');
}

act(5000);
```

This will not close the port, and it's easier than trying to run `firebase` from Node.

`cd prerender && node serverless`

[The code is in StackBlitz](https://stackblitz.com/edit/angular-express-prerender?file=host%2Fprerender%2Fserverless.js).

{% embed https://garage.sekrab.com/posts/prerendering-in-angular-part-ii %}