How deployment services make client-side routing work

category: notes

Recently I deployed a Single Page Application (SPA) to Vercel (formerly Zeit). I was surprised to find that my client-side routes worked, even though my deployment only had a single index.html file and a JavaScript routing library. Here’s which ‘magic’ makes that possible, for if you ever end up needing the same.

How servers serve

In the most normal of circumstances, when we access a web page by its URL, it is requested from a server. This has worked since, I believe, the early nineties (URLs were more recently standardised in the URL spec). The browser deals with that for us, or we can do it in something like curl:

curl GET https://hiddedevries.nl/en/blog/

We tell the server “give me this page on this host”. To respond, the server will look on its file system (configs may vary). If it can find a file on the specified path, it responds with that file. Cool.

Now, If you have a Single Page App that does not do any server side rendering, you’ll just have a single page. This is, for instance, what I deployed:

public
- index.html
- build
  - bundle.js
  - bundle.css
- images
  - // images

Let’s say I deployed it to cats.gallery. If someone requests that URL, the server looks for something to serve in the root of the web server (or virtual equivalent of that). It will find index.html and serve that. Status: 200 OK.

Client side routes break by default

Our application is set up with client side routing, but, for reasons, we have no server side fallback (don’t @ me). It contains a div that will be filled with content based on what’s in the URL. This is decided by the JavaScript, which runs on the client, after our index.html was served.

For instance, there is cats.gallery/:cat-breed, which displays photos of cats of a certain breed.

Cat sitting in middle of road A British shorthair - photo by Martin Hedegaard on Flickr

When we request cats.gallery/british-shorthair, as per above, the server will look for a folder named british-shorthair. We’ve not specified a file, the server will look for index.html there. No such luck, because we created no fallback, there’s just the single index.html in the root. If servers don’t find anything to serve, they’ll serve an error page with the 404 status code.

It does matter how we’ve loaded the sub page about British shorthairs. Had we navigated to cats.gallery and clicked a link to the British shorthair page, that would have worked, because the client side router would have replaced the content on the same single page.

How do they make it work?

One way to fix the problem, is to use hash-based navigation and do something like cats.gallery/#british-shorthair. Technically, we’re still requesting that index.html in the root, the server will be able to serve it, and the router can figure out that it needs to serve cats of the British shorthair breed.

But, we want our single page app to feel like a multi page app, and have “normal” URLs. Surprisingly, this still works on some services. The trick they need to do is to serve the index.html file that’s in our project’s root, but for any route. Want to see British shorthairs? Here’s index.html. Want to see Persian cats? Look, index.html. Returning to the home page? index.html for you, sir!

Vercel

Vercel does this automagically, as described in the SPA fallback section of their documentation.

This is the set up they suggest:

{
  "routes": [
    { "handle": "filesystem" },
    { "src": "/.*", "dest": "/index.html" }
  ]
}

First handle the file system, in other words, look for the physical file if it exists. In any other case, serve index.html.

Netlify

On Netlify, this is the recommended rewrite rule:

/*    /index.html   200

GitHub Pages

Sadly, on GitHub Pages, we cannot configure this kind of rewriting. People have been requesting this since 2015, see issue 408 on an unofficial GitIHub repository.

One solution that is offered by a friendly commenter: symlinks! GitHub Pages allows you to use a custom 404 page, by creating a 404.html file in the root of your project. Whenever the GitHub Pages server cannot find your files and serves a 404, it will serve 404.html.

Commenter wf9a5m75 suggests a symlink:

$> cd docs/
$> ln -s index.html 404.html

This will serve our index.html when files are not found. Caveat: it will do this with a 404 status code, which is going to be incorrect if we genuinely have content for the path. Boohoo. It could have all sorts of side effects, including for search engine optimisation, if that’s your thing.

Another way to hack around it, is to have a meta refresh tag, as Daniel Buchner suggests over at Smashing Magazine.

Let’s hope GitHub Pages will one day add functionality like that of Vercel and Netlify.

Wrapping up

Routing was easier when we used servers to decide what to serve. Moving routing logic to the client has some advantages, but comes with problems of its own. The best way to ‘do’ a single page app, is to have a server-side fallback. so that servers can just do the serving as they have always done. Generate static files, that servers can find. Don’t make it our user’s problem that we wanted to use a fancy client-side router. Now, should we not have a fallback for our client-side routes, redirect rules can help, like the one’s Vercel and Netlify recommend. On GitHub Pages, we can hack around it.

Comments & mentions (3)

Johan Ronsse replied: Hmm yeah I have struggled with this. You can configure Apache to redirect to index.html. Some of our stuff is on S3 and have yet to find a reliable solution there (server is nginx). I stopped using Github pages when they just straight up broke my old Jekyll websites.
Matthias Ott likes this
Hidde replied:
Good to hear it happens to the best, thought I may have been the only one struggling. BTW, I am terrible with .htaccess syntax (re Apache), if you happen to know how to do it there, I'll add it to the post! :-)
Leave a comment
Posted a response to this?

This website uses Webmentions. You can manually notify me if you have posted a response, by entering the URL below.