Bring Back the 90's Guestbook with JAMstack: How I Added Dynamic Comments to My Static 11ty Site
Source: Dev.to
The Architecture: Three Simple Pieces
The solution consists of three interconnected components working in harmony:
1. The Form (Frontend)
A simple HTML form that leverages Netlify Forms:
<form name="guestbook" method="POST" data-netlify="true">
<p>
<label>Name *</label>
<input type="text" name="name" required />
</p>
<p>
<label>Message *</label>
<textarea name="message" required></textarea>
</p>
<p>
<button type="submit">Sign Guestbook</button>
</p>
</form>
The magic here is data‑netlify="true" – this single attribute tells Netlify to intercept form submissions and store them, no backend required.
2. The Webhook (Serverless Function)
When someone submits the form, Netlify triggers a webhook that rebuilds the site with the new entry:
// netlify/functions/guestbook-webhook.js
const fetch = require('node-fetch');
exports.handler = async function (event, context) {
if (event.httpMethod !== 'POST') {
return { statusCode: 405, body: 'Method Not Allowed' };
}
try {
const payload = JSON.parse(event.body);
if (payload.type === 'submission' && payload.data?.name === 'guestbook') {
console.log('New guestbook submission received:', payload.data);
// Wait a bit before triggering rebuild
console.log('Waiting 5 seconds before triggering rebuild...');
await new Promise(resolve => setTimeout(resolve, 5000));
const buildHookUrl = process.env.NETLIFY_BUILD_HOOK_URL;
if (buildHookUrl) {
const response = await fetch(buildHookUrl, {
method: 'POST',
body: JSON.stringify({ trigger: 'guestbook_submission' }),
headers: { 'Content-Type': 'application/json' }
});
if (response.ok) {
console.log('Build triggered successfully');
}
}
}
return {
statusCode: 200,
body: JSON.stringify({ received: true })
};
} catch (error) {
console.error('Webhook error:', error);
return {
statusCode: 500,
body: JSON.stringify({ error: 'Internal Server Error' })
};
}
};
3. The Data Fetcher (11ty Data File)
During build time, 11ty fetches all submissions from Netlify’s API:
// src/_data/guestbook.js
const fetch = require('node-fetch');
module.exports = async function () {
const siteId = process.env.NETLIFY_SITE_ID;
const token = process.env.NETLIFY_FORMS_ACCESS_TOKEN;
if (!token || !siteId) {
console.warn('No Netlify API credentials found. Using sample data.');
return getSampleEntries();
}
// Get form ID first
const formsUrl = `https://api.netlify.com/api/v1/sites/${siteId}/forms`;
const formsResponse = await fetch(formsUrl, {
headers: {
Authorization: `Bearer ${token}`,
'User-Agent': 'curl/7.79.1'
}
});
const forms = await formsResponse.json();
const guestbookForm = forms.find(form => form.name === 'guestbook');
// Fetch submissions with retry logic
const url = `https://api.netlify.com/api/v1/sites/${siteId}/forms/${guestbookForm.id}/submissions`;
let response;
let retries = 3;
let delay = 2000;
while (retries > 0) {
const submissionsResponse = await fetch(url, {
headers: {
Authorization: `Bearer ${token}`,
'User-Agent': 'curl/7.79.1'
}
});
if (submissionsResponse.ok) {
response = await submissionsResponse.json();
break;
} else if (retries > 1) {
console.log(`Retrying in ${delay}ms... (${retries} attempts left)`);
await new Promise(resolve => setTimeout(resolve, delay));
delay *= 2;
retries--;
}
}
// Transform and return entries
return response.map(submission => ({
name: submission.data.name,
message: submission.data.message,
website: submission.data.website || '',
date: new Date(submission.created_at),
id: submission.id
}));
};
The Mystery: Mobile Submissions Vanishing
The guestbook worked perfectly on a laptop, but a mobile submission showed a loading spinner, landed on the success page, and never appeared on the live site. The entry was present in Netlify’s form dashboard, but not on the site.
The root cause turned out to be eventual consistency in distributed systems.
The Race Condition That Taught Me a Lesson
- User submits form → Netlify stores it immediately.
- Webhook triggers instantly → Site rebuild starts.
- Site queries Netlify API for submissions → Too soon!
- API returns old data (submission not yet indexed).
- Site rebuilds without the new entry.
Desktop tests were usually lucky; mobile networks exposed the timing issue.
The Solution: Patience and Retries
- Delay the rebuild – add a short, configurable wait (e.g., 5 seconds) before calling the build hook.
- Retry fetching submissions – implement exponential back‑off retries in the 11ty data file.
These changes ensure submissions from any device appear reliably on the live site.
Why This Matters: The IndieWeb Philosophy
The guestbook is nostalgic and embodies the IndieWeb principle of owning your content. Unlike third‑party comment systems, all data lives on the Netlify account, can be exported, and isn’t locked into another platform.
Lessons Learned
- Static doesn’t mean static: Serverless functions add dynamic features to static sites.
- Timing matters: Distributed systems aren’t instantaneous; consider race conditions.
- Test on real networks: Desktop Wi‑Fi differs from mobile 4G.
- Simple is powerful: Three small files replace an entire backend.
The Complete Flow
- User fills form → Netlify stores submission.
- Netlify sends webhook → Serverless function receives it.
- Function waits 5 seconds → Triggers build hook.
- 11ty builds site → Fetches submissions with retry logic.
- Site deploys → New message appears automatically.
Try It Yourself
Want to add a guestbook to your 11ty site? Here’s what you need:
- A Netlify site with Forms enabled.
- A build hook URL (Site settings → Build & deploy → Build hooks).
- A Netlify Personal Access Token with
forms:readpermission. - The three code files referenced above.
Environment Variables
Set these in Netlify:
NETLIFY_FORMS_ACCESS_TOKENNETLIFY_SITE_IDNETLIFY_BUILD_HOOK_URL
That’s it. No database, no server, no maintenance—just the magic of the JAMstack.
This post originally appeared on my blog. Follow me for more tales from the IndieWeb frontier!