Skip to content
Piyak Labs
Go back

Login-free comments, built by hand on Cloudflare

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.



Previous Post
Turning one VPS into an AI agent home + a dev box I can reach from anywhere
Next Post
K-map-router — open Google Maps directions in Korea's map apps

Comments

Loading comments...

Leave a comment

Your comment will be published after admin approval.