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

Published: (December 21, 2025 at 08:41 PM EST)
4 min read
Source: Dev.to

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

  1. User submits form → Netlify stores it immediately.
  2. Webhook triggers instantly → Site rebuild starts.
  3. Site queries Netlify API for submissionsToo soon!
  4. API returns old data (submission not yet indexed).
  5. 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

  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:

  • A Netlify site with Forms enabled.
  • A build hook URL (Site settings → Build & deploy → Build hooks).
  • A Netlify Personal Access Token with forms:read permission.
  • The three code files referenced above.

Environment Variables

Set these 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 the JAMstack.

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

Back to Blog

Related posts

Read more »