Today we will continue with our previous efforts to Replace i18n Package in Angular, and adapt our Express server to generate multiple static pages per language. The plan is:
- Adapting to have multilingual support, URL driven
- Testing the APP_BASE_URL support
- Adapting to make it run for cookie driven apps
- Look into Firebase Netlify and Surge support
anchorMultilingual 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.
/en/static/projects/index.html
// 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.
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.
anchorAPP_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.
anchorCookie 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.
// 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
// 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), but here we’ll keep it simple:
// 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.
anchorCloud 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.
anchorAbout 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:
// 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