This content originally appeared on Modern Web Development with Chrome and was authored by Paul Kinlan
<p>My site is <a href="https://github.com/PaulKinlan/paul.kinlan.me">entirely static</a>. It's built with <a href="https://gohugo.io">Hugo</a> and hosted with <a href="https://zeit.co">Zeit</a>. 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.</p>
<p>I've created a <a href="https://github.com/PaulKinlan/paul.kinlan.me/tree/main/static/share/image">simple UI</a> for this site and also my <a href="https://github.com/PaulKinlan/podcastinabox-editor">podcast creator</a> that enables me to quickly post new content to my statically hosted site.</p>
<figure><img src="https://paul.kinlan.me/images/2019-05-24-creating-a-commit-with-multiple-files-to-github-with-js-on-the-web-0.jpeg"></figure>
<p>So. How did I do it?</p>
<p>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.</p>
<p>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.</p>
<p>The entire code can be seen on my <a href="https://github.com/PaulKinlan/podcastinabox-editor/blob/master/record/javascripts/main.mjs#L90">repo</a>.</p>
<p>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.</p>
<p>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.</p>
<ol>
<li>I will be only able to push to the master branch known as <code>heads/master</code>.</li>
<li>I will know where certain files will be stored (Hugo forces me to have a specific directory structure)</li>
</ol>
<p>With that in mind, the general process to creating a commit with multiple files is as follows:</p>
<p>Get a reference to the repo.</p>
<ol>
<li>Get a reference to the tip of the tree on <code>heads/master</code> branch.</li>
<li>For each file that we want to commit create a <code>blob</code> and then store the references to the <code>sha</code> identifier, path, mode in an array.</li>
<li>Create a new <code>tree</code> that contains all the blobs to add to the reference to the tip of the <code>heads/master</code> tree, and store the new <code>sha</code> pointer to this tree.</li>
<li>Create a commit that points to this new tree and then push to the <code>heads/master</code> branch.</li>
</ol>
<p>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.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-JavaScript" data-lang="JavaScript"><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">createCommit</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">async</span> (<span style="color:#a6e22e">repositoryUrl</span>, <span style="color:#a6e22e">filename</span>, <span style="color:#a6e22e">data</span>, <span style="color:#a6e22e">images</span>, <span style="color:#a6e22e">commitMessage</span>, <span style="color:#a6e22e">recording</span>) => {
<span style="color:#66d9ef">try</span> {
<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">token</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">localStorage</span>.<span style="color:#a6e22e">getItem</span>(<span style="color:#e6db74">'accessToken'</span>);
<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">github</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Octokat</span>({ <span style="color:#e6db74">'token'</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">token</span> });
<span style="color:#66d9ef">const</span> [<span style="color:#a6e22e">user</span>, <span style="color:#a6e22e">repoName</span>] <span style="color:#f92672">=</span> <span style="color:#a6e22e">repositoryUrl</span>.<span style="color:#a6e22e">split</span>(<span style="color:#e6db74">'/'</span>);
<span style="color:#66d9ef">if</span>(<span style="color:#a6e22e">user</span> <span style="color:#f92672">===</span> <span style="color:#66d9ef">null</span> <span style="color:#f92672">||</span> <span style="color:#a6e22e">repoName</span> <span style="color:#f92672">===</span> <span style="color:#66d9ef">null</span>) {
<span style="color:#a6e22e">alert</span>(<span style="color:#e6db74">'Please specifiy a repo'</span>);
<span style="color:#66d9ef">return</span>;
}
<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">markdownPath</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">`site/content/</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">filename</span><span style="color:#e6db74">}</span><span style="color:#e6db74">.markdown`</span>.<span style="color:#a6e22e">toLowerCase</span>();
<span style="color:#66d9ef">let</span> <span style="color:#a6e22e">repo</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">await</span> <span style="color:#a6e22e">github</span>.<span style="color:#a6e22e">repos</span>(<span style="color:#a6e22e">user</span>, <span style="color:#a6e22e">repoName</span>).<span style="color:#a6e22e">fetch</span>();
<span style="color:#66d9ef">let</span> <span style="color:#a6e22e">main</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">await</span> <span style="color:#a6e22e">repo</span>.<span style="color:#a6e22e">git</span>.<span style="color:#a6e22e">refs</span>(<span style="color:#e6db74">'heads/master'</span>).<span style="color:#a6e22e">fetch</span>();
<span style="color:#66d9ef">let</span> <span style="color:#a6e22e">treeItems</span> <span style="color:#f92672">=</span> [];
<span style="color:#66d9ef">for</span>(<span style="color:#66d9ef">let</span> <span style="color:#a6e22e">image</span> <span style="color:#66d9ef">of</span> <span style="color:#a6e22e">images</span>) {
<span style="color:#66d9ef">let</span> <span style="color:#a6e22e">imageGit</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">await</span> <span style="color:#a6e22e">repo</span>.<span style="color:#a6e22e">git</span>.<span style="color:#a6e22e">blobs</span>.<span style="color:#a6e22e">create</span>({ <span style="color:#a6e22e">content</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">image</span>.<span style="color:#a6e22e">data</span>, <span style="color:#a6e22e">encoding</span><span style="color:#f92672">:</span> <span style="color:#e6db74">'base64'</span> });
<span style="color:#66d9ef">let</span> <span style="color:#a6e22e">imagePath</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">`site/static/images/</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">image</span>.<span style="color:#a6e22e">name</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>.<span style="color:#a6e22e">toLowerCase</span>();
<span style="color:#a6e22e">treeItems</span>.<span style="color:#a6e22e">push</span>({
<span style="color:#a6e22e">path</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">imagePath</span>,
<span style="color:#a6e22e">sha</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">imageGit</span>.<span style="color:#a6e22e">sha</span>,
<span style="color:#a6e22e">mode</span><span style="color:#f92672">:</span> <span style="color:#e6db74">"100644"</span>,
<span style="color:#a6e22e">type</span><span style="color:#f92672">:</span> <span style="color:#e6db74">"blob"</span>
});
}
<span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">recording</span>) {
<span style="color:#66d9ef">let</span> <span style="color:#a6e22e">audioGit</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">await</span> <span style="color:#a6e22e">repo</span>.<span style="color:#a6e22e">git</span>.<span style="color:#a6e22e">blobs</span>.<span style="color:#a6e22e">create</span>({ <span style="color:#a6e22e">content</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">recording</span>.<span style="color:#a6e22e">data</span>, <span style="color:#a6e22e">encoding</span><span style="color:#f92672">:</span> <span style="color:#e6db74">'base64'</span> });
<span style="color:#66d9ef">let</span> <span style="color:#a6e22e">audioPath</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">`site/static/audio/</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">recording</span>.<span style="color:#a6e22e">name</span><span style="color:#e6db74">}</span><span style="color:#e6db74">.</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">recording</span>.<span style="color:#a6e22e">extension</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>.<span style="color:#a6e22e">toLowerCase</span>();
<span style="color:#a6e22e">treeItems</span>.<span style="color:#a6e22e">push</span>({
<span style="color:#a6e22e">path</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">audioPath</span>,
<span style="color:#a6e22e">sha</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">audioGit</span>.<span style="color:#a6e22e">sha</span>,
<span style="color:#a6e22e">mode</span><span style="color:#f92672">:</span> <span style="color:#e6db74">"100644"</span>,
<span style="color:#a6e22e">type</span><span style="color:#f92672">:</span> <span style="color:#e6db74">"blob"</span>
});
}
<span style="color:#66d9ef">let</span> <span style="color:#a6e22e">markdownFile</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">await</span> <span style="color:#a6e22e">repo</span>.<span style="color:#a6e22e">git</span>.<span style="color:#a6e22e">blobs</span>.<span style="color:#a6e22e">create</span>({ <span style="color:#a6e22e">content</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">btoa</span>(<span style="color:#a6e22e">jsonEncode</span>(<span style="color:#a6e22e">data</span>)), <span style="color:#a6e22e">encoding</span><span style="color:#f92672">:</span> <span style="color:#e6db74">'base64'</span> });
<span style="color:#a6e22e">treeItems</span>.<span style="color:#a6e22e">push</span>({
<span style="color:#a6e22e">path</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">markdownPath</span>,
<span style="color:#a6e22e">sha</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">markdownFile</span>.<span style="color:#a6e22e">sha</span>,
<span style="color:#a6e22e">mode</span><span style="color:#f92672">:</span> <span style="color:#e6db74">"100644"</span>,
<span style="color:#a6e22e">type</span><span style="color:#f92672">:</span> <span style="color:#e6db74">"blob"</span>
});
<span style="color:#66d9ef">let</span> <span style="color:#a6e22e">tree</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">await</span> <span style="color:#a6e22e">repo</span>.<span style="color:#a6e22e">git</span>.<span style="color:#a6e22e">trees</span>.<span style="color:#a6e22e">create</span>({
<span style="color:#a6e22e">tree</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">treeItems</span>,
<span style="color:#a6e22e">base_tree</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">main</span>.<span style="color:#a6e22e">object</span>.<span style="color:#a6e22e">sha</span>
});
<span style="color:#66d9ef">let</span> <span style="color:#a6e22e">commit</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">await</span> <span style="color:#a6e22e">repo</span>.<span style="color:#a6e22e">git</span>.<span style="color:#a6e22e">commits</span>.<span style="color:#a6e22e">create</span>({
<span style="color:#a6e22e">message</span><span style="color:#f92672">:</span> <span style="color:#e6db74">`Created via Web - </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">commitMessage</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>,
<span style="color:#a6e22e">tree</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">tree</span>.<span style="color:#a6e22e">sha</span>,
<span style="color:#a6e22e">parents</span><span style="color:#f92672">:</span> [<span style="color:#a6e22e">main</span>.<span style="color:#a6e22e">object</span>.<span style="color:#a6e22e">sha</span>]});
<span style="color:#a6e22e">main</span>.<span style="color:#a6e22e">update</span>({<span style="color:#a6e22e">sha</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">commit</span>.<span style="color:#a6e22e">sha</span>})
<span style="color:#a6e22e">logToToast</span>(<span style="color:#e6db74">'Posted'</span>);
} <span style="color:#66d9ef">catch</span> (<span style="color:#a6e22e">err</span>) {
<span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">error</span>(<span style="color:#a6e22e">err</span>);
<span style="color:#a6e22e">logToToast</span>(<span style="color:#a6e22e">err</span>);
}
}
</code></pre></div><p>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.</p>
<p>What about Zeit?</p>
<p>Well, it's just kinda all automatic now. I use the <code>static-builder</code> to run the hugo command and that is pretty much it. :)</p>
This content originally appeared on Modern Web Development with Chrome and was authored by Paul Kinlan