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é:

  1. Nepoužíva externé hostingové služby pre komentáre
  2. Vyžaduje manuálne schválenie pred zobrazením komentárov
  3. Upozorní ma e-mailom, keď niekto komentuje
  4. Chráni pred botmi bez otravných CAPTCHA
  5. 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:

  1. Overenie Turnstile prebehne na serveri
  2. Komentár sa uloží do D1 so stavom status: 'pending'
  3. Vygeneruje sa jedinečný token pre administrátorské akcie
  4. 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:

  1. Zamknúť presné verzie v package.json (bez prefixu ^)
  2. Udržiavať počet závislostí minimálny - len @astrojs/cloudflare a resend
  3. Používať raw D1 dopyty namiesto ORM
  4. 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!

Odkazy #