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
anchorBenefits 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 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.
anchorBrowser only application
We need to accomplish the following:
- Detect the language from URL in the language middleware
- Serve the right
base href
value inindex.html
- Reroute unsupported languages to default language
- Handle the root URL
anchorDetect language from URL
Starting with the language middleware:
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)
// 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}));
anchorIndex 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, that replaces the string with the selected language. We can place the following code anywhere on our server:
// 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});
});
// ...
}
anchorUnsupported 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).
// config.js
module.exports = {
// ...
// supported languages
languages: ['en', 'ar']
};
In our language middleware:
// 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:
// 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});
});
anchorRoot 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.
// 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.)
// nothing matches? redirect to /en/path
app.get('/*', function (req, res) {
res.redirect(301, '/' + res.locals.lang + req.path);
});
anchorServer 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
.
// 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.toString().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);
});
};
anchorServing a little more than language
There are solutions out there for translation, that switch the site language without a refresh (ngx-Translate 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.