Guestbook Comment System — Design Spec
Overview
A page-specific guestbook/comment system for hellahung.com, built entirely on Cloudflare's stack (Pages Functions + D1). Every page gets a retro-styled comment section at the bottom (above the footer) where visitors can leave messages and reply to existing comments. All submissions require manual admin approval before going live.
Stack
- Frontend: Eleventy static HTML + vanilla JS for comment interactivity
- API: Cloudflare Pages Functions (
functions/api/)
- Database: Cloudflare D1 (SQLite)
- CAPTCHA: Cloudflare Turnstile (free tier)
- SSR for bots: Cloudflare Pages Middleware (
functions/_middleware.js)
Data Model
comments table
| Column |
Type |
Notes |
| id |
INTEGER |
Primary key, autoincrement |
| page_slug |
TEXT |
Which page the comment belongs to (e.g. "bio") |
| name |
TEXT |
Display name of the commenter |
| message |
TEXT |
Comment body |
| status |
TEXT |
pending / approved / rejected |
| is_admin |
INTEGER |
1 if posted by admin as a verified name |
| created_at |
TEXT |
ISO 8601 timestamp |
replies table
| Column |
Type |
Notes |
| id |
INTEGER |
Primary key, autoincrement |
| comment_id |
INTEGER |
FK to comments.id |
| name |
TEXT |
Display name of the replier |
| message |
TEXT |
Reply body |
| status |
TEXT |
pending / approved / rejected |
| is_admin |
INTEGER |
1 if posted by admin as a verified name |
| created_at |
TEXT |
ISO 8601 timestamp |
restricted_names table
| Column |
Type |
Notes |
| id |
INTEGER |
Primary key, autoincrement |
| name |
TEXT |
Case-insensitive restricted name (e.g. "hellahung", "fudster") |
admin_passwords table
| Column |
Type |
Notes |
| id |
INTEGER |
Primary key, autoincrement |
| label |
TEXT |
Human-readable label (e.g. "Fudster") |
| password_hash |
TEXT |
Hashed password (bcrypt or similar) |
API Endpoints (Pages Functions)
All live under functions/api/.
Public endpoints
| Method |
Path |
Description |
| GET |
/api/comments?page={slug}&p={pageNum} |
Fetch approved comments for a page (5 per page, with their approved replies) |
| POST |
/api/comments |
Submit a new comment (requires Turnstile token) |
| POST |
/api/replies |
Submit a reply to a comment (requires Turnstile token) |
Admin endpoints (password-protected)
| Method |
Path |
Description |
| POST |
/api/admin/login |
Authenticate with password, returns session token |
| GET |
/api/admin/pending |
List all pending comments and replies |
| POST |
/api/admin/moderate |
Approve or reject a comment/reply by ID |
| POST |
/api/admin/post |
Post as a verified/restricted name (skips approval, gets badge) |
| GET |
/api/admin/names |
List restricted names |
| POST |
/api/admin/names |
Add a restricted name |
| DELETE |
/api/admin/names/:id |
Remove a restricted name |
| GET |
/api/admin/passwords |
List admin passwords (labels only, not hashes) |
| POST |
/api/admin/passwords |
Add an admin password |
| DELETE |
/api/admin/passwords/:id |
Remove an admin password |
Visitor Flow
- Visitor scrolls to bottom of any page — sees the guestbook section
- Approved comments are displayed, 5 per page, with page number navigation (1, 2, 3...)
- Each comment shows its approved replies nested flat underneath (1 level deep, unlimited replies per comment)
- Visitor fills in name + message, solves Turnstile CAPTCHA
- On submit:
- Name is checked against
restricted_names (case-insensitive) — if match, rejected immediately with an error message ("This name is reserved")
- Otherwise, saved to D1 with
status: pending
- Visitor sees: "Thanks! Your message is awaiting approval."
- Visitor can click "Reply" on any comment to expand an inline reply form (same flow: name + message + Turnstile)
Admin Flow
- Admin navigates to
/admin — sees a login form
- Enters their password — authenticated via hashed comparison against
admin_passwords table
- Admin dashboard shows:
- Pending queue: All pending comments and replies, with approve/reject buttons
- Post as verified: Form to post a comment or reply as a restricted name (e.g. "hELLA HUNG"). These skip approval and go live immediately with a verified badge.
- Manage restricted names: Add/remove names from the restricted list
- Manage passwords: Add/remove admin passwords (each moderator gets their own)
- Session managed via a simple token stored in a cookie or localStorage
Guestbook UI
Placement
- Bottom of every page, above the footer
- Injected via the Eleventy layout (
base.njk and themed.njk)
Styling
- Gold-bordered section matching the existing retro aesthetic
- Section header: "GUESTBOOK" in red with glow, subtext in aqua
- Comment names in gold with timestamp
- Comment body in white/light gray
- Admin/verified comments get a star or checkmark badge next to the name
- Reply button in aqua, reply form bordered in aqua
- Submit button: gold border, gold text ("SIGN IT!" for comments, "REPLY!" for replies)
- Page numbers at the bottom of the comment list, styled as gold links
Pagination
- 5 comments per page
- Page number navigation (1, 2, 3...) — not "load more"
- Loaded via JavaScript API calls on page change
- First page of comments visible on initial load
SEO / Googlebot Support
- Middleware (
functions/_middleware.js) intercepts all HTML page requests
- Checks the
User-Agent header for known bots (Googlebot, Bingbot, etc.)
- Bot requests only: Queries D1 for the first 5 approved comments for that page slug and injects them as static HTML into the page before serving
- Regular visitors: Get the plain static HTML; comments load via client-side JavaScript
- This ensures comments are indexed by search engines without adding D1 query overhead for every visitor
Security
- Turnstile CAPTCHA on all public comment/reply submissions
- Restricted names blocked at submission time (case-insensitive matching)
- Admin passwords stored as hashes (never plaintext)
- Admin session tokens with expiration
- Rate limiting on comment/reply submissions (Cloudflare's built-in or simple in-memory tracking)
- Input sanitization on all user-submitted content (prevent XSS — escape HTML in names and messages)
File Structure (New Files)
functions/
_middleware.js # Bot detection + SSR for comments
api/
comments.js # GET (list) + POST (submit) comments
replies.js # POST submit reply
admin/
login.js # POST authenticate
pending.js # GET pending items
moderate.js # POST approve/reject
post.js # POST as verified name
names.js # GET/POST/DELETE restricted names
passwords.js # GET/POST/DELETE admin passwords
db/
schema.sql # D1 schema (all tables)
seed.sql # Initial restricted names + first admin password
admin.html # Admin dashboard page
_includes/
guestbook.njk # Guestbook partial (injected into layouts)
Out of Scope (for now)
- Email/webhook notifications on new comments
- Emoji reactions or upvotes on comments
- Image uploads in comments
- OAuth or third-party auth for admin (using simple passwords)
- Comment editing by visitors after submission