+++ 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 {{ partial "svg.html" (dict "name" "rss") }} RSS ``` 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
Open on GitHub ↗
Comments powered by Isso
``` ## 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 >}}