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)

[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):

<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

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.xmlhttps://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:

[params.homeInfoParams]
  Title = "Hello, internet!"
  Content = "Notes, experiments, and occasional rabbit holes."

[[params.socialIcons]]
  name = "rss"        # lowercase matters
  url  = "/index.xml"

Create:

layouts/partials/extend_footer.html

Paste:

<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:

/* 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:

hugo           # outputs to ./public
# or
hugo server    # serves at http://localhost:1313

Check feeds:

# 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:

# in that post’s front matter
comments = false

Full content in RSS (global):

[params]
  ShowFullTextinRSS = true

Show RSS buttons on section/taxonomy lists:

[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)

# 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)

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" . }}.

<!-- 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.

# /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:

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:

/* 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.

# 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:

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:
    [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:
    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

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

docker ps -a --format 'table {{.ID}}	{{.Names}}	{{.Status}}	{{.Ports}}' | grep -i isso

Inspect basic state

docker inspect -f 'Name={{.Name}}  Running={{.State.Running}}  Status={{.State.Status}}  StartedAt={{.State.StartedAt}}  FinishedAt={{.State.FinishedAt}}' isso

Logs

docker logs --tail=200 isso
docker logs -f isso

Ports & reachability

docker port isso
sudo ss -ltnp | grep ':8080 '
curl -i http://127.0.0.1:8080/

Restart policy (keep it running)

docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' isso
docker update --restart unless-stopped isso

Start/stop the container

docker start isso
docker stop isso

Who/what stopped it (events)

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).

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)

docker compose ps
docker compose logs --tail=200 isso
docker compose up -d isso

If the container doesn’t exist (create/recreate)

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)

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.


Downloads: Markdown · Signature (.asc)

View OpenPGP signature
-----BEGIN PGP SIGNATURE-----

iQIzBAABCgAdFiEEVaKl3oR5K6zGyu4/tYgoUOBMjSoFAmpkbuMACgkQtYgoUOBM
jSr2VxAAgdHpf+4EIx2ASmSB+MeAHp7s1+2EPhmn96QuhiQO9Dr1e9250LbF/R8S
A0zcN8mmyOtKv2rediU6HsGy6ddwdTAtpQDRvMw2Xuk2pBUZaG5LuvlNgSse9zVB
dcG3GVLuMSRgNmyolhFoSWn46aa+3wZGrzYZQVUt8op+2fVp36agq2YoB4zeGKSm
Ri43rO/kTingD0bclOA+SMbHpKQOcLyHwDlVrqIjVXcJ/kKLcm1G3Z5owo+gB6jY
EMjxYiqyf5Zcbt4q/WzojbHLffjXoMzOgZ1sDOIObVQni9atgV49X4oIZ3+xSXSH
W7ZbH6sg9LlrU8djddBXUKB0i72IVZ/4F5icVFUcK+szOmASy8fFP7gCOcCRATtb
eNIFtiAlRXI0CqJdeq5fE9b+LX8lRpNnX229tg7GFgddzYUmFkKAsoJ9EUzUvUHm
Xzqlm9DKy9LG/CeOxo473fIo4YCT2fcmMnt9nCZW4iDKOVl1nCupkTn5qsfNdpQM
KycaNwsLBHPZWV+jDon8NEzYQk07n1Q9rlEWdL0egvn2S+pYnvLZbfi5dRhe+ciC
qcEW/I1NZZRdU7MUEzhWvhbWsZtTkm6OevXjnqACv91DIQv7tNrKwZuASnpFeDRa
GE0QYRrsaI3vCLR3cUmBAFsvZdwgzH0RQLb8w21XBW/BoFvixbA=
=9k+S
-----END PGP SIGNATURE-----

Verify locally:

torsocks curl -fSLO http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/sources/posts/new_features.md
torsocks curl -fSLO http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/sources/posts/new_features.md.asc
gpg --verify new_features.md.asc new_features.md