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):
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-idandcategory-idfrom 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.xml→https://example.com/index.xml - Section feeds:
/posts/index.xml,/notes/index.xml, etc. (folders are lowercase)
If you’re using a
.onionaddress or staging domain, just set that inbaseURL.
Locally, Hugo serves feeds athttp://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"
Option B — Add an icon in the footer (theme-safe)
Create:
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:
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:
- Open any post locally.
- Scroll to the bottom — Giscus should appear.
- 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/, notcontent/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
.onionmirror.
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.onionvhost - 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-issoto 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)
Theme correctness for Giscus.
PaperMod doesn’t add.light—it only toggles.darkand storespref-theme. I now:- read
localStorage.pref-themefirst, - fall back to class/data-theme, then
- fall back to OS preference.
I also re-theme the Giscus iframe on every toggle viapostMessage.
- read
Isso over Tor.
On the.onionvhost, I moved the proxy to a^~ /isso/block so it beats the static regex. That fixed the404on/isso/js/embed.min.js.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_lightanddark_dimmedwithout reload. - Open DevTools → Network on onion:
/isso/js/embed.min.jsloads 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
DBandPOST_URIfirst.
# 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.confor within your container at/config/isso.conf), look for:[general] dbpath = /var/lib/isso/comments.db - What goes in
POST_URI? Isso stores auristring 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. TheLIKEquery above deliberately matches any uri containing yourPOST_URIto 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:
curl -fSLO https://blog.alipour.eu/sources/posts/new_features.md curl -fSLO https://blog.alipour.eu/sources/posts/new_features.md.asc gpg --verify new_features.md.asc new_features.md