One of the golden rules I try to follow in web development is “don’t break URLs”. That is, even if pages get moved around, or a site is completely revamped, the old URLs should still redirect somewhere. It’s a terrible experience to bookmark a page or follow a link from another site, only to find that the page has completely vanished. Let’s try not to make the problem of link rot even worse.
The best way to redirect old pages is to get the web server to send a 301 Moved Permanently HTTP header. This has the benefit that search engines will see the 301 header and automatically update their caches to reference the new URL. Where this can fall down is when using static site hosting such as GitHub Pages or Surge.sh. By design, you’re not allowed to send any web server directives such as custom HTTP headers, so the 301 redirect option is out.
So what do you do when you want to move some URLs around when using GitHub Pages? Here are the options I found.
Moving a page within the same repository
Let’s say you have a repository with a
gh-pages branch (or equivalent publishing branch) and you want to move/rename a page within that branch. For example, going from
/newurl.html. There are two options for keeping the old URL alive as a redirect, as helpfully described by GitHub’s documentation.
Option 1: Running with Jekyll
If you’re using Jekyll for static site publishing, GitHub allows you to install the jekyll-redirect-from plugin. In your new page’s metadata, you specify which URLs it will redirect from. This will generate an HTML page at the old URL that simply contains a redirect
<meta> element, pointing to the new URL. I haven’t used Jekyll much, so I can’t tell you much more than the documentation.
Option 2: Hyding from Jekyll
For everything else, you can manually create an HTML page that does the equivalent of the Jekyll plugin.
- First, move the file to the new location in git and commit that, so that the git history of the file remains intact:
git mv oldurl.html newurl.html; git commit
- Create a new HTML file at the old location, containing the important meta redirect definition:
<meta http-equiv="refresh" content="0; url=/newurl.html">
- Commit the new file, push both commits, and you now have an old-to-new redirect in place.
Waaaaaait a second, redirect meta tags ⁉️
Errr, yep. Even though the W3C recommends that redirect meta tags are not used, they’re pretty much the only option for this scenario. This is one of the big downsides of static site hosting. However, today’s browsers optimise those redirects pretty well. For accessibility purposes (and as a back-up if the browser fails to auto-redirect) it’s still best to include some text explaining the redirect, with a link to the new page. This is the boilerplate I’ve used previously:
<!DOCTYPE html> <html> <head> <meta charset="utf8"> <meta http-equiv="refresh" content="0; url=https://newurl/"> <link rel="canonical" href="https://newurl/"> <title>This page has moved</title> </head> <body> <p>This page has moved. Redirecting you to <a href="https://newurl/">https://newurl/</a>…</p> </body> </html>
I’ve also got a canonical link in there as well. Even though search engines can usually understand the refresh meta instruction, there’s no guarantee that they’ll update their caches to point to the new URL. Adding a canonical link definition gives an extra hint to update to the new page.
Renaming an entire repository
GitHub allows you to rename a git repository and will automatically redirect references from the old name to the new one. This works for links to the web UI, API requests, git command line operations… but not GitHub Pages. Which means that if you have your documentation publicly published at
https://your-username.github.io/old-repo-name/ and you rename the repository, none of the publicly cached references to your docs will redirect to the equivalent page at
Note: If you’re using a custom domain name (e.g.
my-superb-docs.com), this should be handled without your intervention. The public URL doesn’t change at all, just the source location of the files. For everything else, however, there’s a workaround available.
GitHub allows you to create public websites in two ways. One has already been featured above, which is having a specific part of your repo (either a branch or directory) hooked up to the publishing feature. This gives you a URL of
https://your-username.github.io/repo-name/ – where the repo name will always be a subdirectory of the top-level domain.
The other way is using a repository called
your-username.github.io. Anything that’s pushed to the
master branch in this repo will be visible at (surprise, surprise)
https://your-username.github.io/, including directories. So you can publish a subdirectory to a top-level repo, or publish to the root of a different repo, and they’ll both be visible as subdirectories of the public domain.
But what about name conflicts?
- Pushing a directory called
masterbranch of the
your-username.github.iorepo will give you a URL of
- Pushing the root files to the
gh-pagesbranch of the
some-projectrepository will also give you a URL of
Through some quick testing, I found that the
gh-pages branch (or equivalent) of the project repository will always override the subdirectory of the parent repo (
your-username.github.io). This is what can use to our advantage, by treating the parent repo as fallback content when renaming the project repo.
Step by step, mile by mile
To give a solid example, this is the scenario I encountered recently. I had a presentation slide deck that I’d done for a local meetup (SydCSS), with the repo name
sydcss-preso-gradient-circus. Then I gave that presentation for a mini-conference (Decompress) – I wanted to publish both sets of slides (as the content differed) at a more generic URL, while still having the old URL redirect to the new one. So
https://gilmoreorless.github.io/sydcss-preso-gradient-circus/ would redirect to
https://gilmoreorless.github.io/preso-gradient-circus/sydcss/. These are the steps I took:
gilmoreorless.github.iorepo. It contained the meta redirect example shown earlier, pointing to the new URL of the SydCSS version of the presentation.
- Copied all the files in the
gh-pagesbranch of the
sydcss-preso-gradient-circusrepo to a
sydcsssubdirectory within the same repo. This meant the root page
/and the subdirectory
/sydcss/both had the same content.
- Published the Decompress version of the presentation to the
decompresssubdirectory of the same repo.
- Renamed the repo in the GitHub web interface, from
preso-gradient-circus. At this point, any requests to
https://gilmoreorless.github.io/sydcss-preso-gradient-circus/would hit the fallback page created in step 1. That would then redirect them to the new URL of
- With the redirect in place, I changed the
preso-gradient-circusrepo to just be a simple list of links to the different versions of the presentation.
In summary, the situation right now is:
- https://gilmoreorless.github.io/sydcss-preso-gradient-circus/ is served from the
gilmoreorless.github.iorepo with a redirect to the next link.
- https://gilmoreorless.github.io/preso-gradient-circus/sydcss/ is served from the
- https://gilmoreorless.github.io/preso-gradient-circus/decompress/ is served from the
It seems like a lot of effort when it’s written down like that, but it only took a few minutes to do the process. It took me far longer to write this post than to set up the redirects (including the testing of fallback content). I think it was worth it; even though it was just a slide deck, I care about keeping URLs working wherever possible.
Also, this post is mainly serving as documentation for Future Me in case I ever need to do it again. Anyone else getting a benefit from it is merely a bonus.