Prerendering in Angular - Part IV
Tags: angular, webdev, javascript, tutorial
Published: 2022-10-05 17:05:57

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](https://garage.sekrab.com/posts/using-angular-app_base_href-token-to-serve-multilingual-apps-and-hosting-on-netlify) 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](https://stackblitz.com/edit/angular-express-prerender) project under `prerender-builder/prerender-multi` folder.

## Custom index file

As we previously did with the [express prerender function](https://garage.sekrab.com/posts/prerendering-in-angular-part-ii), we have custom `index.[lang].html` files created via a builder we spoke of in our [Replacing i18n series](https://garage.sekrab.com/posts/pre-generating-multiple-index-files-using-angular-builders-and-gulp-tasks-to-serve-a-multilingual-angular-app). 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](https://stackblitz.com/edit/angular-express-prerender) 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:

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

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

## Language 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](https://stackblitz.com/edit/angular-runtime-translation?file=src%2Flocale%2Fcr-en.js) used.

This is embedded in `index.en.url.html`. For example:
```html
<!-- 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:

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

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

```ts
// 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`

```ts
// 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;\
}
```

## The 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.

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

```json
"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).

### Prerendering 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.

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

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


#### RESOURCES


-   [StackBlitz project](https://stackblitz.com/edit/angular-express-prerender)

#### RELATED POSTS

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