DEV Community

Cover image for Bring Back the 90's Guestbook with JAMstack: How I Added Dynamic Comments to My Static 11ty Site
Brennan K. Brown
Brennan K. Brown

Posted on

Bring Back the 90's Guestbook with JAMstack: How I Added Dynamic Comments to My Static 11ty Site

Remember the guestbooks of the early web? Those digital sign-in pages where visitors left their mark before the era of social media? As I built my IndieWeb site with 11ty, I missed that personal touch. But here's the catch: my site is statically generated. How do you accept user submissions on a site that never runs server code?

This is the story of how I revived the classic guestbook using Netlify's serverless platform, complete with a real-world debugging adventure that taught me about the delicate timing of distributed systems.

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" action="/guestbook-success" class="guestbook-form">
  <div class="form-group">
    <label for="name">Name *</label>
    <input type="text" id="name" name="name" required>
  </div>

  <div class="form-group">
    <label for="message">Message *</label>
    <textarea id="message" name="message" rows="4" required></textarea>
  </div>

  <button type="submit" class="btn">Sign Guestbook</button>
</form>
Enter fullscreen mode Exit fullscreen mode

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' })
    };
  }
};
Enter fullscreen mode Exit fullscreen mode

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
  }));
};
Enter fullscreen mode Exit fullscreen mode

The Mystery: Mobile Submissions Vanishing

Here's where it gets interesting. The guestbook worked perfectly when I tested it from my laptop. But when my partner tried signing from her phone, she'd stare at a loading spinner, eventually land on the success page, and... nothing. The message would appear in Netlify's form dashboard, but never on the live site.

I spent a good amount of time debugging this. Was it a mobile browser issue? A network problem? A CSS bug hiding the messages?

The real culprit was something far more subtle: eventual consistency in distributed systems.

The Race Condition That Taught Me a Lesson

Here's what was happening:

  1. User submits form → Netlify stores it immediately
  2. Webhook triggers instantly → Site rebuild starts
  3. Site queries Netlify API for submissions → Too soon!
  4. API returns old data (submission not yet indexed)
  5. Site rebuilds without the new entry

The submission existed, but Netlify's API needed a moment to index it. On desktop, I was usually lucky with timing. On mobile networks with variable latency, the race condition was exposed.

The Solution: Patience and Retries

I implemented a two-pronged fix:

  1. Delay the webhook: Wait 5 seconds before triggering the rebuild
  2. Add retry logic: If the API fails, retry with exponential backoff

This transformed a flaky, timing-dependent system into a robust one that works consistently across all devices.

Why This Matters: The IndieWeb Philosophy

This guestbook is nostalgic as hell, and it embodies the IndieWeb principle that you should own your content. Unlike Disqus or other third-party comment systems, all data lives on my Netlify account. I control it, I can export it, and I'm not locked into anyone's platform.

Lessons Learned

  1. Static doesn't mean static: With serverless functions, static sites can have dynamic features
  2. Timing matters: Distributed systems aren't instantaneous. Always consider race conditions
  3. Test on real networks: Desktop WiFi is not the same as mobile 4G
  4. Simple is powerful: Three small files replace an entire backend infrastructure

The Complete Flow

  1. User fills form → Netlify stores submission
  2. Netlify sends webhook → Serverless function receives it
  3. Function waits 5 seconds → Triggers build hook
  4. 11ty builds site → Fetches submissions with retry logic
  5. Site deploys → New message appears automatically

Try It Yourself

Want to add a guestbook to your 11ty site? Here's what you need:

  1. A Netlify site with Forms enabled
  2. A build hook URL (Site settings > Build & deploy > Build hooks)
  3. A Netlify Personal Access Token with forms:read permission
  4. The three code files above

Set these environment variables in Netlify:

  • NETLIFY_FORMS_ACCESS_TOKEN
  • NETLIFY_SITE_ID
  • NETLIFY_BUILD_HOOK_URL

That's it. No database, no server, no maintenance. Just the magic of JAMstack.

The guestbook may seem like a relic, but it represents something timeless. The joy of direct connection on the open web. The simplicity of static sites and the power of serverless functions means we can have the best of both worlds. Blazing-fast, secure websites that still support human interaction.

Now if you'll excuse me, I have a guestbook to check. I wonder who signed it today?


This post originally appeared on my blog. Follow me for more tales from the IndieWeb frontier!

Top comments (0)