Adding comments to the blog
I built this blog with Astro and always meant to add comments. So I started where most people do, surveying the usual tools — Giscus, Disqus, and friends.
The thing is, this blog mixes developer write-ups with personal, everyday posts. That single fact knocked out the most popular option immediately: GitHub-login comments like Giscus or Utterances. The moment a non-developer reader sees “sign in with GitHub to comment,” they close the tab. For a blog that isn’t developers-only, a login wall is a dealbreaker, not a convenience.
Disqus was out too — it’s heavy and comes with ads and trackers I don’t want on a personal site. Waline is genuinely good, but it expects a backend and a database running outside Cloudflare. That’s one more service to deploy, monitor, and keep alive.
The blog already lives on Cloudflare Pages. So the goal sharpened into one sentence: no login, no spam, all in the same stack.
So I built it myself
The shape is deliberately small. The site itself is a statically built Astro bundle. Anything dynamic is a Pages Function — a serverless handler — talking to a single D1 database (Cloudflare’s SQLite). There is no origin server to run.
Static Astro ── Cloudflare Pages
├─ /api/comments → spam check → write to D1 (pending approval)
├─ /api/likes → like counter
└─ /api/telegram → approve / delete from Telegram
D1: comments, likes
The job of stopping spam without a login falls to Cloudflare Turnstile — a
CAPTCHA that’s free, cookie-less, and requires no sign-in. But the important part
is where it’s verified. The browser solving the challenge isn’t enough; a script
could forge that. So the flow is two-sided: the widget produces a token in the
browser, and the Function calls Turnstile’s siteverify server-side with my
secret key. Only if that check passes does the comment ever reach the database.
The client widget is the convenience; the server verification is the actual gate.
Comments are “approved before they appear”
The most important decision was pre-moderation. Every comment is written to D1 with a status of pending, and it stays invisible on the site until I approve it.
Concretely, each row carries an approved flag — 0 pending, 1 approved,
2 rejected. The public read endpoint (GET /api/comments) only ever returns
rows where approved = 1. A brand-new comment is born as 0, so the default
state of anything a stranger submits is not shown. For a young blog, that
matters: it can never be papered over with spam, because nothing is public until I
say so. Moderation is opt-in, not cleanup-after-the-fact.
That left one question: where do I actually approve things? Building a separate admin page felt like a chore I didn’t want to maintain.
No admin page — a Telegram bot
So instead of an admin screen, I made a Telegram bot the moderation tool. When a comment comes in, the Function pings the bot, and the bot DMs me with inline buttons:
💬 New comment (pending)
👤 author
content...
[✅ Approve] [❌ Reject]
Tapping a button is the whole workflow. Telegram sends that tap back to my
/api/telegram/webhook Function as a callback, which flips the row’s approved
flag in D1. Approve a comment and it goes live; the message then keeps a
🗑 Delete button, so I can remove an already-published comment later from the
exact same chat.
One detail worth calling out: that webhook is a public URL, so it needs to know a
request genuinely came from Telegram. Telegram supports this directly — you
register the webhook with a secret_token, and it then sends that value in an
X-Telegram-Bot-Api-Secret-Token header on every callback. The Function just
checks it:
// placeholders — never commit real values
if (env.TELEGRAM_WEBHOOK_SECRET) {
const got = request.headers.get("X-Telegram-Bot-Api-Secret-Token");
if (got !== env.TELEGRAM_WEBHOOK_SECRET) {
return new Response("Forbidden", { status: 403 });
}
}
No separate admin panel, no extra login — moderation happens from my phone, in a
chat I already have open, and forged approval requests get a 403.
Wrapping up
So here’s the whole thing: zero cost, zero servers to operate, one stack — Cloudflare. Comments come in without a login, spam is filtered out, and I moderate from my phone.
And the comment box is right below this post. Leave one. If you’d like to adapt the same pattern for your own blog, I hope this was a useful map of how the pieces fit together.
Comments
Loading comments...
Leave a comment
Your comment will be published after admin approval.