Prerendering in Angular - Part IV

ayyash

Ayyash

Posted on October 5, 2022

Prerendering in Angular - Part IV

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.

Custom 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;
Enter fullscreen mode Exit fullscreen mode

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\
}
Enter fullscreen mode Exit fullscreen mode

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 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>
Enter fullscreen mode Exit fullscreen mode

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 = {};
Enter fullscreen mode Exit fullscreen mode

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];
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;\
}
Enter fullscreen mode Exit fullscreen mode

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.

// 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\
}
Enter fullscreen mode Exit fullscreen mode

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"\
  }\
}
Enter fullscreen mode Exit fullscreen mode

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.

// 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

RELATED POSTS

💖 💪 🙅 🚩
ayyash
Ayyash

Posted on October 5, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related