~/blog/blog-comments

Blog Comments With GitHub

by Kyle Travis on 2020-09-09

For more background, see my previous post about GitHub Actions.

Now that it's possible to follow the blog and be notified by watching on GitHub, I wanted to incorporate a little more interactivity. As much as anyone enjoys shouting into the void, I do like the idea of getting feedback - so it's time to incorporate comments.

I follow a gamedev blog on github that uses issues for posts, which seems like a pretty elegant solution to me: you get comments, reactions, labels, and search "for free". Plus, issues are written in markdown, which is already the format this blog uses for posts (mostly - foreshadowing). I wanted to maintain the independence of the static site, so that means mirroring each post to a GitHub issue, and then displaying comments on the site by retrieving them from GitHub's API.

Making Issues

Using another GitHub action, we can sync the contents of the ./src/blog/ directory with the repo's issues. At some point in the distant past when writing the static site generator, I decided to use @key = value lines in the markdown to set page metadata (which is then used during generation). I don't know why I did this, really. This has the side effect of including the metadata assignments in the rendered page if I don't strip them out while processing, but since the format is simple it's a quick adjustment. I'm probably going to go in and change those out for markdown "comments", i.e. [//]: # (key = value) which is a creative use of link labels - but it works for now.

The steps of the action boil down to:

Here is the complete action, and as before, I'm not responsible if my terrible JavaScript gets you fired/burns your house down:

name: blog-post-create-issues
on:
  push:
    branches: [ master ]
jobs:
  create:
    runs-on: ubuntu-latest
    steps:
    - name: checkout
      uses: actions/checkout@v2
    - name: collect-titles
      run: "grep -E '@name\\s*\\=' ./src/blog/ -R > posts.txt && cat posts.txt"
    - uses: actions/github-script@v3
      with:
        github-token: ${{secrets.GITHUB_TOKEN}}
        script: |
          const fs = require("fs")
          const { owner, repo } = context.repo
          const result = await github.issues.listForRepo({ owner, repo })
          const issues = Object.fromEntries(result.data.map(x => [x.title, x]))
          let lines = fs.readFileSync('posts.txt').toString().split('\n')
          lines.forEach(l => {
          	l = l.trim()
          	if (!l) return
            let [f, nameLine] = l.split(':')
            let labels = ["blog-post"]
            const body = fs.readFileSync(f).toString().split('\n').map(x => {
              if (x.match(/^\s*@labels/)) {
                labels = labels.concat(x.trim().slice(x.indexOf('=')+1).split(",").map(l => l.trim()))
              }
              return x
            }).filter(x => !x.match(/^@/))
              .join('\n')
              .replace(/(!\[[^\]]*\]\()(\/[^\)]+\))/g, "$1https://github.com/${owner}/${repo}/raw/master$2") // MD image
              .replace(/(\[[^\]]*\]\()(\/[^\)]+\))/g, "$1https://github.com/${owner}/${repo}/blob/master$2") // MD link
              .replace(/(src=")(\/[^"]+")/g, "$1https://github.com/${owner}/${repo}/raw/master$2") // <img> tag
            const title = nameLine.slice(nameLine.indexOf('=')+1).trim()
            if (title in issues) {
              const found = issues[title]
              github.issues.update({
                owner, repo,
                issue_number: found.number,
                labels: found.labels.concat(labels),
                title, body,
              })
              return
            }
            github.issues.create({ owner, repo, title, body, labels })
          })

The only notable pieces are the three replace calls run on the file contents - if I leave relative links and images like ![caption](/static/foo.png), they will break when inserted into a github issue. Instead, since we know relative static content will exist under https://github.com/${owner}/${repo}/, we can link to the appropriate /blob or /raw page. The same is done with image tags. This probably isn't a 100% foolproof or robust solution, but since I'm still in control of the source it just needs to be good enough to avoid broken links.

an example goat this image link is modified in the created issue

Displaying Comments

So now we have a way for readers to comment on (and react to) posts, but needing to leave the site to see these comments defeats the purpose. I initially wrote a JavaScript snippet using octokit to pull comments from the associated issue, create DOM elements, and shove them in a div at the bottom of the page. For such a rudimentary solution, it worked pretty well.

The main drawback of the putting a simple custom script on the page is that the reader has to leave in order to submit a comment. While researching options I stumbled on utteranc.es which is fantastic, and as you can see below, what I ended up using. The app is extremely easy to integrate with a GitHub repo - I could have omitted the action above and relied on utteranc.es to generate issues for me, but the current method does give me a bit more control over the issue body. The simplicity of the setup instructions speaks for itself - just add the GitHub app to the repo and a script tag to the page.

Takeaways

follow on github