Serving the same Angular build with different URLs
Tags: angular, webdev, javascript, tutorial
Published: 2022-07-25 07:18:00

Previously we created a server in **Express** to serve the same Angular build with a different language file, using rewrite rules for that file, detecting a cookie. That works well for intranets and authorized applications. Today we shall create our application to detect the language in the URL instead. Like this:

`www.site.com/**en**/content/details`

Benefits of language specific URL
---------------------------------

I can think of two benefits for having the language in the URL:

-   **Search bot confusion**: for public content that can be crawled by search bots, if bots can crawl multiple languages, serving different content for the same URL confuses the bot, and affects the site's rank.
-   **Localized results**: speaking of bots, the language in the URL allows us to have [**alternate links**](https://developers.google.com/search/docs/advanced/crawling/localized-versions) in the header for different languages, search engines return the relevant match to users. Google states that

> Note that the language-specific subdomains in these URLs (`en`, `en-gb`, `en-us`, `de`) are not used by Google to determine the target audience for the page; you must explicitly map the target audience.

But then again, Google does not document everything, I have a hunch, that it does make a difference.

-   The second benefit is **users' convenience** if they happen to choose a different language to their locale. Saving URLs in favorites, opening in different devices, or sharing it amongst their friends, it is preferred that the URL itself holds the language information to know user's intent.

Great, two benefits, in three bullet points. I hope you're convinced. Let's get on with it.

Find the files in [StackBlitz](https://stackblitz.com/edit/angular-runtime-translation), though don't expect much, the environment is too strict to allow them to run.

Browser only application
------------------------

We need to accomplish the following:

-   Detect the language from URL in the language middleware
-   Serve the right `base href` value in `index.html`
-   Reroute unsupported languages to default language
-   Handle the root URL

### Detect language from URL

Starting with the **language middleware**:

```javascript
module.exports = function (config) {
  return function (req, res, next) {
    // exclude non html sources, for now exclude all resources with extension
    if (req.path.indexOf('.') > 1) {
      next();
      return;
    }

    // derive language from url, the first segment of the URL, no checks yet
    res.locals.lang = req.path.split('/')[1];

    next();
  };
}
```

We are extracting the first segment of the URL no matter what. Following are the routes: (find them in StackBlitz under [/host/server/routes-url.js](https://stackblitz.com/edit/angular-runtime-translation?file=host%2Fserver%2Froutes-url.js))

```javascript
// express routes
module.exports = function (app, config) {
  // reroute according to lang, does not matter what param is passed because it's already set
  app.get('/:lang/locale/language.js', function (req, res) {
    res.sendFile(config.getLangPath(res.locals.lang));
  });

  // use static files in client, but skip index
  app.use('/:lang', express.static(config.rootPath + '/client', {index: false}));

	// TODO: exclude unsupported languages
  app.get('/:lang/*', function(req, res){
		// TODO: here, develop an HTML template engine to replace the base href value
    res.render(config.rootPath + `client/index.html`, {lang: res.locals.lang});
  });

  // nothing matches? redirect to /root
  app.get('/*', function (req, res) {
    // if none, redirect to default language (TODO: default language)
    res.redirect(301, '/' + res.locals.lang + req.path);
  });
};
```

#### Why `index: false` option

We had no issues in the browser-only app in the previous article; letting the `index.html` be served by the **express static** module, since we served a static file. Now that we are going to develop a **template engine** to change the `index.html`, we need to **disable the default index for root URLs in the static middleware**. So `site.com/en/` should not be served by the static middleware, thus we pass `index: false` option:

`app.use('/:lang', express.static(config.rootPath + '/client', {index: false}));`

There are less direct methods, renaming index.html and changing the default file; to name a few.

### Index base href replacement

The first task on our list of tasks is to generate the right base href per language served. We will create a [**simple HTML template engine**](https://expressjs.com/en/advanced/developing-template-engines.html), that replaces the string with the selected language. We can place the following code anywhere on our server:

```javascript
// in epxress routes
// ...
const fs = require('fs') // this engine requires the fs module

module.exports = function (app, config) {
	// ...
  app.engine('html', (filePath, options, callback) => {
    // define the template engine
    fs.readFile(filePath, (err, content) => {
      if (err) return callback(err);

      // replace base href tag, with the proper language
      const rendered = content.toString()
        .replace('<base href="/">', `<base href="/${options.lang}/">`);
      return callback(null, rendered)
    });
  });
	// setting the engine and views folder are not needed
  // ...
  app.get('/:lang/*', function(req, res){
	  // use the HTML engine to render
    res.render(config.rootPath + `client/index.html`, {lang: res.locals.lang});
  });
	// ...
}
```

### Unsupported languages

The other challenge is finding a non supported language and rolling back. In the language middleware, we need to find the language first, **compare it to supported languages** list, if not found, return a default language. Let's first add a list of supported languages to our `config` (again, this is a personal choice, that looks a bit all over the place, but for the scope, it should do).

```javascript
// config.js
module.exports = {
	// ...
  // supported languages
  languages: ['en', 'ar']
};
```

In our **language middleware:**

```javascript
// language middleware:
// derive language from url, the first segment of the URL,
// check if found in supported languages
res.locals.lang = config.languages.find(n => n === req.path.split('/')[1]) || 'en';
```

In our routes, we need to take care of one route only, the one that decides the language. So for the `index.html` route, we'll pass an array of all supported languages as a path:

```javascript
// routes, use only supported lanugages URLs
app.get(config.languages.map(n => `/${n}/*`), function(req, res){
  // pass language found in language middleware
  res.render(config.rootPath + `client/index.html`, {lang: res.locals.lang});
});
```

### Root URL

The last bit is to redirect the root URL to an existing language. The best choice is to try and fetch a **cookie** first before defaulting to some language. Thus the cookie bit is still useful in our language middleware.

```javascript
// language middleware
module.exports = function (config) {
  return function (req, res, next) {

		// check cookies for language
    res.locals.lang = req.cookies[config.langCookieName] || 'en';

		// exclude non html sources, exclude all resources with extension
    if (req.path.indexOf('.') > 1) {
      next();
      return;
    }

    // derive language from url, the first segment of the URL,
		// then fall back to cookie
    res.locals.lang = config.languages.find((n) => n === req.path.split('/')[1]) ||
      res.locals.lang;

		// set cookie for a year
    res.cookie(config.langCookieName, res.locals.lang, {
      expires: new Date(Date.now() + 31622444360),
    });

    next();
  };
}
```

Then in the routes, the last route to add:

(This also takes care of any URLs that were not previously prefixed with language, or prefixed with a non supported language, which is a scenario we do not wish to dive into.)

```javascript
// nothing matches? redirect to /en/path
app.get('/*', function (req, res) {
  res.redirect(301, '/' + res.locals.lang + req.path);
});
```

Server platform
---------------

Pretty much the same as browser only routes. We do not need to create a new engine, **the template engine is already provided by Angular**. Reading the documentation of the `ngExpressEngine`, the property that renders the HTML file is `document`.

```javascript
// build routes in SSR and change language via url
// find it in stackblitz host/server/routes-ssr-url.js

const ssr = require('./main');
const fs = require('fs');

module.exports = function (app, config) {
  // ngExpressEngine
  app.engine('html', ssr.AppEngine);
  app.set('view engine', 'html');
  app.set('views', config.rootPath + '/client');

  // reroute according to lang, does not matter what param is passed because its already set
  app.get('/:lang/locale/language.js', function (req, res) {
    res.sendFile(config.getLangPath(res.locals.lang));
  });

  // use static files in client, skip index.html
  app.use(
    '/:lang',
    express.static(config.rootPath + '/client', { index: false })
  );

  // exclude unsupported languages
  app.get(config.languages.map((n) => `/${n}/*`), function (req, res) {

    // use Angular engine, pass a new string of HTML in document property
    const content = fs.readFileSync(config.rootPath + `client/index.html`);
    const rendered = content.replace('<base href="/">', `<base href="/${res.locals.lang}/">`);

  	// first attribute does not matter, it's the default in views folder
		res.render('', {
      req,
      res,
      // overwrite here
      document: rendered
     });
    }
  );

  // nothing matches? redirect to /en/path
  app.get('/*', function (req, res) {
    res.redirect(301, '/' + res.locals.lang + req.path);
  });
};
```

Serving a little more than language
-----------------------------------

There are solutions out there for translation, that switch the site language without a refresh ([ngx-Translate](https://github.com/ngx-translate/core) is one), there are a couple of issues with that. One of them is the need to change more than just the language file in `index.html`. We already adapted the HTML `base href` value, what else can we adapt? Let's find out next episode. 😴

Thank you for reading this far, I have been typing with a makeshift bandage around my index finger. Forgive my mizbells.

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

[Serving multilingual Angular application with ExpressJS](https://garage.sekrab.com/posts/serving-multilingual-angular-application-with-expressjs)

[Serving the same Angular build with different URLs](https://garage.sekrab.com/posts/serving-the-same-angular-build-with-different-urls)

### RESOURCES

-   [Developing template engines for Express](https://expressjs.com/en/advanced/developing-template-engines.html)
-   [Google localized versions of your pages](https://developers.google.com/search/docs/advanced/crawling/localized-versions)
-   [Stackblitz Project](https://stackblitz.com/edit/angular-runtime-translation)

{% embed https://garage.sekrab.com/posts/serving-the-same-angular-build-with-different-urls %}