NGINX config for Single Page Applications

Whether you're using create-react-app, Next.js or even something non React based (like Angular), you'll come across the case where you write a Single Page Application (SPA) and need to support linking into a particular page.

Default webserver config doesn't really support this very well.  eg.

If you have a page like https://example.com/ and want to link directly to a users page like https://example.com/users/ it typically will give you a 404 error as there is no index.html in a users directory on the server. The cheapout option is to change your links from paths to anchors (eg. https://example.com#users) but this is ugly, plus prevents you using them to link to a section on the page (ie. their intended usage).

So here's the config I came up with for my photography site.

http {
    # what times to include
    include       /etc/nginx/mime.types;
    # what is the default one
    default_type  application/octet-stream;

    # Sets the path, format, and configuration for a buffered log write
    log_format compression '$remote_addr - $remote_user [$time_local] '
        '"$request" $status $upstream_addr '
        '"$http_referer" "$http_user_agent"';

    server {
        # listen on port 80
        listen 80;
        # save logs here
        access_log /var/log/nginx/access.log compression;

        gzip on;
        gzip_types text/html application/javascript application/json text/css;

        # where the root here
        root /usr/share/nginx/html;
        # what file to server as index
        index index.html;

        location / {
            # First attempt to serve request as file, then
            # as directory, then fall back to redirecting to index.html
            try_files $uri $uri/ $uri.html /index.html;
        }

        location ~* \.(?:css|js|jpg|svg)$ {
            expires 30d;
            add_header Cache-Control "public";
        }

        location ~* \.(?:json)$ {
            expires 1d;
            add_header Cache-Control "public";
        }
    }
}

The interesting parts of this are the location sections.

location / is what it makes all work, the try_files directive basically says to attempt to serve it directly, then fall back with a / (ie. to serve an index.html from a directory), then try a html file with the same path name, finally just serve up the normal index.html. This last step is what makes the SPA work, typically the main page will do your routing and looks at the URL to do it, beyond that it doesn't care where it's served from.

The second and third location sections are overrides to set up caching for images, javascript, and css files, along with json files separately. In the case of my site, images, javascript and css won't change (and if the js or css do, they get new filenames), so we cache for 30 days, however, the json files will update whenever I add a new image to the site, rebuild, and push. But I do want some caching, so 1 day seems reasonable.