The last piece of our quest is to adapt our Angular Prerender Builder to create multiple folders, per language, and add the index files into it. Hosting those files depends on the server we are using, and we covered different hosts back in Twisting Angular Localization post.
If you are going to make the best use of this article, I suggest you read the above post first.
At this point, I understand how reliant I have become on older posts, to release the dependency as much as possible, I will use some hard coded values.
Find the new builder in StackBlitz project under prerender-builder/prerender-multi
folder.
anchorCustom index file
As we previously did with the express prerender function, we have custom index.[lang].html
files created via a builder we spoke of in our Replacing i18n series. In our multilingual prerenderer, we need to loop through the supported languages, and pass the right index file. We can pass the supported language as part of the schema, or we can pass them from a wiretindex
builder (which we built in the article linked above).
I am not going to use the index builder in our StackBlitz to simplify matters, but if you do, it would be fetched the same way the browser target and the server target are fetched, like this:
// optional
// index.ts, inside execute function
// let schema have options.indexTarget to point to writeindex target
// get writeindex target to read supported languages and other options
const indexTarget = targetFromTargetString(options.indexTarget);
const indexOptions = (await context.getTargetOptions(indexTarget)) as any;
In our StackBlitz, let’s add the needed options directly to the schema. The following elements are needed to figure out where the index file is.
// new options to add to schema
interface Options {
destination: string; // this is where the index files are
languages: string[]; // the supported languages, in 2-char codes
localePath: string; // this is where the locale scripts sits inside the browser target
}
Remember we still need to create a server build, to generate the main.js
. Whether with ngExpressEngine
or not, it must at least export the AppServerModule
and the renderModule
function.
anchorLanguage files
This part is specific to our chosen way of localization, which is to embed a JavaScript that runs on both client and server, with the translation keys, inside cr.resources
declared variable. Here is a shortcut to the English locale used.
This is embedded in index.en.url.html
. For example:
<!-- host/index/index.en.url.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<base href="/en/" />
<!-- this file has the resources and embedded in the index file -->
<script src="locale/cr-en.js" defer></script>
...
</html>
The Angular renderModule
function has no context, so we need to impose one. In our case, the resources are defined in a global variable: cr
. In worker.ts
we add the following global
definition:
// to worker.ts, add the cr resources group like we did previously
// wait for execution to populate
// if you set noImplicitAny in tsconfig, this can be casted to any
// (<any>global).cr = {};
global.cr = {};
In PreRender
function we import the script, and populate
// in worker.ts, we need to import the file, which assigns the global.cr[language],
// then we need to populate our server global.cr.resources
await import(localePath);
global['cr'].resources = global['cr'][language];
And finally we construct output paths containing the language:
// worker.ts: correct route and language, like ./client/en/route/index.html
const outputFolderPath = path.join(clientPath, language, route);
We change the model passed to PreRender
to contain the missing information: language
and localePath
// worker.ts
// change the render options, and change the PreRender signature
export interface RenderOptions {
indexFile: string; // this is now the full path of the index file
// ...
// add language, and the locale script path
language: string;
localePath: string;
}
anchorThe main loop
Now back to our index.ts
, we need to loop through the languages, and prepare two paths: the index path, and the locale path.
// index.ts
// change the renderer, pass 'options' from execute function
async function _renderUniversal(
routes: string[],
context: BuilderContext,
clientPath: string,
serverPath: string,
// passing options
options: IOptions
): Promise<BuilderOutput> {
// ... the changes are as follows
try {
// loop through options languages
for (const lang of options.languages) {
// create path to pass to worker for example: './index/index.lang.url.html'
const indexFile = path.resolve(context.workspaceRoot, getIndexOutputFile(options, lang));
// check existence of locale else skip silently, client/locale/cr-en.js for example
const langLocalePath = path.join(clientPath,`${options.localePath}/cr-${lang}.js`);
if (!fs.existsSync(langLocalePath)) {
context.logger.error(`Skipping locale ${lang}`);
continue;
}
// then the results map
const results = (await Promise.all(
routes.map((route) => {
const options: RenderOptions = {
// ...
// adding these
localePath: langLocalePath,
language: lang
};
// ...
})
)) as RenderResult[];
// ...
}
// ...
}
return { success: true };
}
// the index outpfile can be as simple or as complicated as the project setup needs
function getIndexOutputFile(options: any, lang: string): string {
// index/index.lang.url.html as an example
return `${options.destination}/index.${lang}.url.html`;
// could be client/en/index.html, like in Surge host
}
Back to angular.json
, we assign the required params in a new target:
"prerender-multi": {
"builder": "./prerender-builder:prerender-multi",
"options": {
"browserTarget": "cr:build:production",
"serverTarget": "cr:server:production",
"routes": ["/projects", "/projects/1"],
"languages": ["en", "ar", "tr"],
// where are the index files?
"destination": "./host/index",
// where in browser target do the locale scripts sit?
"localePath": "locale"
}
}
Run ng run cr:prerender-multi
creates language folders under the browser target, notice that I did not group them under a static
folder as I did in Part II of this series, to simplify matters, and to be more realistic: we use Angular builder when we don’t have SSR in mind (otherwise use Express to prerender), to host on hosts like Netlify, which favors static files over routing rules. This is why it is important to place the static files directly under the public hosting folder (in our case, client
folder).
anchorPrerendering the home index.html
A note about creating a prerendered version of the homepage itself, like en/index.html
. If we generate a static index.html
, things will work as expected, but there is a price to pay. All non static pages, will load the index.html
first before it kicks off JavaScript to hydrate. That is bad!
- For SEO, the server version is that static physical file of index, no matter what route is requested
- For user experience. the site may flicker the static index before it reroutes.
In hosts like Netlify, or Firebase where we deliberately create the language sub folders, I would avoid generating the root statically.
// if you have this, avoid prerendering root
// in firebase host
"i18n": {
"root": "/"
},
"rewrites": [
{
"source": "/ar{,/**}",
"destination": "/ar/index.html"
},
{
"source": "**",
"destination": "/en/index.html"
}
]
// in netlify
# [[redirects]]
from = "/en/*"
to = "/en/index.html"
status = 200
But if we use index.[lang].html
on root, and serve it as a rewrite, or as the case is with Surge /en/200.html
file, serving a static root is not a problem.
// if you have this, prerendering root is ok
// in firebase host
"rewrites": [
{
"source": "/ar{,/**}",
"destination": "/index.ar.html"
},
{
"source": "**",
"destination": "/index.en.html"
}
]
// in netlify
[[redirects]]
from = "/en/*"
to = "/index.en.html"
status = 200
I hope I covered all corners of this beast. If you have questions about creating a single build per multiple languages, hosting on different hosts, or prerendering for different languages, or you have an idea or suggestion, please hit the comment box, or the twitter link to let me know.
Let me know if there are other subjects in Angular you would like to see ripped apart 😁