This content originally appeared on Modern Web Development with Chrome and was authored by Paul Kinlan
<p>My blog is an entirely static site, built with Hugo and hosted with Zeit. This is a great solution for me, a simple blog has a pretty simple deployment process and it loads blazingly fast.</p>
<p>Statically generated sites do have some drawbacks, the largest is when you need anything dynamic to be integrated into your page (comments for example). Not being able to easily host dynamic content will mean that you end up relying on 3rd party JavaScript that will then get full access to your page and you won't know what it is doing - it could be tracking your users or slowing down your page load.</p>
<p>I recently took my current comment widget (Disqus) off the critical render path by only loading it when the user scrolls to the comments (using <code>IntersectionObserver</code>) and whilst this was a reasonable solution to the load performance and tracking problems, I actually wanted to remove Disqus all together.</p>
<p>Enter the <a href="https://webmention.net/draft/">Webmention</a> spec. Webmention is a specification that describes how a site author can be contacted when another site 'mentions' (or likes) content on your site. This ultimately allows for a decentralised method for discovering content that links to your site, hopefully providing value and insight.</p>
<p>The webmention spec does not describe any data formats that should be used for communicating what the 'mentioning site' has said, that is left up you to parse using standard microformats or other mechanisms to understand the content of the page. This is great, however I believe that it leads to centralised services such as <a href="https://webmention.io/">webmention.io</a> providing the much needed infrastructure to get the meaning out of the page.</p>
<p>I liked the idea of using Webmention, but it requires a server side setup to get (and possibly store) notifications of when someone mentions your site, this is not always possible with a static builder like I have on my site. The rest of this post will quickly describe how I got likes, mentions and reposts hosted on my Zeit hosted Hugo build.</p>
<h3 id="step-one---find-a-webmention-hub">Step one - find a webmention hub</h3>
<p>I found webmention.io and it does the trick. It handles the incoming pingbacks and mentions, it will also validate that the calling site is actually linking to your content and finally it will parse data out of the page so that you have some understanding of the context.</p>
<p>Webmention.io will validate that you own the site through an open authentication process (it was neat it looks for rel=me that points to an auth provider)</p>
<h3 id="step-two---tell-pages-that-you-can-handle-mentions">Step two - tell pages that you can handle mentions</h3>
<p>This is as simple as adding the two following <code>link</code> tags</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-html" data-lang="html"><<span style="color:#f92672">link</span> <span style="color:#a6e22e">rel</span><span style="color:#f92672">=</span><span style="color:#e6db74">"webmention"</span> <span style="color:#a6e22e">href</span><span style="color:#f92672">=</span><span style="color:#e6db74">"https://webmention.io/paul.kinlan.me/webmention"</span>>
<<span style="color:#f92672">link</span> <span style="color:#a6e22e">rel</span><span style="color:#f92672">=</span><span style="color:#e6db74">"pingback"</span> <span style="color:#a6e22e">href</span><span style="color:#f92672">=</span><span style="color:#e6db74">"https://webmention.io/paul.kinlan.me/xmlrpc"</span>>
</code></pre></div><h3 id="step-three---integrate-the-webmentionio-api-into-your-site">Step three - integrate the webmention.io API into your site</h3>
<p>You have two options here, you can add a widget on to your page that will call the webmention.io API, or you can integrate webmention.io API into your build step. I would like as little 3rd party hosted JS as possible, so I chose the latter. I integrated webmentions in to my deployment process.</p>
<p>I use Hugo because the build is fast, and with that in mind, I had to work out how to integrate the webmention API into Hugo in an optimal way. The hard constraint was to not call the API endpoint for every page on my site, I have a lot of pages, and not a lot of comments yet.</p>
<p>Luckily the Webmention.io site provides a handy endpoint will let you receive all of the mentions for your domain. The unlucky bit is that this file contains one entry for every action that has been done against your site.</p>
<p>Hugo also has the notion of Data files that can be pulled directly into the template for any given page, so you have to map the Webmention data file to a new structure that makes it easy to read inside a Hugo template.</p>
<p>The process I chose is below, but the summary is that I turn the array from a list of actions to a dictionary of URL's that each contain the actions exposed by the API (like, repost and reply), and the final step is then to split the dictionary of URLs into individual files that are named as the md5 hash of the url.</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:#e6db74">"use strict"</span>;
<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">fs</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">require</span>(<span style="color:#e6db74">'fs'</span>);
<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">fetch</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">require</span>(<span style="color:#e6db74">'node-fetch'</span>);
<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">md5</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">require</span>(<span style="color:#e6db74">'md5'</span>);
<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">processMentionsJson</span> <span style="color:#f92672">=</span> (<span style="color:#a6e22e">data</span>) => {
<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">urlData</span> <span style="color:#f92672">=</span> {};
<span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">children</span>.<span style="color:#a6e22e">forEach</span>(<span style="color:#a6e22e">item</span> => {
<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">wmProperty</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">item</span>[<span style="color:#e6db74">"wm-property"</span>];
<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">url</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">item</span>[<span style="color:#a6e22e">wmProperty</span>];
<span style="color:#66d9ef">if</span>(<span style="color:#a6e22e">url</span> <span style="color:#66d9ef">in</span> <span style="color:#a6e22e">urlData</span> <span style="color:#f92672">===</span> <span style="color:#66d9ef">false</span>) <span style="color:#a6e22e">urlData</span>[<span style="color:#a6e22e">url</span>] <span style="color:#f92672">=</span> {};
<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">urlDataItem</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">urlData</span>[<span style="color:#a6e22e">url</span>];
<span style="color:#66d9ef">if</span>(<span style="color:#a6e22e">wmProperty</span> <span style="color:#66d9ef">in</span> <span style="color:#a6e22e">urlDataItem</span> <span style="color:#f92672">===</span> <span style="color:#66d9ef">false</span>) <span style="color:#a6e22e">urlDataItem</span>[<span style="color:#a6e22e">wmProperty</span>] <span style="color:#f92672">=</span> [];
<span style="color:#a6e22e">urlDataItem</span>[<span style="color:#a6e22e">wmProperty</span>].<span style="color:#a6e22e">push</span>(<span style="color:#a6e22e">item</span>);
});
<span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#a6e22e">urlData</span>);
<span style="color:#75715e">// For each URL in the blog we now have a JSON stucture that has all the like, mentions and reposts
</span><span style="color:#75715e"></span> <span style="color:#66d9ef">if</span>(<span style="color:#a6e22e">fs</span>.<span style="color:#a6e22e">existsSync</span>(<span style="color:#e6db74">'./data'</span>) <span style="color:#f92672">===</span> <span style="color:#66d9ef">false</span>) <span style="color:#a6e22e">fs</span>.<span style="color:#a6e22e">mkdirSync</span>(<span style="color:#e6db74">'./data'</span>);
Object.<span style="color:#a6e22e">keys</span>(<span style="color:#a6e22e">urlData</span>).<span style="color:#a6e22e">forEach</span>(<span style="color:#a6e22e">key</span> => {
<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">item</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">urlData</span>[<span style="color:#a6e22e">key</span>];
<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">md5url</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">md5</span>(<span style="color:#a6e22e">key</span>);
<span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#a6e22e">key</span>, <span style="color:#a6e22e">md5url</span>)
<span style="color:#a6e22e">fs</span>.<span style="color:#a6e22e">writeFileSync</span>(<span style="color:#e6db74">`./data/</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">md5url</span><span style="color:#e6db74">}</span><span style="color:#e6db74">.json`</span>, <span style="color:#a6e22e">JSON</span>.<span style="color:#a6e22e">stringify</span>(<span style="color:#a6e22e">item</span>));
});
}
(<span style="color:#a6e22e">async</span> () => {
<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">mentionsUrl</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">URL</span>(<span style="color:#a6e22e">process</span>.<span style="color:#a6e22e">argv</span>[<span style="color:#ae81ff">2</span>]); <span style="color:#75715e">// Fail hard if it's not a uRL
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">mentionsResponse</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">await</span> <span style="color:#a6e22e">fetch</span>(<span style="color:#a6e22e">mentionsUrl</span>);
<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">mentiosnJson</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">await</span> <span style="color:#a6e22e">mentionsResponse</span>.<span style="color:#a6e22e">json</span>();
<span style="color:#a6e22e">processMentionsJson</span>(<span style="color:#a6e22e">mentiosnJson</span>);
})();
</code></pre></div><p>Once the data is parsed and saved correctly, it is a quick process of setting up the template so that it can be read into the Data attribute of the template.</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-html" data-lang="html">{{ $urlized := .Page.Permalink | md5 }}
{{ if index .Site.Data $urlized }}
{{ $likes := index (index .Site.Data $urlized) "like-of" }}
{{ $replys := index (index .Site.Data $urlized) "in-reply-to" }}
{{ $reposts := index (index .Site.Data $urlized) "repost-of"}}
<<span style="color:#f92672">h4</span>>Likes</<span style="color:#f92672">h4</span>>
{{ range $i, $like := $likes }}
<<span style="color:#f92672">a</span> <span style="color:#a6e22e">href</span><span style="color:#f92672">=</span><span style="color:#e6db74">"{{$like.url}}"</span>><<span style="color:#f92672">img</span> <span style="color:#a6e22e">src</span><span style="color:#f92672">=</span><span style="color:#e6db74">"{{ $like.author.photo}}"</span> <span style="color:#a6e22e">alt</span><span style="color:#f92672">=</span><span style="color:#e6db74">"{{ $like.author.name }}"</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">"profile photo"</span>></<span style="color:#f92672">a</span>>
{{end}}
<<span style="color:#f92672">h4</span>>Reposts</<span style="color:#f92672">h4</span>>
{{ range $i, $repost := $reposts }}
<<span style="color:#f92672">a</span> <span style="color:#a6e22e">href</span><span style="color:#f92672">=</span><span style="color:#e6db74">"{{$repost.url}}"</span>><<span style="color:#f92672">img</span> <span style="color:#a6e22e">src</span><span style="color:#f92672">=</span><span style="color:#e6db74">"{{ $repost.author.photo}}"</span> <span style="color:#a6e22e">alt</span><span style="color:#f92672">=</span><span style="color:#e6db74">"{{ $repost.author.name }}"</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">"profile photo"</span>></<span style="color:#f92672">a</span>>
{{end}}
<<span style="color:#f92672">h4</span>>Comments and Replies</<span style="color:#f92672">h4</span>>
{{ range $i, $reply := $replys }}
<<span style="color:#f92672">a</span> <span style="color:#a6e22e">href</span><span style="color:#f92672">=</span><span style="color:#e6db74">"{{$reply.url}}"</span>><<span style="color:#f92672">img</span> <span style="color:#a6e22e">src</span><span style="color:#f92672">=</span><span style="color:#e6db74">"{{ $reply.author.photo}}"</span> <span style="color:#a6e22e">alt</span><span style="color:#f92672">=</span><span style="color:#e6db74">"{{ $reply.author.name }}"</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">"profile photo"</span>></<span style="color:#f92672">a</span>>
{{end}}
{{end}}
</code></pre></div><p>If all goes well, you should see some icons at the bottom of the page that are real people interacting with the site.</p>
<h3 id="step-4---publish-the-site-when-comments-occur">Step 4 - publish the site when comments occur</h3>
<p>Whilst the above steps will let me aggregate the mentions and render them in the sites output, I still have to ensure that the site is rebuilt regularly so that the comments appear publicly.</p>
<p>I chose to use a simple cron service that will call Zeit's deployment API to force a re-depoly of the site every hour or so.</p>
This content originally appeared on Modern Web Development with Chrome and was authored by Paul Kinlan