Insights / Technical notes
Build Astro Blog Comments with Cloudflare Only
How we added comments to an Astro blog without an external comment service, using only Cloudflare Pages Functions, D1, Turnstile, and Wrangler configuration.

Table of contents
Static sites usually avoid server-side state. Comments are the moment that rule becomes inconvenient.
For Acecore, we did not add an external comment SaaS or embedded widget. The implementation in PR #101 keeps the whole feature inside Cloudflare:
- Astro renders the comment UI.
- Cloudflare Pages Functions expose
/api/comments. - Cloudflare D1 stores comments.
- Cloudflare Turnstile protects POST requests.
wrangler.jsoncdefines theCOMMENTS_DBbinding.
That is the main point: the comment feature is not a third-party island inside the page. It uses the same Cloudflare boundary as the rest of the static site.
Architecture
The implementation has a small surface area.
| Layer | File or service |
|---|---|
| UI | src/components/BlogComments.astro |
| Page placement | src/views/BlogPostPage.astro |
| API | functions/api/comments.ts |
| Storage | D1 binding COMMENTS_DB |
| Bot protection | Cloudflare Turnstile |
| Schema | migrations/0001_create_blog_comments.sql |
The UI fetches comments with GET /api/comments?slug=...&locale=... and submits with POST /api/comments.
The Pages Function validates origin, payload, Turnstile, rate limits, duplicates, and blocked content before inserting into D1.
Why D1
Comments are relational enough to benefit from SQL:
- select by
post_slug - order by
created_at - hide rows with
deleted_at - count recent rows by
client_hash - detect duplicate
body_hashvalues
The schema stores a soft delete timestamp instead of physically removing rows. That keeps moderation simple: visible comments are rows where deleted_at IS NULL.
Cloudflare D1 supports prepared statements from Workers and Pages Functions. Using prepare(...).bind(...) keeps comment input out of raw SQL strings.
Wrangler As The Contract
Bindings are defined in wrangler.jsonc.
{
"d1_databases": [
{
"binding": "COMMENTS_DB",
"database_name": "acecore-comments",
},
],
"env": {
"production": {
"d1_databases": [
{
"binding": "COMMENTS_DB",
"database_name": "acecore-comments",
},
],
},
},
}
The important part is keeping the binding name stable while pointing every environment at the single D1 database, acecore-comments.
Cloudflare Pages documentation treats Wrangler configuration as the source of truth for a Pages project, which is exactly what we want for reviewable infrastructure changes.
Turnstile Must Be Verified Server-Side
The widget in the browser is only the beginning. The Pages Function sends the token to Cloudflare Siteverify with the secret key.
It also checks the hostname returned by the verification result. This matters for preview URLs and for preventing tokens from an unexpected hostname from being accepted.
Spam Controls
The first version is intentionally strict.
It rejects:
- URLs
- email addresses
- HTML tags
- Markdown links
- repeated-character spam
- common promotional terms
- honeypot field submissions
It also applies in-memory rate limits and persistent D1-backed limits. The persistent limit uses a salted hash of IP and User-Agent instead of storing raw values.
SEO And Search
The comment UI is marked with data-pagefind-ignore, and comments are loaded client-side. That means comments are not treated as indexed article content.
For a corporate blog, that is intentional. The article body is reviewed content; comments are interaction. If comments should become searchable content, they need approval and static rendering as a separate design.
Summary
External comment services are convenient, but they are not the only option.
If a site already runs on Cloudflare Pages, a lightweight comment feature can live entirely inside Cloudflare: Pages Functions for the API, D1 for storage, Turnstile for abuse prevention, and Wrangler for bindings.
That keeps UI, data, security boundaries, and infrastructure configuration under the same operational model as the site itself.
References
Cloudflare Comments
Architecture for a comment feature built only with Cloudflare
Astro renders the UI, Cloudflare Pages Functions form the API boundary, and D1 and Turnstile are connected as Cloudflare components.
Place the UI in Astro
Place the comment list, submission form, and Turnstile widget below each article.
Receive requests in a Pages Function
`/api/comments` handles GET/POST/OPTIONS, input validation, and CORS.
Store data in D1
Use the `COMMENTS_DB` binding to store comments, hashes, and creation times in SQLite-compatible D1.
Protect it with Turnstile
Validate Cloudflare Turnstile tokens server-side and check the hostname allowlist.
Differences between an external comment service and an in-house Cloudflare implementation
External comment service
- Fast to adopt, but the UI, data location, terms, and rendering speed depend on the service
- External scripts and iframes can easily affect article loading
- Multilingual UI and visual consistency with the site are often constrained
- Comment handling, deletion, and migration depend on the service specification
Built only with Cloudflare
- Pages Functions, D1, and Turnstile provide both the API and storage
- Astro HTML and CSS can fit naturally into the site design
- Wrangler configuration can align the D1 binding with the Cloudflare database name
- You decide the spam controls, deletion process, and scope of stored personal information
Decisions to make before implementation
- Keep comments in the site's Cloudflare architecture instead of entrusting them to an external service
- Use D1 for storage and Cloudflare Pages Functions as the API boundary
- Always validate Turnstile tokens server-side
- Reject URLs, email addresses, HTML, Markdown links, and promotional wording before submission
- Align the D1 database name and COMMENTS_DB binding with the Cloudflare configuration
Related pages
FAQ
Why not use an external comment widget?
External widgets are quick, but UI, data ownership, script loading, moderation, and migration all depend on the service. This implementation keeps those decisions inside the site.
Is D1 enough for comments?
For post_slug based reads, created_at ordering, duplicate checks, and soft deletion, D1 is a good fit. Larger community features need a broader design.
Is client-side Turnstile enough?
No. The Pages Function must verify the Turnstile token with Cloudflare Siteverify before writing to D1.