759 lines
24 KiB
Markdown
759 lines
24 KiB
Markdown
+++
|
||
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
|
||
<section id="comments" class="giscus">
|
||
<script
|
||
src="https://giscus.app/client.js"
|
||
data-repo="yourname/blog-comments"
|
||
data-repo-id="YOUR_REPO_ID"
|
||
data-category="Comments"
|
||
data-category-id="YOUR_CATEGORY_ID" <!-- Get them from Giscus's page! -->
|
||
data-mapping="pathname"
|
||
data-strict="0"
|
||
data-reactions-enabled="1"
|
||
data-emit-metadata="0"
|
||
data-input-position="bottom"
|
||
data-theme="preferred_color_scheme"
|
||
data-lang="en"
|
||
data-loading="lazy"
|
||
crossorigin="anonymous"
|
||
async>
|
||
</script>
|
||
<noscript>Enable JavaScript to view comments powered by Giscus.</noscript>
|
||
</section>
|
||
```
|
||
|
||
**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
|
||
<a href="/index.xml" rel="alternate" type="application/rss+xml" title="RSS" class="rss-link">
|
||
{{ partial "svg.html" (dict "name" "rss") }}
|
||
<span>RSS</span>
|
||
</a>
|
||
```
|
||
|
||
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
|
||
<!-- comments.html -->
|
||
<div class="comments-switch" style="display:flex;gap:.5rem;margin:.5rem 0 1rem;">
|
||
<button id="btn-isso" type="button" aria-pressed="true">Anonymous (Isso)</button>
|
||
<button id="btn-giscus" type="button" aria-pressed="false">GitHub (Giscus)</button>
|
||
<a href="https://github.com/AlipourIm/blog-comments/discussions/categories/comments" target="_blank" rel="noopener">Open on GitHub ↗</a>
|
||
</div>
|
||
|
||
<!-- Isso panel (default) -->
|
||
<div id="panel-isso">
|
||
<section id="isso-thread"></section>
|
||
<script
|
||
src="/isso/js/embed.min.js"
|
||
data-isso="/isso"
|
||
data-isso-css="true"
|
||
data-isso-lang="en"
|
||
data-isso-max-comments-nested="5"
|
||
data-isso-sorting="newest"
|
||
async>
|
||
</script>
|
||
<small class="isso-powered" style="display:block;margin:.5rem 0;color:var(--secondary,#888)">
|
||
Comments powered by <a href="https://isso-comments.de" target="_blank" rel="noopener">Isso</a>
|
||
</small>
|
||
</div>
|
||
|
||
<!-- Giscus panel (lazy-loaded on click) -->
|
||
<div id="panel-giscus" style="display:none">
|
||
<section id="giscus-thread"></section>
|
||
</div>
|
||
|
||
<script>
|
||
(function () {
|
||
const panelIsso = document.getElementById('panel-isso');
|
||
const panelGisc = document.getElementById('panel-giscus');
|
||
const issoBtn = document.getElementById('btn-isso');
|
||
const giscBtn = document.getElementById('btn-giscus');
|
||
let gLoaded = false;
|
||
|
||
function isDark() {
|
||
// 1) explicit PaperMod preference
|
||
const pref = localStorage.getItem('pref-theme');
|
||
if (pref === 'dark') return true;
|
||
if (pref === 'light') return false;
|
||
|
||
// 2) page state (classes / data-theme)
|
||
const H = document.documentElement, B = document.body;
|
||
const hasDark =
|
||
H.classList.contains('dark') || B.classList.contains('dark') ||
|
||
H.dataset.theme === 'dark' || B.dataset.theme === 'dark' ||
|
||
H.classList.contains('theme-dark') || B.classList.contains('theme-dark');
|
||
if (hasDark) return true;
|
||
|
||
// 3) OS preference
|
||
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||
}
|
||
const gTheme = () => (isDark() ? 'dark_dimmed' : 'noborder_light');
|
||
|
||
function loadGiscus() {
|
||
if (gLoaded) return;
|
||
const s = document.createElement('script');
|
||
s.src = 'https://giscus.app/client.js';
|
||
s.async = true;
|
||
s.crossOrigin = 'anonymous';
|
||
s.setAttribute('data-repo','AlipourIm/blog-comments');
|
||
s.setAttribute('data-repo-id','R_kgDOQGARyA');
|
||
s.setAttribute('data-category','Comments');
|
||
s.setAttribute('data-category-id','DIC_kwDOQGARyM4Cw3x-');
|
||
s.setAttribute('data-mapping','pathname');
|
||
s.setAttribute('data-strict','0');
|
||
s.setAttribute('data-reactions-enabled','1');
|
||
s.setAttribute('data-emit-metadata','0');
|
||
s.setAttribute('data-input-position','bottom');
|
||
s.setAttribute('data-lang','en');
|
||
s.setAttribute('data-theme', gTheme()); // initial theme
|
||
document.getElementById('giscus-thread').appendChild(s);
|
||
|
||
// Retheme once iframe exists
|
||
const reTheme = () => {
|
||
const iframe = document.querySelector('iframe.giscus-frame');
|
||
if (!iframe) return;
|
||
iframe.contentWindow?.postMessage(
|
||
{ giscus: { setConfig: { theme: gTheme() } } },
|
||
'https://giscus.app'
|
||
);
|
||
};
|
||
|
||
// PaperMod toggle button
|
||
document.getElementById('theme-toggle')?.addEventListener('click', () =>
|
||
setTimeout(reTheme, 0)
|
||
);
|
||
|
||
// Also react to attribute changes
|
||
const mo = new MutationObserver(reTheme);
|
||
mo.observe(document.documentElement, { attributes: true, attributeFilter: ['class','data-theme'] });
|
||
mo.observe(document.body, { attributes: true, attributeFilter: ['class','data-theme'] });
|
||
|
||
gLoaded = true;
|
||
}
|
||
|
||
function show(which) {
|
||
const showIsso = which === 'isso';
|
||
panelIsso.style.display = showIsso ? 'block' : 'none';
|
||
panelGisc.style.display = showIsso ? 'none' : 'block';
|
||
issoBtn?.setAttribute('aria-pressed', showIsso);
|
||
giscBtn?.setAttribute('aria-pressed', !showIsso);
|
||
if (!showIsso) loadGiscus();
|
||
}
|
||
|
||
issoBtn?.addEventListener('click', e => { e.preventDefault(); show('isso'); });
|
||
giscBtn?.addEventListener('click', e => { e.preventDefault(); show('giscus'); });
|
||
|
||
// default
|
||
show('isso');
|
||
})();
|
||
</script>
|
||
```
|
||
|
||
|
||
## 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://<your-onion-host>/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 <isso_container_name>
|
||
```
|
||
|
||
---
|
||
|
||
## If running Isso in Docker
|
||
|
||
Open a shell in the container, then run the same steps:
|
||
|
||
```bash
|
||
docker exec -it <isso_container_name> /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 <isso_container_name>
|
||
```
|
||
|
||
---
|
||
|
||
## 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/<slug>/`). 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 /<comment_id>` 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 >}}
|