Content negotiation is the ability of a web server to deliver the document that best matches the browser’s preferences/capabilities. For example, if a resource exists in multiple languages, the web server can choose which variant it serves based on the Accept-Language header delivered by the browser. This tutorial describes how to configure content negotiation in Apache2 to serve different languages based on browser preferences.
I do not issue any guarantee that this will work for you!
1 Preliminary Note
In Apache2, content negotiation is made available by the mod_negotiation module which is built-in by default.
I will use a Debian Squeeze system here with Apache2 already installed, but the Apache configuration is independent of the distribution you use.
On Debian Squeeze, there’s a global content negotiation configuration in /etc/apache2/mods-available/negotiation.conf; because I want to demonstrate how to configure it on a per-vhost basis, I comment out everything in that file (of course, you can do your content negostiation configuration in this file instead of in your vhosts if you want the configuration to be valid for all your vhosts):
<IfModule mod_negotiation.c> # # LanguagePriority allows you to give precedence to some languages # in case of a tie during content negotiation. # # Just list the languages in decreasing order of preference. We have # more or less alphabetized them here. You probably want to change this. # #LanguagePriority en ca cs da de el eo es et fr he hr it ja ko ltz nl nn no pl pt pt-BR ru sv tr zh-CN zh-TW # # ForceLanguagePriority allows you to serve a result page rather than # MULTIPLE CHOICES (Prefer) [in case of a tie] or NOT ACCEPTABLE (Fallback) # [in case no accepted languages matched the available variants] # #ForceLanguagePriority Prefer Fallback </IfModule>
Restart Apache afterwards:
I will modify Debian’s default Apache vhost with the document root /var/www and the vhost configuration file /etc/apache2/sites-available/default in this tutorial.
Let’s delete any index file in /var/www…
rm -f /var/www/index.*
… and create three new index files, one in German (index.html.de), one in English (index.html.en), and one in French (index.html.fr):
<html> <head> <title>Deutsch</title> </head> <body text="#000000" bgcolor="#FFFFFF" link="#FF0000" alink="#FF0000" vlink="#FF0000"> <h1>Willkommen zu unserer deutschen Seite!</h1> </body> </html>
<html> <head> <title>English</title> </head> <body text="#000000" bgcolor="#FFFFFF" link="#FF0000" alink="#FF0000" vlink="#FF0000"> <h1>Welcome To Our English Site!</h1> </body> </html>
<html> <head> <title>Français</title> </head> <body text="#000000" bgcolor="#FFFFFF" link="#FF0000" alink="#FF0000" vlink="#FF0000"> <h1>Bienvenue sur notre site français!</h1> </body> </html>
Our /var/www directory looks as follows:
ls -la /var/www/
root@server1:~# ls -la /var/www/
drwxr-xr-x 2 root root 4096 Jul 20 00:13 .
drwxr-xr-x 14 root root 4096 Feb 14 18:43 ..
-rw-r–r– 1 root root 196 Jul 20 00:06 index.html.de
-rw-r–r– 1 root root 186 Jul 20 00:03 index.html.en
-rw-r–r– 1 root root 207 Jul 20 00:09 index.html.fr
I want Apache to deliver the right document (variant) based on the browser’s language preferences. Browsers send an Accept-Language header which lists their preferred language(s). This can be achieved in two ways:
- By using the MultiViews option where Apache does a file name pattern match and chooses the appropriate variant from the results.
- By using a type map where you explicitly list all variants.
MultiViews works as follows: if you request a resource named foo, and foo does not exist in the directory, Apache will search for all files named foo.*, like foo.html, foo.html.de, foo.html.en, foo.de.html, foo.en.html, foo.en.html.gz, foo.gz.en.html, foo.html.gz.en, foo.html.en.gz, and so on.
MultiViews is a per-directory setting, i.e., it has to be set with an Options directive in a <Directory>, <Location>, or <Files> section in the Apache configuration. It has to be set explicitly (i.e. Options MultiViews); Options All does not set it.
[...] <Directory /var/www/> Options Indexes FollowSymLinks MultiViews DirectoryIndex index.html AllowOverride None Order allow,deny allow from all </Directory> [...]
That’s basically all we need for content negotiation.
Now let’s look at our browser’s language preferences (I use Firefox 5 here). Go to Tools > Options:
Under Content, there is a Languages area where you can click on the Choose… button:
In this example, I have en-US and en enabled (the order is important!):
Now let’s go to our default vhost (my server’s IP address is 192.168.0.100, so I go to http://192.168.0.100). I have the LiveHTTPHeaders plugin for Firefox installed so that I can check the headers sent by Firefox. As you see, my browser sends the header Accept-Language: en-us,en;q=0.5 which means its first preference is en-US (if no q value (q = Quality/Priority) is present, this means q=1.0; q must be a value between 0 (lowest priority) and 1 (highest priority)).
Because we have specified no file name in our request (just http://192.168.0.100 instead of http://192.168.0.100/index.html or http://192.168.0.100/foo.html), Apache will use the DirectoryIndex directive to search for an index file, and because we use DirectoryIndex index.html, it will search for index.html. Because index.html does not exist, MultiViews will now try to find the best match for the request; we don’t have an index.html.en-US file, but we have an index.html.en file (en is the second-best preference of our browser), so index.html.en gets served:
(Apache will also try to match language subsets if no other match can be found. For example, if our browser accepted only en-US, Apache would serve the en variant – index.html.en – instead of serving a 406 “Not Acceptable” error (en gets a low q value, but because the browser does not accept any other language, the en variant will be served). But if the browser sends an Accept-Language: en-us,fr;q=0.8 header, for example, the French variant – index.html.fr – will be served because of the low q value for en.)
Now let’s add further languages to the end of our language preferences (like de and fr):
As you see, Firefox sends a modified Accept-Language header, with en-US and en still being the first and second preference, and de and fr added with lower q values. Therefore, Apache will still serve the en variant:
Now let’s change the order of languages in Firefox. For example, move de to the top of the list:
Apache will now serve the de variant, and you see that de has now the top priority in the Accept-Language header:
Now let’s delete all languages from our browser’s preferences and add languages (like es and it) for which Apache does not have any variants:
Apache will now serve a 406 “Not Acceptable” error because no variant matches:
To solve this problem, we add the LanguagePriority and ForceLanguagePriority directives to our vhost:
[...] <Directory /var/www/> Options Indexes FollowSymLinks MultiViews DirectoryIndex index.html AllowOverride None Order allow,deny allow from all LanguagePriority en de fr ForceLanguagePriority Fallback </Directory> [...]
LanguagePriority specifies a precendence of language variants for cases where the client does not express a preference (list language variants with decreasing priority from left to right, which means that in LanguagePriority en de fr, en has the highest and fr the lowest priority).
ForceLanguagePriority Fallback uses the LanguagePriority list to serve a valid variant instead of a 406 “Not Acceptable” error in cases where the browser accepts only language for which no variants are available. In this case, the en variant would be served (if it is available), then the de variant (if there’s no en variant), and then the fr variant (if there are no en or de variants).
… and reload the page, and you should get the en variant now instead of the 406 error:
Theoretically it is also possible that a client sends an Accept-Language header where languages have the same priority (e.g. Accept-Language: en;q=0.5,de;q=0.5). In this case Apache would send a 300 “Multiple Choices” result because it cannot decide which of the available variants to serve. To prevent this, add Prefer to the ForceLanguagePriority directive:
[...] <Directory /var/www/> Options Indexes FollowSymLinks MultiViews DirectoryIndex index.html AllowOverride None Order allow,deny allow from all LanguagePriority en de fr ForceLanguagePriority Prefer Fallback </Directory> [...]
This makes that if two or more variants match with the same priority, the first matching variant from the LanguagePriority directive will be served. (In case of the Accept-Language: en;q=0.5,de;q=0.5 header, that would be en.)
In general, it’s a good idea to use the
LanguagePriority [list of languages]
ForceLanguagePriority Prefer Fallback
3 Type Maps
Instead of using MultiViews, we can use a type map to explicitliy specify the available variants. Type maps have the extension .var, and we need to add a AddHandler type-map var directive to our configuration. Also, we remove MultiViews from the Options line. Since Apache has no chance of knowing that it should read index.var when we request http://192.168.0.100/, we need to specify index.var in the DirectoryIndex line. (This is a drawback of the type map way – for example, if you have a foo.var file and request foo, Apache has no way of knowing that it should read foo.var. You’d have to add foo.var to the DirectoryIndex line and request http://192.168.0.100/ instead of http://192.168.0.100/foo.)
Our configuration looks as follows:
[...] <Directory /var/www/> Options Indexes FollowSymLinks DirectoryIndex index.var AllowOverride None Order allow,deny allow from all AddHandler type-map var LanguagePriority en de fr ForceLanguagePriority Prefer Fallback </Directory> [...]
Now we create our /var/www/index.var file as follows:
URI: index URI: index.html.en Content-Type: text/html Content-Language: en URI: index.html.de Content-Type: text/html Content-Language: de URI: index.html.fr Content-Type: text/html Content-Language: fr
The type map file should have the same name as the resource it describes (e.g. index.var for index.html.de, index.html.en, and index.html.fr). It must have an entry for each available variant, and entries for different variants must be separated by a blank line (blank lines are not allowed within an entry). Entries consist out of contiguous HTTP header lines (see http://httpd.apache.org/docs/2.0/mod/mod_negotiation.html#typemaps for more details). It is a convention to begin a map file with a line for the entity as a whole, although this is not required and will be ignored (i.e., the line URI: index at the beginning is not required).
That’s it! You can now do the same tests as in chapter 2, and if nothing went wrong, the correct variant should be served.
- Apache2 Content Negotiation: http://httpd.apache.org/docs/2.0/content-negotiation.html
- mod_negotiation: http://httpd.apache.org/docs/2.0/mod/mod_negotiation.html
- Debian: http://www.debian.org/