Creating a commit with multiple files to Github with JS on the web

My site is entirely static. It's built with Hugo and hosted with Zeit. I'm pretty happy with the setup, I get near instant builds and super fast CDN'd content delivery and I can do all the things that I need to because I don't have to manage any state.

I've created a simple UI for this site and also my podcast creator that enables me to quickly post new content to my statically hosted site.

So. How did I do it?

It's a combination of Firebase Auth against my Github Repo, EditorJS to create edit the content (it's neat) and Octokat.js to commit to the repo and then Zeit's Github integration to do my hugo build. With this set up, I am able to have an entirely self hosted static CMS, similar to how a user might create posts in a database backed CMS like Wordpress.

In this post I am just going to focus on one part of the infrastructure - committing multiple files to Github because it took me a little while to work out.

The entire code can be seen on my repo.

If you are building a Web UI that needs to commit directly to Github, the best library that I have found is Octokat - it works with CORS and it seems to handle the entire API surface of the Github API.

Git can be a complex beast when it comes to understanding how the tree, branches and other pieces work so I took some decisions that made it easier.

  1. I will be only able to push to the master branch known as heads/master.
  2. I will know where certain files will be stored (Hugo forces me to have a specific directory structure)

With that in mind, the general process to creating a commit with multiple files is as follows:

Get a reference to the repo.

  1. Get a reference to the tip of the tree on heads/master branch.
  2. For each file that we want to commit create a blob and then store the references to the sha identifier, path, mode in an array.
  3. Create a new tree that contains all the blobs to add to the reference to the tip of the heads/master tree, and store the new sha pointer to this tree.
  4. Create a commit that points to this new tree and then push to the heads/master branch.

The code pretty much follows that flow. Because I can assume the path structure for certain inputs I don't need to build any complex UI or management for the files.

const createCommit = async (repositoryUrl, filename, data, images, commitMessage, recording) => {
  try {
    const token = localStorage.getItem('accessToken');
    const github = new Octokat({ 'token': token });
    const [user, repoName] = repositoryUrl.split('/');

    if(user === null || repoName === null) {
      alert('Please specifiy a repo');
      return;
    }
    
    const markdownPath = `site/content/${filename}.markdown`.toLowerCase();
    let repo = await github.repos(user, repoName).fetch();
    let main = await repo.git.refs('heads/master').fetch();
    let treeItems = [];

    for(let image of images) {
      let imageGit = await repo.git.blobs.create({ content: image.data, encoding: 'base64' });
      let imagePath = `site/static/images/${image.name}`.toLowerCase();
      treeItems.push({
        path: imagePath,
        sha: imageGit.sha,
        mode: "100644",
        type: "blob"
        });
    }

    if (recording) {
      let audioGit = await repo.git.blobs.create({ content: recording.data, encoding: 'base64' });
      let audioPath = `site/static/audio/${recording.name}.${recording.extension}`.toLowerCase();
      treeItems.push({
        path: audioPath,
        sha: audioGit.sha,
        mode: "100644",
        type: "blob"
        });
    }

    let markdownFile = await repo.git.blobs.create({ content: btoa(jsonEncode(data)), encoding: 'base64' });
    treeItems.push({
      path: markdownPath,
      sha: markdownFile.sha,
      mode: "100644",
      type: "blob"
    });

    let tree = await repo.git.trees.create({
      tree: treeItems,
      base_tree: main.object.sha
    });
  
    let commit = await repo.git.commits.create({
      message: `Created via Web - ${commitMessage}`,
      tree: tree.sha,
      parents: [main.object.sha]});

    main.update({sha: commit.sha})

    logToToast('Posted');
  } catch (err) {
    console.error(err);
    logToToast(err);
  }
}

Let me know if you've done anything similar with static hosting. I'm very excited that I can build a modern frontend for what is an entirely server-less hosting infrastructure.

What about Zeit?

Well, it's just kinda all automatic now. I use the static-builder to run the hugo command and that is pretty much it. :)

I lead the Chrome Developer Relations team at Google.

We want people to have the best experience possible on the web without having to install a native app or produce content in a walled garden.

Our team tries to make it easier for developers to build on the web by supporting every Chrome release, creating great content to support developers on web.dev, contributing to MDN, helping to improve browser compatibility, and some of the best developer tools like Lighthouse, Workbox, Squoosh to name just a few.

I love to learn about what you are building, and how I can help with Chrome or Web development in general, so if you want to chat with me directly, please feel free to book a consultation.

I'm trialing a newsletter, you can subscribe below (thank you!)