用 JAMstack 复刻 90 年代的访客留言本:我如何在静态 11ty 网站中添加动态评论

发布: (2025年12月22日 GMT+8 09:41)
6 min read
原文: Dev.to

I’m happy to translate the article for you, but I’ll need the full text you’d like translated (everything after the source line). Please paste the content here, and I’ll provide a Simplified Chinese version while preserving the formatting, code blocks, URLs, and technical terms as requested.

架构:三个简单组成部分

解决方案由三个相互关联、协同工作的组件组成:

1. 表单(前端)

一个利用 Netlify Forms 的简易 HTML 表单:

<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>

这里的关键是 data‑netlify="true" —— 这个属性告诉 Netlify 拦截表单提交并存储,无需后端。

2. Webhook(无服务器函数)

当有人提交表单时,Netlify 会触发一个 webhook,使用新条目重新构建站点:

// 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. 数据获取器(11ty 数据文件)

在构建阶段,11ty 会从 Netlify 的 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
  }));
};

神秘现象:移动端提交消失

客人簿在笔记本电脑上运行完美,但在移动设备上提交时出现加载旋转图标,跳转到成功页面,却从未出现在现场页面。该条目在 Netlify 的表单仪表盘中可见,但在站点上没有显示。

根本原因是 分布式系统中的最终一致性

教会我一课的竞争条件

  1. 用户提交表单 → Netlify 立即存储。
  2. Webhook 立即触发 → 网站重建开始。
  3. 网站查询 Netlify API 获取提交太快了!
  4. API 返回旧数据(提交尚未被索引)。
  5. 网站在没有新条目的情况下重建。

桌面测试通常比较走运;移动网络则暴露了时间问题。

解决方案:耐心与重试

  • 延迟重建 – 在调用构建钩子之前添加一个短暂、可配置的等待(例如 5 秒)。
  • 重试获取提交 – 在 11ty 数据文件中实现指数退避重试。

这些更改可确保来自任何设备的提交可靠地显示在线上站点上。

为什么这很重要:IndieWeb 哲学

访客簿带有怀旧感,并体现了 IndieWeb 的 拥有你的内容 原则。与第三方评论系统不同,所有数据都存储在 Netlify 账户中,可以导出,并且不会被锁定在其他平台上。

Lessons Learned

  • 静态并不等于静态: Serverless 函数为静态站点添加动态功能。
  • 时机很重要: 分布式系统并非瞬时完成;需要考虑竞争条件。
  • 在真实网络上测试: 桌面 Wi‑Fi 与移动 4G 不同。
  • 简洁即强大: 三个小文件即可取代整个后端。

完整流程

  1. 用户填写表单 → Netlify 存储提交。
  2. Netlify 发送 webhook → 无服务器函数接收。
  3. 函数等待 5 秒 → 触发构建钩子。
  4. 11ty 构建站点 → 使用重试逻辑获取提交。
  5. 站点部署 → 新消息自动出现。

亲自尝试

想在你的 11ty 网站上添加留言簿吗?以下是你需要的:

  • 一个已启用 Forms 的 Netlify 站点。
  • 一个 构建钩子 URL(站点设置 → 构建与部署 → 构建钩子)。
  • 一个具有 forms:read 权限的 Netlify 个人访问令牌
  • 上文提到的三个代码文件。

环境变量

在 Netlify 中设置这些:

  • NETLIFY_FORMS_ACCESS_TOKEN
  • NETLIFY_SITE_ID
  • NETLIFY_BUILD_HOOK_URL

就这样。无需数据库、服务器或维护——只有 JAMstack 的魔力。

本文最初发布在我的博客。关注我,获取更多 IndieWeb 前沿的故事!

Back to Blog

相关文章

阅读更多 »