Weixuan

Using self-hosted Utterances as Jekyll blog commenting system

Jul 28, 2020 jekyll

Why I chose Utterances over Disqus or Gitalk for my blog, and how to self-host it to use custom styles.

Contents

What are the choices?

There are plenty of comment systems for blogs out there, the most popular one has to be Disqus. I added it to this blog initially, since the process was just so easy. However, then I found out there are some privacy concerns1, that it has “the largest and deepest profiles on the web”2. So I decided to switch to an open-source alternative. Gitalk and Utterances are both open-source projects that record comments as GitHub issues but using different approaches.

Gitalk is an amazing project by all means, but to use it on a static website (e.g. Jekyll), you have to expose your OAuth app client secret, while granting it with all the power of reading and writing public repositories. Even though this is because of how GitHub specifies scopes for OAuth Apps, users may be discouraged from commenting3.

Utterances, on the other hand, uses a GitHub App instead of an OAuth one. This offers more granular permission controls, specifically, it does not have any access to code.

Quick start

Setting Utterances up is surprisingly easy, maybe more so than Disqus. Simply include the following script tag where you want the comment section.

<script src="https://utteranc.es/client.js"
        repo="username/reponame"
        issue-term="pathname"
        label="💬comment"
        theme="github-light"
        crossorigin="anonymous"
        async>
</script>

You can read more about the various arguments on the official website.

The above code includes client.js, which will replace the script tag with an iframe of utterances.html, along with some query parameters. Both of these files are served statically from the website. While this iframe approach greatly simplifies installation and offers isolation, custom styles cannot be directly applied due to same-origin policy. Utterances currently do not support passing in CSS, for good reasons4.

One solution is to self-host a modified version of Utterances.

However, please note that this will not help with gaining trust in users as self-hosting requires using your own GitHub App (due to origin limit). Also, you have to dive into the source code.

It’s probably better to contribute a theme on the official repository, but if you want to know more about how it works under the hood, let’s proceed 😎.

Customisations and self-hosting

Utterances consist of two parts, utterances - a static website along with some scripts, and utterances-oauth that powers the GitHub OAuth flow and issue creation.

I will assume that your blog is on GitHub Pages using the default URL - https://<github_username>.github.io.

The author of Utterances wrote a wonderful self-hosting instruction, which is for an old version using Azure Web App, but GitHub App configuration is the same. The next section is heavily referenced to it.

Creating a GitHub App

Create a new GitHub App at https://github.com/settings/apps/new with the following configurations:

FieldValue
User authorization callback URLthe url of your Cloudflare worker, with /authorized appended. E.g. https://utterances.username.workers.dev/authorized, we will configure this in the next section
Webhook URLIt's a required field, but utterances doesn't use it. Pick anything... your blogs url... doesn't matter.
PermissionsIssues: Read & Write. No other permissions necessary.
Where can this GitHub App be installedOnly on this account

Keep the tab open after the App is created, we will be using Client ID and Client secret soon.

Configuring Cloudflare Workers

Cloudflare Workers have a generous free plan, and provides you with a free <username>.workers.dev subdomain, while you can also use your domain, it’s easier to setup. Make sure it’s the same as specified earlier for your GitHub App.

After cloning utterances-oauth, modify the deploy command5 in package.json to be

"deploy": "cfworker deploy-dev src/index.ts"

Then, create a .env file with the following contents under the project root directory. Remember to keep it a secret 🤫.

BOT_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
CLIENT_ID=xxxxxxxxxxxxxxx
CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
STATE_PASSWORD=01234567890123456789012345678901
ORIGINS=https://<github_username>.github.io,http://localhost:4000

CLOUDFLARE_EMAIL=xxxxxxxxxxxxxxx
CLOUDFLARE_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
CLOUDFLARE_ACCOUNT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
CLOUDFLARE_WORKERS_DEV_PROJECT=utterances

To use your domain, you will also need to specify ZONE_ID, along with your domain ACCOUNT_ID. The deploy command is also different, being more similar to the original command.

To fill out the first section, follow the instructions on utterances-oauth’s GitHub repository. Note that you should use your GitHub Pages origin for ORIGINS.

For the second section, go to your Cloudflare Workers dashboard.

When you are ready, deploy using yarn run deploy after installing the dependencies with yarn install. If everything goes to plan, you will see the word alive at the URL of your worker.

Creating a fork of the website and scripts

So we’ve got the backend configured, now we need to set up the scripts embedded into our websites. It all starts by forking utterances on GitHub. Remove the && echo 'utteranc.es' > dist/CNAME from predeploy in package.json, and the scripts will be served from <github_username>.github.io/utterances/.

As they will no longer be under the root domain, some paths need to be changed. You can check out my fork of utterances for details.

While package.json is still open, let’s also change the start and build commands to include this new path, using parcel’s public-url option:

"start": "parcel serve src/*.html src/client.ts src/stylesheets/themes/*/{index,utterances}.scss --no-hmr --port 4000 --public-url /utterances",
"build": "parcel build src/*.html src/client.ts src/stylesheets/themes/*/{index,utterances}.scss --experimental-scope-hoisting --public-url /utterances",

This will correct the paths of scripts included through the script tag, the rest need to be changed manually I’m afraid.

<if condition="NODE_ENV === 'production'">
  <link id="theme-stylesheet" rel="stylesheet" href="https://<github_username>.github.io/utterances/stylesheets/themes/github-light/index.css">
</if>
<else>
  <link id="theme-stylesheet" rel="stylesheet" href="http://localhost:4000/utterances/stylesheets/themes/github-light/index.css">
</else>
<if condition="NODE_ENV === 'production'">
  <script src="https://<github_username>.github.io/utterances/client.js"
          repo="<github_username>/utterances"
          more-stuff=...>
  </script>
</if>
<else>
  <script src="http://localhost:4000/utterances/client.js"
          repo="<github_username>/utterances"
          more-stuff=...>
  </script>
</else>
if (script === undefined) {
  // Internet Explorer :(
  // tslint:disable-next-line:max-line-length
  script = document.querySelector('script[src^="https://<github_username>.github.io/utterances/client.js"],script[src^="http://localhost:4000/utterances/client.js"]') as HTMLScriptElement;
}
// create the comments iframe and it's responsive container
const utterancesOrigin = script.src.match(/^https:\/\/<github_username>\.github\.io|http:\/\/localhost:\d+/)![0];
const url = `${utterancesOrigin}/utterances/utterances.html`;
    link.href = `/utterances/stylesheets/themes/${theme}/utterances.css`;
    document.head.appendChild(link);

    addEventListener('message', event => {
      if (event.origin === origin && event.data.type === 'set-theme') {
        link.href = `/utterances/stylesheets/themes/${event.data.theme}/utterances.css`;
      }
    });

Last but not least, edit src/utterances-api.ts and utterances.json:

Phew…That’s all! Use yarn run start to test if everything works as expected, and use yarn run deploy to deploy to GitHub Pages. Now you can freely modify the styles.

I probably have spent too much time on the commenting system, feel free to leave some comments to help me justify it 😍.

References

  1. Criticism, privacy, and security concerns - Wikipedia 

  2. Advertisers Can Now Target You Based On The Comments You Leave On Websites 

  3. Gitalk Issue #95 

  4. Jeremy Danyow (author of Utterances) explains why custom CSS is not supported 

  5. @cfworker/dev package 

Latest Posts