Using Angular APP_BASE_HREF token to serve multilingual apps, and hosting on Netlify
Tags: angular, tutorial, netlify, webdev
Published: 2022-08-15 06:37:00

Previously, we worked out a way to generate the HTML pages ahead of time, and this will be very beneficial in cloud hosting. In this article, we shall push our **multilingual single-build Angular application to Netlify client-only hosting** and watch it sink or float.

Netlify
-------

To host on Netlify a browser-only app, all files must sit in the same folder, including any external configuration. We first make our `angular.json` copy all necessary assets to the host folder. On [StackBlitz](https://stackblitz.com/edit/angular-runtime-translation) find the example build under `/host-netlify`

In our `angular.json`, let's adapt the `writeindex` task as well:

```json
// in writeindex target, the destination should also be the same folder
// if you are using gulp tasks, you need to adjust options as well
"writeindex": {
  "builder": "./builder:localizeIndex",
  "options": {
		// source from netlify and place in same folder
    "source": "host/client/placeholder.html",
    "destination": "host/client",
   // ...
},
```

Our `package.json` will include only two tasks

```json
"build": "ng build --configuration=production",
"postbuild": "ng run cr:writeindex"
```

With `netlify` installed globally, move into `host` output folder, and run `netlify dev` to test locally. First, the cookie driven solution:

### Cookie driven apps

Netlify uses a special cookie to identify Language: `nf_lang`, this comes really handy in this setup. The `netlify.toml` then looks like this:

```toml
# the publish folder
[build]
  publish = "client/"

# netlfy.toml
[[redirects]]
  from = "/*"
  to = "/index.ar.html"
  status = 200
  # condition, cookie nf_lang set to "ar"
  conditions = {Language = ["ar"]}

# all other languages, default to "en"
[[redirects]]
  from = "/*"
  to = "/index.en.html"
  status = 200
```

In our config (or external configuration), set the cookie name to `nf_lang`

```ts
// src/app/config.ts
export const Config = {
  Res: {
    cookieName: 'nf_lang',
		//...
  },
};
```

Running locally, switching languages, works. Moving on. I wish life was always this easy.

### URL driven apps

After building with `base href` set to the right language (for example `/en/`), we need to rewrite all assets to root folder:

`/en/main.x.js` → `/main.x.js`

This is a rewrite rule, not a physical serve. But how do we set it apart from friendly URLs? `/en/products` for example? There are three ways:

-   **Manually place rules for specific routes**, then a general rewrite for the rest. This is the least efficient, so I will not dig into it.
-   Using Netlify [query rule](https://docs.netlify.com/routing/redirects/redirect-options/#query-parameters) to distinguish assets from friendly URLs
-   Relying on Angular itself to do the heavy lifting, using **APP_BASE_HREF** token, this is my preferred option

#### Filtering by query

We can distinguish URLs sent in every navigation request with a query string, for example: `nf_route`

In our `netlify.toml`

```toml
# redirect anything with query nf_route

# plain redirects need to be handled first
[[redirects]]
  from = "/en"
  to = "/index.en.url.html"
  status = 200

[[redirects]]
  from = "/ar"
  to = "/index.ar.url.html"
  status = 200

# for every url with nf_route in query, redirect back to root
[[redirects]]
  from = "/en/*"
  to = "/index.en.url.html"
  status = 200
  query = {nf_route = "1"}

[[redirects]]
  from = "/ar/*"
  to = "/index.ar.url.html"
  status = 200
  query = {nf_route = "1"}

# catch resources and rewrite, this will fail if url is /en/products without query
[[redirects]]
  from = "/:lang/*"
  to = "/:splat"
  status = 200

# if nothing matches redirect root to language cookie
[[redirects]]
  from = "/*"
  to = "/ar/"
  status = 301
  conditions = {Language = ["ar"]}

# default redirect
[[redirects]]
  from = "/*"
  to = "/en/"
  status = 301
```

Browsing to `/en` serves the `index.en.url.html`, and all other static resources under `/:lang/*` rule. A friendly URL like `/en/products/details?nf_route=1` will be served with `index.en.url.html` as well. To make that work, we need to append `nf_route=1` to all Angular routes. One way to do that is add a `NavigationEnd` event listener, and use `Location.go`, which replaces the URL without refreshing.

In root component, or App module:

```ts
export class AppModule {
  // Location from @angular/common
  constructor(router: Router, location: Location) {
    router.events
      .pipe(filter((event) => event instanceof NavigationEnd))
      .subscribe({
        next: (e: NavigationEnd) => {
          // for netlify query trick, add nf_route for all navigation
      	  if (router.url.indexOf('nf_route=') < 0 ) {
            // append it if it's not present
            location.go(router.url, 'nf_route=1');
          }
        },
      });
  }
}
```

One use case might slip momentarily, and that is when we browse to a friendly URL that has no query. The result is a `404.html` page. Netlify hosting catches that with a custom `404.html` page. We can make use of that to redirect via JavaScript, if it is not an asset.

```html
<!-- in host/client, add a 404.html page -->
<!DOCTYPE html>
<html>
<body>
    <script>
      if (window.location.href.indexOf('.') < 0) {
        // do nothing if it's a resource file, else add nf_route and replace
        window.location.replace(window.location.href + '?nf_route=1');
      }
    </script>
</body>
</html>
```

This also means we need to add the `404.html` to the `assets` array in `angular.json`. This solution feels like a bit of a hack. Let's move on to a better solution.

#### Angular `APP_BASE_HREF`

The other solution is to rely on Angular to provide the language prefix in URL, without changing the `base href` of the document. To do that, we add the `APP_BASE_HREF` token to our root providers, the value of which is reliant on the external language script.

```ts
// in root app.module
@NgModule({
  // ...
  providers: [
    { provide: LOCALE_ID, useClass: LocaleId },
    // here, provide APP_BASE_HREF, this will make URL based solution
    // use value saved in Res class
    { provide: APP_BASE_HREF, useValue: '/' + Res.language }
  ],
})
export class AppModule { }
```

This makes the app work with `base href` set to `/` and all routes are prefixed by the language provided by the script present in `index.[lang].html`. (In development, that is always the default language `/en`.)

To use this method, the following hosting setup is needed:

```toml
# netlify.toml with URL driven solution using APP_BASE_HREF token
# redirect anything with en or ar in root
# use the index file that has no language in the base href
[[redirects]]
  from = "/en/*"
  to = "/index.en.html" # not index.en.url.html
  status = 200

[[redirects]]
  from = "/ar/*"
  to = "/index.ar.html"
  status = 200

# if nothing matches redirect root to language cookie
[[redirects]]
  from = "/*"
  to = "/ar/"
  status = 301
  conditions = {Language = ["ar"]}

# default redirect
[[redirects]]
  from = "/*"
  to = "/en/"
  status = 301
```

All assets are relative to root, and are served statically. Angular routes are rewritten. This is a great solution.

#### APP_BASE_HREF in SSR

We almost never need it in SSR, even if we opt in serverless functions, but let's see if the above solution holds in SSR. We faced a similar issue with `LOCALE_ID`, the value accesses the script too soon, and it needs to be updated upon routing. So instead of `useValue` we use `useClass` and extend the `String` class:

```ts
// in app.module
{ provide: APP_BASE_HREF, useClass: RootHref }
```

Somewhere in our application (find it in StackBlitz `src/app/core/res.ts`)

```ts
// The RootHref class extends String
export class RootHref extends String {
  // for browser platform this needs to be in constructor
  constructor() {
    super('/' + (cr.resources.language || 'en'));
  }
}
```

Note: when I placed it in `toString` method, it did not work in browser platform. I ran out of candles so I did not dig any deeper!

#### Loose end

Since we have no server middleware to save the chosen language in a cookie, user's choice might be lost upon revisiting. A `click` event handler is enough to fix that:

```ts
// in html template:
`(click)="saveLanguage(language.name)"
 [href]="getLanguageLink(language.name)"`
// in component code, save cookie for next time
saveLanguage(lang: string) {
   this.setCookie(lang, Config.Res.cookieName, 365);
}
```

Hosting on browser-only Netlify was worth it. Let's invite others to the party.

Hosting on Firebase and Surge
-----------------------------

I started writing the host configuration for Firebase thinking it had more to offer, and for Surge thinking it would never work. But I was surprised on both accounts. Next episode of this long series to **twist Angular localization** will be dedicated to Firebase and Surge. 😴

Thank you for reading this far, have you fallen into the trap of revisiting old code like I have?

### RESOURCES

-   [StackBlitz Project](https://stackblitz.com/edit/angular-runtime-translation)
-   [Netlify File-based Configurations Redirects ".toml"](https://docs.netlify.com/configure-builds/file-based-configuration/#redirects)
-   [Netlify query redirects](https://docs.netlify.com/routing/redirects/redirect-options/#query-parameters)
-   [Angular APP_BASE_HREF token](https://angular.io/api/common/APP_BASE_HREF)

{% embed https://garage.sekrab.com/posts/using-angular-app_base_href-token-to-serve-multilingual-apps-and-hosting-on-netlify %}