Originally posted here: https://markus-kottlaender.medium.com/github-pages-with-dynamic-routes-40f512900efa
GitHub Pages is a super convenient hosting service for static sites, e.g. a personal portfolio or blog or a project’s documentation and even modern web apps are in many cases not much more than a static HTML file and (a lot of) Javascript. But static sites come with the downside of... well... being static. That means you can’t have dynamic routes, like your-project.github.io/posts/<post-slug>
where <post-slug>
is a dynamic parameter. All possible routes need to be known in advance and point to a static file. Maybe those files are generated by some build process and whenever you add a new blog post, you just re-deploy the page. Using CI/CD pipelines like GitHub Actions/Workflows, that process might even boil down to pushing a new markdown file to your repository and that is sufficiently convenient for a lot of scenarios. But sometimes it’s not and you just need dynamic paths, especially when user generated content is involved or a project becomes more complex.
How do dynamic routes even work?
If you are well familiar with the concept and you just want to know how to trick GitHub Pages into supporting dynamic routes, you can skip this part and continue with The Solution.
A route/path is traditionally pointing to a (static) file on the server that is represented by a domain.
your-server.com/some/path/index.html
If you try to access a file that does not exist on that server, it will respond with an error, which usually means it will serve you a default 404.html
that comes with the server. You’ve probably seen something like this:
That’s such a default error file, in this case served by an nginx server. However, you can configure a server in a way that it serves you a certain resource/file, no matter what path you request. Let’s say you have an index.html
on the server and you configured it accordingly. You can now call your-server.com/index.html
but also your-server.com/some/path/that/does-not-exist.html
. It will always return the same index.html
file. Now, that file can also be a script instead of just some static HTML file. Otherwise your dynamic routes wouldn’t be that dynamic since they all serve exactly the same content.
A front controller is such a dynamic script that handles all requests to your server and serves content dynamically, e.g. by fetching data from a database, based on what the actual request was, and then generating an HTML response from a template. This way you can support routes with dynamic parts, that you don’t have to know in advance, like the /posts/<post-slug>
example from above.
GitHub Pages
GitHub Pages does not support such a front controller because it is not meant to serve dynamic content. Sure. You can use Javascript in your static HTML files to change its content dynamically, e.g. based on user interaction, and most web apps, as mentioned before, are nothing more than a static HTML file and then Javascript takes over from there. But all this happens in your browser and not on GitHub’s servers. So if you call your-username.github.io/some/file.html
it will look for exactly that file and nothing else and if it can’t find it, because you didn’t add it to your repository, it will show you this:
That is GitHub’s default 404.html
file. At this point I assume that most developers/users would now simply accept the bitter reality that GitHub Pages might not be the right service for them and instead move on to a more comprehensive hosting solution, where they have more control over what the server actually does behind the scenes. But not me! I’m a lazy minimalist and one platform account must be enough!
An old-fashioned alternative
At first I considered just being fine with a compromise and instead of having “real” dynamic routes, I could go back in the history of single page apps and use the #
method, like the old AngularJS. In case you ever wondered, the part after the #
is not really part of the actual URL a server responds to. It is just used by the browser to jump to an HTML anchor and you can access it in Javascript. The server does not even know about this part. It’s client-side only. But that means you can have routes that look like this:
your-server.com/#/posts/<post-id>
The app lives at /
on the server and processes the part after #
when running in the browser. When clicking a link in your app, it just updates the part after #
and changes the content accordingly via Javascript. But that doesn’t look that nice and modern frameworks, like Next.js do not even support this form of routing anymore. Vue’s Nuxt.js actually has a fallback option but still...
The Solution
You already saw part of the solution in this post. It’s the 404 page. GitHub Pages actually allows you to add a custom 404.html
to your repository, to adjust it to your project’s branding and so on. If you are familiar with the front controller pattern, you might have an “aahhhhhaaaa” moment now. The important part of this pattern is that it just takes any request and routes it to your app where the request is then handled. Well.. a 404 page is not much different. It handles aaaaall requests... that do not match any existing resource. You know where I am going with this? It is a bit different though. A classical front controller lives on the server and sends the desired response back to you as if the resource you requested actually exists physically. Tricking GitHub Pages into supporting dynamic routes is a bit more... tricky. Because it simply doesn’t! But we can make it look as if it does. The average human eye won’t notice the difference and it even works with the dynamic routing features of modern frameworks like Vue’s Nuxt.js or React’s Next.js.
Proof of concept
The simple trick is to let your custom 404.html
redirect any request back to your app and then your app uses the browser history API to update the URL that your browser shows, to whatever was requested originally. You need to pass that information to your app when redirecting. For that I use... guess what?... our old friend, the #
. I have a GitHub page set up here: https://mktcode.github.io/static-dynamic-routing and its 404.html
looks like this:
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body></body>
<script>
window.location.href = '/static-dynamic-routing/#' + window.location.pathname.replace('/static-dynamic-routing', '')
</script>
</html>
So instead of showing some 404 Not found! message, it just redirects you to the root path where the app lives. Note that this GitHub Page example lives in the subdirectory /static-dynamic-routing/
which is normal, when you set up a GitHub page for a repository. It will live under <your-username>.github.io/<repo-name>/
. That’s why we have to do some replacements here. Otherwise we’d redirect the user to mktcode.github.io/
. Fortunately you can configure a custom domain for your GitHub Page very easily and then you don’t have to take care of this.
So now, no matter what route we call, we’ll end up at our app and it will know about that route, so it can act accordingly. In my little example I do not much more than replacing the displayed URL in the address bar and manipulating some content. That’s basically how dynamic routing works in those modern frameworks.
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
</head>
<body>
<h1>My App</h1>
<h2 id="header">Post Path: {PATH}</h2>
</body>
<script>
if (window.location.hash.length > 1) {
const path = window.location.hash.replace('#', '')
history.pushState({ page: 1 }, "Some title", '/static-dynamic-routing' + path)
document.getElementById('header').innerHTML = document.getElementById('header').innerHTML.replace('{PATH}', path)
}
</script>
</html>
Try opening this link in your browser: https://mktcode.github.io/static-dynamic-routing/posts/my-post
For a split second you can see the #
in your browser’s address bar. That’s when the redirect happens and then we pretend it never did happen. And that’s basically all there is to it.
Use with Nuxt.js
If you are more the react type of person, you’ll have to implement that on your own. I’ll only show the Nuxt.js way.
In Nuxt.js you can easily configure dynamic routes by just creating a file like /pages/posts/_slug.vue
. Nuxt will do the rest and you have routes like /posts/my-post-title
. This even works in static site mode but only if the site is delivered by the integrated Nuxt server or any other server configured in the same way (think: front controller pattern). With GitHub Pages this does not work and you’ll just see the 404 page. But here’s the proof that my approach works totally fine even with Nuxt.js:
https://mktcode.github.io/dynamic-nuxt-gh-pages/post/my-totally-dynamic-post-title
All it needs is the 404.html
file in the static
directory and a router middleware, which performs a nuxt-internal redirect to the original route, resulting in the address bar of your browser being updated. If that route does not exist in your application, the Nuxt error page shows. By the way... It now uses #!
for the redirect, to still allow normal HTML anchors. Everything that worked before should still work, plus... dynamic routes for GitHub Pages! Well... kind of. :)
The End
Hope you enjoyed my first ever article! :) Follow me on Twitter and GitHub and comment and bla bla bla. There’s more to come! Five years later... “Hey I think I’ll start writing dev articles!” :D
Awesome hack, thank you!! Definitely going to see about implementing this in a app I'm workin on, 100% amazing!!
Thank you! Indeed it is pretty "hacky". Especially SEO-wise this is not really optimal. But as long as GItHub does not offer a native solution, it's the best you can get.
Btw... I just published my next article: https://markus-kottlaender.medium.com/when-youre-working-on-a-static-site-and-github-pages-feels-like-the-perfect-hosting-solution-a41c37f4e326
Will repost that here in a bit.
I played the SEO game for awhile back in the early 2000's.. and to me, it's just not worth it. good products/projects will shine through, word of mouth is far more useful then SEO. But maybe I'm wrong. LoL. Organic over advert IMO.
100% agreed!