Colin McDonnell @colinhacks
published May 12th, 2022
Spoiler alert: GitHub Pages and GitHub actions are not involved.
I recently set out to build a proper documentation site for Zod with a short and eminently reasonable list of goals:
README.md
is the "source of truth".master
branch is updated.Despite the eminent reasonability, I had a hard time finding a simple solution. After a lot of research and false starts, I've found what I think is the lowest-effort, highest-reward way to publish a documentation site for your GitHub-hosted project.
Let's jump in.
index.html
to your repoCreate a file called index.html
in your project root and paste the following contents into it.
Replace all occurrences of
user/repo
with your project's name. And maybe write up a slightly more detailedmeta
description :)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>user/repo</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="description" content="user/repo is cool!" />
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" />
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify/lib/themes/vue.css" />
<style>
.markdown-section { max-width: 700px; }
</style>
</head>
<body>
<nav
style="display: flex; flex-direction: row; align-items: center; justify-content: space-between;"
>
<!-- <ul><li href="/link">Link 1</li></ul> -->
</nav>
<div id="app"></div>
<script>
window.$docsify = {
subMaxLevel: 1,
maxLevel: 3,
auto2top: true,
repo: 'user/repo',
routerMode: 'history',
// homepage: "README.md"
};
</script>
<script src="//cdn.jsdelivr.net/npm/docsify@4.12.2/lib/docsify.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/prismjs@1.28.0/prism.min.js"></script>
</body>
</html>
Those 1150 bytes are doing a lot, but most importantly it's loading Docsify. Docsify is a documentation generator. You may recognize it from its goofy looking gradient cover pages with big pill-shaped buttons.
I'm not a fan of these cover pages, so we won't be using one here. All in all, though, I think Docsify is extraordinarily well designed.
It also differs from some other options like Docusaurus and Gitbook in that it's a fully client-side documentation generator. Your Markdown files don't get pre-compiled to HTML ahead of time; instead, Docsify fetches it asynchronously, parses the contents in-browser, and injects the rendered HTML into the page.
"The horror! Multiple round-trip requests before the first meaningful paint!?" Turns out, it's not a big issue. Modern globe-spanning ultra-fast CDNs for static content, this still feels near-instant. Google agrees; the Zod site gets a 98 Performance score on Lighthouse.
There's no build step required with Docsify. You could deploy your index.html
and a README.md
to any static site hosting service and it would work. We're going to be using Netlify to deploy this site, but before we get to that, let's see how the site is looking. We can use Netlify's CLI to start a simple static site server.
$ npx netlify dev
◈ Netlify Dev ◈
┌─────────────────────────────────────────────────┐
│ │
│ ◈ Server now ready on http://localhost:8888 │
│ │
└─────────────────────────────────────────────────┘
Open http://localhost:8888
in your browser, and you should see a rendered version of your README.md
!
If
netlify dev
detects that you're using a framework, this may not work. In that case you'll need to disable framework detection and tell Netlify to act as a simple static file host.npx netlify dev --framework "#static"
If your primary Markdown file isn't called
README.md
, you can specify a different name with thehomepage
setting in the Docsify configuration.window.$docsify = { subMaxLevel: 1, maxLevel: 3, auto2top: true, repo: 'user/repo', routerMode: 'history', homepage: 'home.md', // <-- add this };
If your project/site contains several Markdown files, you may encounter a strange behavior. Consider the following file structure:
├── index.html
├── README.md
└── CONTRIBUTING.md
Let's say README.md
contains a link with href="/CONTRIBUTING"
. If you open localhost:8888
and click that link, it'll work: the browser's URL bar will change to localhost:8888/CONTRIBUTING
and the contents of CONTRIBUTING.md
will be rendered. Then, if you refresh the page, you'll see a 404.
This happens because Docsify is using the History API to change the browser URL programmatically, without actually reloading the page. This is also how things like React Router handle client-side routing in single-page applications. A fresh reload causes a 404 because the client-side URL doesn't correspond to an actual file on the server.
This is avoidable by using a hash router. In fact, this is the default behavior for Docsify. Unfortunately, it complicates the URL; the URL would be domain.com/#/CONTRIBUTING
instead of simply domain.com/CONTRIBUTING
.
I strongly prefer clean URLs, though so I chose use routerMode: 'history'
and solve the 404 problem with a Netlify feature called rewrites! Create a file called _redirects
with no extension. (With Netlify, both redirects and rewrites are declared in _redirects
.)
$ touch _redirects
Then paste in the following:
/robots.txt /robots.txt 404
/* / 200
This tells Netlify that all incoming requests (/*
) should render the homepage (/
). However, unlike a redirect, the URL should stay the same. Basically Netlify will sneakily return index.html
under the hood, without telling the browser.
The
robots.txt
line prevents your site from servingindex.html
to the Googlebot when it tries to read yourrobots.txt
. Alternatively, you could just add arobots.txt
file to your repo alongsideindex.html
.
Now when we run npx netlify dev
, we can directly open localhost:8888/CONTRIBUTING
without seeing a 404.
Before we deploy, let's push our changes to master
.
$ git add .
$ git commit -am "Add docs site"
$ git push origin master
Firebase used to be my go-to for static file hosting, but these days Netlify leads the pack on developer experience. It only takes a couple minutes to set up Github-linked static site hosting.
Add new site > Import an existing project
.You should now see a configuration page.
"Branch to deploy"
— master
(the default)"Base directory"
— leave blank (defaults to the project root)"Build command"
— leave blank (no build step necessary!)"Publish directory"
— leave blank (defaults to the project root)There is virtually no configuration required, because we're simply hosting static files from the root directory of our master
branch. Couldn't be easier!
Note that we're deploying the entire contents of our repository to Netlify. You could write a build command to delete all unwanted files but I didn't bother. The contents of my repo are already public on the internet, so...🤷♂️
Proceed with the deployment. Once Netlify finishes deploying, Netlify will provide a *.netlify.app
subdomain for your site.
There's plenty more to do before your site is truly ready.
<nav>
in index.html
But I'll leave that as an exercise for the reader.