Roky som prevádzkoval tento blog na Zole bez akéhokoľvek systému komentárov. Statická povaha Zoly sťažovala pridanie komentárov a nelákal ma ani žiadny z riešení tretích strán, ako Disqus alebo Giscus. Keď som migroval na Astro (v čase písania ešte nezverejnené), príležitosť postaviť niečo vlastné sa konečne naskytla.
Požiadavky #
Chcel som riešenie, ktoré:
- Nepoužíva externé hostingové služby pre komentáre
- Vyžaduje manuálne schválenie pred zobrazením komentárov
- Upozorní ma e-mailom, keď niekto komentuje
- Chráni pred botmi bez otravných CAPTCHA
- Nevyžaduje prebudovanie stránky pre nové komentáre
Posledný bod bol kľúčový. Pri generátoroch statických stránok zvyčajne potrebujete prebudovať a znovu nasadiť vždy, keď sa zmení obsah. Pre komentáre je to nepraktické.
Stack #
Po nejakom prieskume som sa rozhodol pre:
- Cloudflare D1 na ukladanie komentárov (SQLite na edge)
- Astro Server Islands pre dynamické načítavanie komentárov
- Resend pre e-mailové notifikácie
- Cloudflare Turnstile pre ochranu pred botmi (zadarmo, na rozdiel od reCAPTCHA)
Krása Server Islands spočíva v tom, že hlavný článok ostáva statický a rýchly, pričom sa dynamicky načítava len sekcia komentárov. Vyhľadávače môžu stále indexovať statický obsah a používatelia uvidia komentáre bez úplného obnovenia stránky.
Databázová schéma #
Schéma D1 je zámerne jednoduchá:
CREATE TABLE comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
post_slug TEXT NOT NULL,
parent_id INTEGER,
author TEXT NOT NULL,
content TEXT NOT NULL,
status TEXT DEFAULT 'pending',
created_at TEXT DEFAULT (datetime('now')),
token TEXT UNIQUE
);
CREATE INDEX idx_comments_post_status ON comments(post_slug, status);
CREATE INDEX idx_comments_parent ON comments(parent_id);
Pole token je náhodný reťazec používaný pre schvaľovacie/zamietacie
odkazy v e-maile. parent_id umožňuje jednoúrovňové vlákna - odpovede na
komentáre, ale nie odpovede na odpovede.
Schvaľovací tok #
Keď niekto odošle komentár:
- Overenie Turnstile prebehne na serveri
- Komentár sa uloží do D1 so stavom
status: 'pending' - Vygeneruje sa jedinečný token pre administrátorské akcie
- Resend mi pošle e-mail s obsahom komentára a dvoma odkazmi
E-mail vyzerá zhruba takto:
New comment on: some-post-slug
Author: John
---
This is the comment content...
---
[APPROVE] | [REJECT]
Kliknutím na Schváliť sa stav zmení na approved. Kliknutím na Zamietnuť
sa komentár úplne vymaže. Žiadny administrátorský panel nie je potrebný -
všetko sa deje cez e-mailové odkazy.
Server Islands v praxi #
Komponent komentárov je obalený atribútom server:defer:
<CommentsSection server:defer postSlug={slug}>
<p slot="fallback">Loading comments...</p>
</CommentsSection>
Toto hovorí Astru, aby vykreslil komponent na serveri v čase požiadavky, nie pri buildovaní. Hlavná stránka sa načíta okamžite z CDN Cloudflare a sekcia komentárov sa stiahne oddelene. Používatelia na chvíľu uvidia “Loading comments…” a potom sa zobrazia skutočné komentáre.
Samotný komponent je priamočiary - vypýta si schválené komentáre z D1 a vykreslí ich:
const { results: comments } = await db
.prepare(
"SELECT * FROM comments WHERE post_slug = ? AND status = 'approved'"
)
.bind(postSlug)
.all()
Lokálny vývoj #
Jeden záker: D1 bindings nie sú dostupné pri buildovaní, len v čase
požiadavky. Pre lokálny vývoj poskytuje adaptér @astrojs/cloudflare
platform proxy, ktorý simuluje prostredie Cloudflare:
adapter: cloudflare({
platformProxy: {
enabled: true,
persist: ".wrangler/state",
},
}),
Spustenie npm run dev teraz funguje s lokálnym D1. Dáta pretrvávajú medzi
reštartmi v adresári .wrangler/state.
Integrácia Turnstile #
Turnstile je bezplatná alternatíva CAPTCHA od Cloudflare. Vo väčšine prípadov beží neviditeľne a zobrazuje výzvu len keď zaznamená podozrivé správanie. Overenie prebieha na serveri:
const turnstileResponse = await fetch(
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
{
method: "POST",
body: new URLSearchParams({
secret: env.TURNSTILE_SECRET_KEY,
response: turnstileToken,
}),
}
)
const result = await turnstileResponse.json()
if (!result.success) {
return new Response(JSON.stringify({ error: "Bot verification failed" }))
}
Naozaj sa mi nepáčili 38-sekundové hádanky “vyberte všetky semafory”, ale keďže s Turnstile zatiaľ nemám skúsenosti, uvidíme, ako to dopadne.
Čo s newsletterom? #
Kým som bol pri tom, pridal som aj formulár na odber newslettera pomocou Resend Audiences. Rovnaký vzor: overenie Turnstile, potom pridanie e-mailu do Resend Audience. Resend automaticky spravuje odhlasovacie odkazy pri odosielaní hromadných e-mailov. Odberateľ uvidí jednoduché “Subscribed!” na formulári.
Obavy zo závislostí #
Keď som prichádzal zo Zoly, obával som sa nestability ekosystému npm. Zola je jediný binárny súbor v Ruste, ktorý sa takmer nemení. Astro má desiatky závislostí.
Moja stratégia na zmiernenie rizika:
- Zamknúť presné verzie v
package.json(bez prefixu^) - Udržiavať počet závislostí minimálny - len
@astrojs/cloudflarearesend - Používať raw D1 dopyty namiesto ORM
- Vyhýbať sa ťažkým integráciám
Kód D1 a Turnstile používa štandardné Web API, takže by mal ostať stabilný roky. Ak by sa Astro v budúcej verzii zlomilo, jadrovú logiku možno extrahovať.
Záver #
Výsledné riešenie má presne tie funkcie, ktoré som chcel, s minimálnym počtom pohyblivých dielov. Komentáre sa načítavajú dynamicky bez prebudovania, ochrana pred botmi je neviditeľná a schvaľovanie prebieha cez e-mail. Celkový počet nových závislostí: dve.
Niekedy je najlepším prístupom postaviť presne to, čo potrebujete, namiesto siahnutia po službe tretej strany. Užívaj!