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.
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 😎.
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.
Create a new GitHub App at https://github.com/settings/apps/new with the following configurations:
Field | Value |
---|---|
User authorization callback URL | the 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 URL | It's a required field, but utterances doesn't use it. Pick anything... your blogs url... doesn't matter. |
Permissions | Issues: Read & Write. No other permissions necessary. |
Where can this GitHub App be installed | Only on this account |
Keep the tab open after the App is created, we will be using Client ID
and Client secret
soon.
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 domainACCOUNT_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.
CLOUDFLARE_API_KEY
is available under Profile > API Tokens
.CLOUDFLARE_ACCOUNT_ID
is the one specifically for your workers.dev
subdomain, available on the Workers dashboard.CLOUDFLARE_WORKERS_DEV_PROJECT
is the name for your worker, setting it to utterances
for example will make your worker reachable at utterances.<username>.workers.dev
.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.
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.
src/index.html
:<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>
src/client.ts
: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`;
src/theme.ts
: 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
:
src/utterances-api.ts
defines the URL where utterances-oauth is running, use the URL of your Cloudflare Workerutterances.json
specifies the origins that are allowed to post comments, change it to that of your GitHub PagesPhew…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 😍.