Note #5 — building my guestbook
I finally shipped a guestbook for my site: pablo.space/guestbook.
I had wanted one for a while, but there was an obvious constraint: my site is built with Eleventy and published on GitHub Pages. That setup is simple, stable, and cheap—but it is also static. A guestbook is not static. It needs a real write path, a real read path, and just enough protection to avoid turning into a spam bucket.
So instead of replacing the site or migrating the whole stack, I kept the site exactly where it already lived and added a thin dynamic layer around it.
The result is straightforward:
-
Eleventy still renders the site.
-
GitHub Pages still serves the frontend.
-
Supabase handles the dynamic part.
-
The guestbook UI talks to a public Edge Function.
-
The postcard backgrounds are local assets from a
notecardsfolder in the project root.
That split was the whole point. I did not want to rebuild my site around the guestbook. I wanted the guestbook to fit the site I already had.
The problem I was actually solving
GitHub Pages is great at serving static files, but it does not run server-side code. That means no native POST endpoint, no database writes, and no request-time backend logic on the host itself. I could have moved the whole site somewhere else, but that would have been an overreaction to a small feature. Instead, I kept the static frontend and moved only the dynamic responsibility elsewhere.
That is where Supabase came in.
Why I used Supabase
Supabase Edge Functions gave me exactly what I needed: a lightweight HTTP endpoint I could call from the browser, backed by a proper database. The guestbook only needed two things:
-
a way to fetch entries
-
a way to submit a new entry
That is simple enough that I did not need a complex application layer. I just needed one clean function and one table.
I also disabled JWT verification for this specific function because the guestbook is intentionally public. Anyone visiting the page should be able to submit a message without signing in. That meant I had to be more deliberate about validation and abuse protection, but that tradeoff was worth it for a public guestbook.
The backend shape
I created a guestbook-api Edge Function in Supabase and used it as the public API.
The function does two jobs:
GET
It returns paginated guestbook entries.
POST
It accepts JSON with these fields:
-
author -
content -
url -
theme -
special
That last field is the honeypot. If it is filled, I treat the request like bot traffic.
The function also handles:
-
CORS headers for browser requests
-
input validation
-
theme validation
-
basic spam heuristics
-
a simple per-IP submission limit using a hashed IP value
This is not enterprise-grade moderation. It does not need to be. It just needs to stop the obvious garbage without making the feature miserable to use.
The database
The table is intentionally small. I only needed the basics:
-
author -
content -
url -
theme -
created_at -
is_spam -
ip_hash
I kept row-level security enabled on the table and let the function be the write layer. I did not want the browser talking directly to the table. That would have been a lazy design and a bad one.
Keeping Eleventy static
The important decision was not trying to make Eleventy behave like a full application framework.
Eleventy stayed responsible for what it is good at:
-
templates
-
page structure
-
layouts
-
asset copying
-
overall site styling
The guestbook page itself is still just a page in the site. The dynamic part happens in the browser through fetch() calls to Supabase.
That meant I could keep the project architecture mostly intact and add a focused client-side layer only where it was actually needed.
The notecard UI
The visual direction came from an existing guestbook I admired on Ky's site. The inspiration was obvious enough that pretending otherwise would be ridiculous, so I would rather say it plainly: the notecard idea came from Ky, and I adapted that feeling to my own site instead of copying the implementation blindly.
I used postcard-style background images stored in a notecards directory at the root of the project. In my case, those assets are .png files. Eleventy copies them into the final output, and the guestbook UI maps the selected theme number to the matching postcard image.
That gave me a simple visual system:
-
each entry carries a
theme -
the frontend resolves that number to a local image
-
the message is rendered on top of that card background
It looks more personal than a generic comment list, which was the whole point. A guestbook should feel like something people leave behind, not something they dump text into.
The frontend behavior
On the frontend, I kept things deliberately boring.
No framework. No extra build system. No unnecessary client SDK. Just a page, a form, a small script, and a couple of fetch() calls.
The guestbook page does four things:
-
loads entries from the Supabase function
-
renders them as themed cards
-
submits new entries through the same function
-
shows loading, success, and error states clearly
That was enough.
I did not need hydration complexity. I did not need React for one form. I did not need to turn the whole site into an app because one page had to talk to an API.
Testing locally before shipping
I did not want to push blind and debug in production like an idiot, so I tested the whole flow locally first.
That meant checking:
-
whether the guestbook page loaded correctly in local Eleventy dev mode
-
whether the notecard images were being copied to the output properly
-
whether
GETreturned entries as expected -
whether
POSTactually created entries -
whether invalid themes failed cleanly
-
whether empty or malformed input was rejected
-
whether the public Edge Function responded correctly to browser requests
That local pass mattered because the hardest bugs in this kind of setup are usually boring integration mistakes:
-
wrong asset paths
-
bad CORS handling
-
incorrect API assumptions
-
path prefix issues on static builds
-
UI code assuming data shape incorrectly
None of those problems are interesting. All of them are common.
Why this approach worked
What I like about this setup is that it respects the boundaries of the tools.
GitHub Pages stays static. Eleventy stays simple. Supabase handles the dynamic part. The browser only does what it needs to do.
That separation made the whole thing easier to reason about.
If I had tried to force everything into GitHub Pages alone, I would have ended up with hacks. If I had migrated the whole site to a different stack just to add a guestbook, I would have solved a small problem with a large, unnecessary disruption.
This was the middle path: minimal change, enough infrastructure, no drama.
What I would improve next
The first version works, but there is still room to refine it.
I want to improve:
-
moderation quality
-
pagination polish
-
the composer experience
-
perhaps a slightly richer presentation for timestamps and entry transitions
But those are refinements, not prerequisites. Shipping the first solid version mattered more than endlessly polishing a feature nobody could use yet.