Reading List
How deployment services make client-side routing work from hiddedevries.nl Blog RSS feed.
How deployment services make client-side routing work
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.
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.
Originally posted as How deployment services make client-side routing work on Hidde's blog.