From README to documentation site in 10 minutes

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:

  1. The site should be generated from the README. Zod's README.md is the "source of truth".
  2. The site should automatically update whenever the master branch is updated.
  3. I didn't want to worry about building or maintaining a complex build workflow in GitHub Actions, if possible.

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.

1. Add this index.html to your repo

Create 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 detailed meta 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.

Docsify sample homepage

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.

98 performance on Lighthouse

2. Run locally

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 the homepage setting in the Docsify configuration.

window.$docsify = {
  subMaxLevel: 1,
  maxLevel: 3,
  auto2top: true,
  repo: 'user/repo',
  routerMode: 'history',
  homepage: 'home.md', // <-- add this
};

3. Configure rewrites

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 serving index.html to the Googlebot when it tries to read your robots.txt. Alternatively, you could just add a robots.txt file to your repo alongside index.html.

Now when we run npx netlify dev, we can directly open localhost:8888/CONTRIBUTING without seeing a 404.

4. Deployment

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.

  1. Log in or create an account at netlify.com.
  2. From the dashboard, click Add new site > Import an existing project.
  3. Authenticate with GitHub (or GitLab or Bitbucket) and select your project's repo

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.

5. Final touches

There's plenty more to do before your site is truly ready.

But I'll leave that as an exercise for the reader.

Hacker News TL;DR Podcast

Colin is a YC-backed founder and open-source developer. Kenny is a human-computer interaction whiz at Apple. Two best friends report daily from the depths of Hacker News.