+++
title = "New features for my blog! Adding Comments + RSS to Hugo PaperMod (Giscus and Isso FTW)"
description = "A friendly, copy-pasteable guide to wiring up Giscus comments (GitHub Discussions) and Isso comments + RSS in Hugo PaperMod."
date = 2025-10-23T19:31:12+02:00
draft = false
slug = "comments-rss-papermod"
ShowToc = true
TocOpen = true
tags = ["hugo", "papermod", "giscus", "rss", "comments"]
categories = ["blog", "meta"]
+++
> Short story: I wanted comments and a proper RSS feed on my Hugo + PaperMod site.
> Longer story: I spent 30 minutes poking around, and now you don’t have to. Here’s exactly what I did.
---
I've been wanting to add comment section to my blog for a while.
There are a few problems.
- I don't want to get attacked by bots or random people for no reason.
- I don't want to allow weird things to happen.
- I don't want to have a database for it.
So... I decided to take a path which is not necesserially the best.
It requires a third party to act on your behalf, but it is not as scary as it sounds.
According to Github:
> **What the app can do**
> The app’s rights are limited to the repo(s) where the site owner installed it and to the permissions it requested (basically: Discussions read/write + metadata). It can’t touch a commenter’s private repos or do things outside those scopes. GitHub’s docs also say a GitHub App can only do what both you and the app have permission to do.
> **What “Act on your behalf” means**
> That scary wording is GitHub’s. The giscus maintainer explains it means “post comments under your username,” and shows that the app requests Discussions read/write + metadata when installed.
> **Do commenters have alternatives?**
> Yes. Anyone can open the linked GitHub Discussion and comment directly on GitHub, bypassing the app’s OAuth popup. The README explicitly mentions this.
> **Privacy & storage**
> No tracking/ads; comments live in GitHub Discussions. giscus is open-source and has a published privacy policy. You can also revoke the app any time in your GitHub settings.
With the notice out of the way let's dive right in!
---
## Why I chose Giscus (instead of Disqus)
- **No ads or trackers** (my page weight thanks me).
- **Uses GitHub Discussions** (portable, transparent, easy to search).
- **Readers log in with GitHub** (no mystery auth popups).
- In theory you can directly go to the repo's discussion section and comment there!
---
## What you need (for your site, not per user)
- A Hugo site using **PaperMod** ✅
- A GitHub account ✅
- A **public** repository for the discussion threads (I called mine `blog-comments`) with **Discussions** enabled ✅
- Giscus app installed on that repo ✅
If you like to keep your main site repo clean, use a separate one, e.g. `github-username/blog-comments`.
---
## 1) Turn on comments with Giscus
PaperMod renders a `comments.html` partial when `params.comments = true`.
So we enable it in config and create the partial.
### 1.1 Enable comments globally
`hugo.toml` (snippet)
```toml
[params]
comments = true
```
You can still opt out per post with `comments = false` in front matter.
### 1.2 Create the comments partial
Create this file **in your site** (not inside the theme):
```
layouts/partials/comments.html
```
Paste the Giscus embed (swap the placeholders with yours from giscus.app):
```html
```
**Notes**
- You’ll get `repo-id` and `category-id` from **giscus.app** after picking your repo + category.
- `data-mapping="pathname"` works great for static sites.
- Prefer a **public** repo so visitors can read discussions without friction.
- Want a different language? Change `data-lang` (e.g., `de`, `fr`).
---
## 2) Enable RSS (home + sections)
Hugo already knows how to generate feeds; we just tell it where.
`hugo.toml`
```toml
baseURL = "https://example.com/" # set your production URL
[outputs]
home = ["HTML", "RSS", "JSON"] # JSON is optional (e.g., for on-site search)
section = ["HTML", "RSS"]
taxonomy = ["HTML", "RSS"]
term = ["HTML", "RSS"]
[services.rss]
limit = -1 # -1 = no cap; or set e.g. 20
```
**Feed URLs**
- Home feed: `/index.xml` → `https://example.com/index.xml`
- Section feeds: `/posts/index.xml`, `/notes/index.xml`, etc. (folders are lowercase)
> If you’re using a `.onion` address or staging domain, just set that in `baseURL`.
> Locally, Hugo serves feeds at `http://localhost:1313/index.xml`.
---
## 3) Show an RSS button on the home page
PaperMod shows a nice RSS icon on **section/taxonomy** lists by default (when enabled), but not on the home page. Two options:
### Option A — Use the home “social icons” row
This appears if you enable **Home-Info** or **Profile** mode:
```toml
[params.homeInfoParams]
Title = "Hello, internet!"
Content = "Notes, experiments, and occasional rabbit holes."
[[params.socialIcons]]
name = "rss" # lowercase matters
url = "/index.xml"
```
### Option B — Add an icon in the footer (theme-safe)
Create:
```
layouts/partials/extend_footer.html
```
Paste:
```html
```
Make it a sensible size:
Create:
```
assets/css/extended/rss.css
```
Paste:
```css
/* Small & aligned RSS icon in the footer */
.rss-link { display: inline-flex; align-items: center; gap: .35rem; }
.rss-link svg { margin-left: 10px; width: 18px; height: 18px; } /* a bit of left margin for good measure!*/
.rss-link span { font-size: .95rem; }
```
PaperMod auto-bundles anything under `assets/css/extended/`, so no extra imports needed.
---
## 4) Verify everything
Build or run dev:
```bash
hugo # outputs to ./public
# or
hugo server # serves at http://localhost:1313
```
Check feeds:
```bash
# Home feed:
curl -s http://localhost:1313/index.xml | head
# A section feed (adjust path to your section name):
curl -s http://localhost:1313/posts/index.xml | head
```
Check comments:
1. Open any post locally.
2. Scroll to the bottom — Giscus should appear.
3. Try a reaction or comment (you’ll be prompted to sign in with GitHub).
---
## 5) Per-post controls (handy later)
Turn comments off for a single post:
```toml
# in that post’s front matter
comments = false
```
Full content in RSS (global):
```toml
[params]
ShowFullTextinRSS = true
```
Show RSS buttons on section/taxonomy lists:
```toml
[params]
ShowRssButtonInSectionTermList = true
```
---
## 6) Troubleshooting (aka “what I bumped into”)
- **Home page has no RSS icon**
That’s expected; either enable Home-Info/Profile mode + `socialIcons`, or use the footer partial.
- **Section feed 404**
Folder names are lowercase (`content/posts/`, not `content/Posts/`).
The URL mirrors that: `/posts/index.xml`.
- **Giscus doesn’t create a discussion**
Double-check the four attributes: `data-repo`, `data-repo-id`, `data-category`, `data-category-id`.
Make sure Discussions is enabled and the Giscus app is installed for the repo.
- **Icon is comically large**
Keep the CSS above; 16–20px usually looks right.
---
## 7) Bonus: fast setup via GitHub CLI (optional, I didn't use it, but I should've perhaps)
```bash
# Create the public comments repo (adjust names)
gh repo create yourname/blog-comments --public -d "Blog comment threads" --confirm
# Enable Discussions for it
gh repo edit yourname/blog-comments --enable-discussions
```
Then visit **giscus.app**, select the repo + category (e.g. “Comments”), and copy the generated IDs into `layouts/partials/comments.html`.
---
### Final config snapshot (condensed)
```toml
baseURL = "https://example.com/"
theme = "PaperMod"
[outputs]
home = ["HTML", "RSS", "JSON"]
section = ["HTML", "RSS"]
taxonomy = ["HTML", "RSS"]
term = ["HTML", "RSS"]
[services.rss]
limit = -1
[params]
comments = true
ShowRssButtonInSectionTermList = true
ShowFullTextinRSS = true
# Optional if using Home-Info/Profile mode:
# [[params.socialIcons]]
# name = "rss"
# url = "/index.xml"
```
---
# Appendix — Why I added Isso, how I wired it, and what I fixed
## The story (why Isso + why I still keep Giscus)
I wanted comments that:
- allow **anonymous** replies (no GitHub account required),
- are **lightweight** and **self-hosted**, and
- can work on my **Tor `.onion` mirror**.
**Isso** checks those boxes: it’s a tiny Python app with SQLite storage, no trackers, and a simple embed. At the same time, I like the developer-friendly workflow of **Giscus** (comments are GitHub Discussions), so I kept both and added a little switcher UI: **“Anonymous (Isso)”** or **“GitHub (Giscus)”**.
Result: readers can pick privacy/anon (Isso) or identity/notifications (Giscus).
## How I run it (high-level)
- **Site:** Hugo + PaperMod
- **Isso:** listening on `127.0.0.1:8080`, stored locally (SQLite)
- **Nginx:** reverse-proxies **`/isso/`** to Isso on both the clearnet and the `.onion` vhost
- **Hugo partial:** renders the two panels (Isso & Giscus) and a switcher; Giscus is **lazy-loaded** and **auto-rethemed** with the site theme
> I’m deliberately keeping Isso’s own config tiny in this appendix; the important part for my setup is the **Nginx proxying** and the **Hugo partial**.
## Hugo partial: switcher + embeds
Save as `layouts/partials/comments.html` and include it in your single layout (e.g. `layouts/_default/single.html`) with `{{ partial "comments.html" . }}`.
```html
```
## Nginx: proxy Isso on **both** sites (and avoid the 404-onion trap)
The bug I hit: my `.onion` server had a regex static block that **won over** the plain `/isso/` location, so `/isso/js/embed.min.js` returned **404**.
**Fix:** make `/isso/` a `^~` prefix location so it beats regex, and place it **above** the static block.
```nginx
# /etc/nginx/sites-enabled/onion-blog (same idea for the clearnet vhost)
server {
listen 127.0.0.1:3301 default_server;
server_name fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion 127.0.0.1 localhost;
add_header Referrer-Policy no-referrer always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options SAMEORIGIN always;
root /srv/hugo/mysite/public-onion;
index index.html;
location / { try_files $uri $uri/ /index.html; }
# /isso -> /isso/
location = /isso { return 301 /isso/; }
# IMPORTANT: this must beat regex locations below
location ^~ /isso/ {
proxy_pass http://127.0.0.1:8080/; # Isso container/port
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_redirect off;
proxy_read_timeout 60s;
}
# static
location ~* \\.(css|js|ico|png|jpg|jpeg|gif|svg|webp|txt|xml)$ {
access_log off;
add_header Cache-Control "public, max-age=31536000, immutable";
try_files $uri =404;
}
}
```
Reload and smoke test:
```bash
sudo nginx -t && sudo systemctl reload nginx
curl -I http:///isso/js/embed.min.js # expect 200
```
> If you ever point `data-isso` to a **different origin**, add CORS headers on that vhost. For same-origin (`data-isso="/isso"`), you don’t need CORS.
## Small CSS tweaks (one color across light/dark + spacing)
I wanted the same blue for **reply / edit / delete** links and for **Submit / Preview** buttons, in both themes, and a little spacing:
```css
/* put this in your site CSS */
:root { --btn-blue: #2563eb; } /* adjust to taste */
/* Isso form buttons */
.isso-post-action input[type="submit"],
.isso-post-action input[name="preview"] {
background: var(--btn-blue) !important;
color: #fff !important;
border: none;
padding: .5rem .75rem;
border-radius: .4rem;
cursor: pointer;
}
/* link actions under comments */
.isso-comment-footer a {
color: var(--btn-blue) !important;
margin-right: .5rem; /* gives "edit" and "delete" some breathing room */
text-decoration: none;
}
.isso-comment-footer a:hover { text-decoration: underline; }
/* keep name/email/website stacked, slightly larger */
.isso-auth-section .isso-input-wrapper {
display: block;
margin: .35rem 0;
}
.isso-auth-section label { font-size: .95rem; }
.isso-auth-section input[type="text"],
.isso-auth-section input[type="email"],
.isso-auth-section input[type="url"] {
width: 100%;
font-size: 1rem;
padding: .5rem .6rem;
}
```
## What changed (the fixes I actually made)
1. **Theme correctness for Giscus.**
PaperMod doesn’t add `.light`—it only toggles `.dark` and stores `pref-theme`. I now:
- read `localStorage.pref-theme` first,
- fall back to class/data-theme, then
- fall back to OS preference.
I also re-theme the Giscus iframe on every toggle via `postMessage`.
2. **Isso over Tor.**
On the `.onion` vhost, I moved the proxy to a `^~ /isso/` block so it beats the static regex. That fixed the `404` on `/isso/js/embed.min.js`.
3. **UI polish.**
- Unified button/link color across themes,
- ensured “edit”/“delete” aren’t touching,
- kept the author/email/website inputs **stacked**, and
- left **Submit**/**Preview** styled and grouped.
## Sanity check
- Toggle the theme: Giscus switches between `noborder_light` and `dark_dimmed` without reload.
- Open DevTools → Network on onion: `/isso/js/embed.min.js` loads with **200**.
- Switcher buttons swap panels instantly; Giscus only loads when clicked.
## 8) Final notes!
I could not fully fix the style of Isso.
CSS is not easy... drat!
I don't think anyone actually reads the blog, let alone comment on it.
But it felt cool to have a comment section and also RSS for it.
I had a lot of not so fun debugging experience, and will probably work on the styling to make it look right, but for now I'm happy with it! ^^
# Admin note — Delete all **Isso** comments for a single post
This note is for future me: how to remove every comment tied to one post in **Isso**.
Tested with SQLite-backed Isso (default). Works whether you serve via clearnet or `.onion` — because this operates directly on the database.
---
## TL;DR (host with SQLite)
> Replace the placeholders for `DB` and `POST_URI` first.
```bash
# 0) Variables — edit these two lines for your setup
DB="/var/lib/isso/comments.db" # path to your Isso SQLite DB (check isso.conf: [general] dbpath)
POST_URI="/posts/skin_routine/" # the post's URI as stored by Isso (often just the path)
# 1) Backup (always do this!)
sqlite3 "$DB" ".backup '$(dirname "$DB")/comments.$(date +%F-%H%M%S).backup.sqlite3'"
# 2) Inspect: find the thread row and preview the comments
sqlite3 "$DB" "
.headers on
.mode column
SELECT id, uri FROM threads WHERE uri LIKE '%' || '$POST_URI' || '%';
SELECT id, tid, created, author, text
FROM comments
WHERE tid IN (SELECT id FROM threads WHERE uri LIKE '%' || '$POST_URI' || '%')
ORDER BY created DESC
LIMIT 25;
"
# 3) Delete all comments for that post (thread)
sqlite3 "$DB" "
DELETE FROM comments
WHERE tid IN (SELECT id FROM threads WHERE uri LIKE '%' || '$POST_URI' || '%');
"
# 4) (Optional) Remove the now-empty thread row too
sqlite3 "$DB" "
DELETE FROM threads WHERE uri LIKE '%' || '$POST_URI' || '%';
VACUUM;
"
# 5) Done. Isso usually picks this up automatically.
# If you're containerized and want to be safe:
# docker restart
```
---
## If running Isso in Docker
Open a shell in the container, then run the same steps:
```bash
docker exec -it /bin/sh
# Inside the container:
DB="/db/comments.db" # or /data/comments.db, check your image/volume mapping
POST_URI="/posts/skin_routine/"
sqlite3 "$DB" ".backup '/db/comments.$(date +%F-%H%M%S).backup.sqlite3'"
sqlite3 "$DB" "SELECT id, uri FROM threads WHERE uri LIKE '%' || '$POST_URI' || '%';"
sqlite3 "$DB" "DELETE FROM comments WHERE tid IN (SELECT id FROM threads WHERE uri LIKE '%' || '$POST_URI' || '%');"
sqlite3 "$DB" "DELETE FROM threads WHERE uri LIKE '%' || '$POST_URI' || '%'; VACUUM;"
exit
# Optionally restart the container:
docker restart
```
---
## Notes & gotchas
- **Find the real DB path**: In `isso.conf` (often `/etc/isso/isso.conf` or within your container at `/config/isso.conf`), look for:
```ini
[general]
dbpath = /var/lib/isso/comments.db
```
- **What goes in `POST_URI`?** Isso stores a `uri` string per thread. With the default embed config, it's usually the **path** (`/posts//`). If you’re unsure, list recent threads:
```bash
sqlite3 "$DB" ".headers on" ".mode column" "SELECT id, uri FROM threads ORDER BY id DESC LIMIT 50;"
```
- **Clearnet vs `.onion`**: If you load the same post under multiple origins _and_ your embed setup stores full origins, you may have **multiple thread rows**. The `LIKE` query above deliberately matches any uri containing your `POST_URI` to catch those.
- **Rollback**: To undo, stop Isso (optional), replace the DB with your backup (`comments.YYYY-MM-DD-HHMMSS.backup.sqlite3`), and start Isso again.
- **HTTP API option**: Isso supports `DELETE /` if admin endpoints/auth are enabled. For bulk removal it’s simpler and safer to use SQLite directly.
- **Safety**: Always run a backup first, and **preview with the SELECT** before you run the DELETE.
---
## One-liners for the impatient
```bash
DB="/var/lib/isso/comments.db"; POST_URI="/posts/skin_routine/"
sqlite3 "$DB" ".backup '$(dirname "$DB")/comments.$(date +%F-%H%M%S).backup.sqlite3'"
sqlite3 "$DB" "DELETE FROM comments WHERE tid IN (SELECT id FROM threads WHERE uri LIKE '%' || '$POST_URI' || '%');"
sqlite3 "$DB" "DELETE FROM threads WHERE uri LIKE '%' || '$POST_URI' || '%'; VACUUM;"
```
---
**Isso container: quick checks & fixes for future me! (thank past me later ^^)**
**Find the container**
```bash
docker ps -a --format 'table {{.ID}} {{.Names}} {{.Status}} {{.Ports}}' | grep -i isso
```
**Inspect basic state**
```bash
docker inspect -f 'Name={{.Name}} Running={{.State.Running}} Status={{.State.Status}} StartedAt={{.State.StartedAt}} FinishedAt={{.State.FinishedAt}}' isso
```
**Logs**
```bash
docker logs --tail=200 isso
docker logs -f isso
```
**Ports & reachability**
```bash
docker port isso
sudo ss -ltnp | grep ':8080 '
curl -i http://127.0.0.1:8080/
```
**Restart policy (keep it running)**
```bash
docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' isso
docker update --restart unless-stopped isso
```
**Start/stop the container**
```bash
docker start isso
docker stop isso
```
**Who/what stopped it (events)**
```bash
docker events --since 24h --filter container=isso
```
**Common config issue: duplicate options/sections**
If logs show DuplicateOptionError or DuplicateSectionError, fix `/config/isso.cfg` (remove duplicate `[server]` blocks or duplicate `public-endpoint`).
```bash
nl -ba /config/isso.cfg | sed -n '1,120p'
grep -n '^\[server\]' /config/isso.cfg
grep -n '^public-endpoint' /config/isso.cfg
```
**Using Docker Compose** *(run these in the directory with your compose file)*
```bash
docker compose ps
docker compose logs --tail=200 isso
docker compose up -d isso
```
**If the container doesn’t exist (create/recreate)**
```bash
docker image ls | grep -i isso
docker run -d --name isso -p 8080:8080 -v /srv/isso/config:/config -v /srv/isso/db:/db --restart unless-stopped ghcr.io/posativ/isso:latest
```
**Optional: exclude from auto-updaters (like Watchtower)**
```bash
docker update --label-add com.centurylinklabs.watchtower.enable=false isso
```
_This note is intentionally self-contained so I can paste it at the end of the blog post as a maintainer-only appendix._
---
{{< sigdl >}}