NGINX: Handling 404 within Static Web

Written by Dominik Pantůček on 2025-03-13

nginxpuppet

Gracefully handling non-existent pages in any web presentation is usually a desired course of action. A bare-bones default "404 Not Found" page tells the user enough, but - frankly - does not look that exciting. But is it possible to have a nice multilingual error page while keeping everything tidy in a Puppet configuration like we use?


Although it has been awhile since we have finally migrated our website from earlier solution to a purely static website generated from a directory tree full of CommonMark markdown flavour files, there are some little features we planned to implement later and this time it was a graceful handling of the infamous "404 Not Found" error when a non-existent link to our website is followed. Our goals were as usual:

  • the page should be multi-lingual and appropriate language should be displayed based on where in the presentation tree the user is trying to find something,
  • it should be created using the same tools and methods we already have implemented - ideally it would be just another ordinary page in our pages repository with different files for different languages, and
  • some fancy graphical representation would be a plus - however we want to stick to purely static HTML and CSS without any Javascript.

It turned out that modern CSS has even some decent animation capabilities so our UX expert was able to convey the important information to the user who got lost in a pretty straightforward way. However there were some problems with references to the stylesheet. Our website uses strictly only relative URLs, allowing us to test multiple branches of it in simple subdirectories of our development environment. However there may be an arbitrary number of path components in the URL of the page that was not found and therefore just serving the prepared 404 page at that location would break links to stylesheet and perhaps any images.

That is why we decided to redirect any non-existing URLs to a well-defined URL for each language. All this can be done relatively easily in the nginx webserver configuration like this:

  location ~ ^ {
    try_files $uri $uri/ @missing;
  }

  location @missing {
    rewrite ^/en /en/404/ redirect;
    rewrite ^/fr /fr/404/ redirect;
    rewrite ^/ /404/ redirect;
  }

As can be seen in the configuration snippet above, all the languages must be explicitly part of the configuration, however there are not many of them and all the configuration can be in a single location block. But of course, we never write our nginx configurations manually, we use our Puppet configuration management system with all this data nicely stored in the hiera YAML files. This simple configuration - luckily - translates into complementary Puppet configuration as follows:

    locations:
      'pages':
        location: '~ ^'
        location_cfg_append:
          try_files: '$uri $uri/ @missing'
      'missingpages':
        location: '@missing'
        location_cfg_append:
          rewrite:
            - '^/en /en/404/ redirect'
            - '^/fr /fr/404/ redirect'
            - '^/ /404/ redirect'

This configuration however does not cover our development environment where multiple branches of our presentation reside in multiple subdirectories of the web root directory. With clever adjustments, this setup can easily be modified to accommodate for such structure. We even skipped the actual manual testing of such configuration and wrote it directly in our Puppet repository:

      'branch':
        location: '~ ^/([^/][^/][^/]+)/'
        location_cfg_append:
          try_files: '$uri $uri/ @missing'
      'missing':
        location: '@missing'
        location_cfg_append:
          rewrite:
            - '^/([^/]+)/en /$1/en/404/ redirect'
            - '^/([^/]+)/fr /$1/fr/404/ redirect'
            - '^/([^/]+)/ /$1/404/ redirect'

The crucial part is to enable the fallback mechanism only for branches which have to have (for other reasons) at least three characters long names and then ensure the default language is matched as last rewrite. Yes, that is enough to redirect the users to appropriate error page of chosen language within the same branch subdirectory.

Hope you liked a lighter topic today and see ya next time for something more heavy again!