cloudflare + github 实现留言板

在 小白服务器踩坑(1)中,我们成功部署网站上线,现在为网站实现留言板功能

实现原理

采用 Github Issues + CloudFlare Worker 实现,在 Worder 添加函数实现留言内容接收,并提交至 Github Issues,Github Issues 来存放留言数据,也方便网站显示留言记录

这样做的好处不需要数据库,也不需要部署后台服务,Github Issues 审核,分类,标签也方便管理留言

创建 Worker

Worker 只是一个函数,前端调用接口实际是执行这个函数,进入 cloudflare,选择计算机和AI/Workders和Pages创建应用程序,选择从 Hello World!开始

可以先部署程序,部署成功后修改程序。在程序详情页面,右上角点击编辑代码,添加提交留言的代码

php 复制代码
const corsHeaders = {
  "Access-Control-Allow-Origin": "*", // 本地 + 生产都能用
  "Access-Control-Allow-Methods": "POST, GET, OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type",
};

export default {
  async fetch(request, env) {
    // =========================
    // 1️⃣ 处理 CORS 预检请求
    // =========================
    if (request.method === "OPTIONS") {
      return new Response(null, {
        status: 204,
        headers: corsHeaders,
      });
    }

    // =========================
    // 2️⃣ GET:健康检查
    // =========================
    if (request.method === "GET") {
      return new Response(
        JSON.stringify({
          status: "ok",
          message: "Comment API is running",
        }),
        {
          headers: {
            "Content-Type": "application/json",
            ...corsHeaders,
          },
        }
      );
    }

    // =========================
    // 3️⃣ 只允许 POST
    // =========================
    if (request.method !== "POST") {
      return new Response("Method Not Allowed", {
        status: 405,
        headers: corsHeaders,
      });
    }

    // =========================
    // 4️⃣ 正常处理留言
    // =========================
    let data;
    try {
      data = await request.json();
    } catch {
      return new Response("Invalid JSON", {
        status: 400,
        headers: corsHeaders,
      });
    }

    const { content } = data;

    if (!content) {
      return new Response("Content is required", {
        status: 400,
        headers: corsHeaders,
      });
    }

    // -------------------------
    // 创建 GitHub Issue
    // -------------------------
    const issueBody = content;

    const res = await fetch(
      `https://api.github.com/repos/${env.GITHUB_REPO}/issues`,
      {
        method: "POST",
        headers: {
          Authorization: `Bearer ${env.GITHUB_TOKEN}`,
          "Content-Type": "application/json",
          "User-Agent": "CF-Worker",
        },
        body: JSON.stringify({
          title: "用户留言",
          body: issueBody,
          labels: ["pending"],
        }),
      }
    );

    if (!res.ok) {
      const err = await res.text();
      return new Response(err, {
        status: 500,
        headers: corsHeaders,
      });
    }

    // -------------------------
    // 发送通知(Telegram 或邮件)
    // -------------------------
    try {
      // Telegram 机器人通知
      if (env.TELEGRAM_BOT_TOKEN && env.TELEGRAM_CHAT_ID) {
        const telegramMessage = `📝 新留言\n\n${content}`;
        await fetch(
          `https://api.telegram.org/bot${env.TELEGRAM_BOT_TOKEN}/sendMessage`,
          {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
            },
            body: JSON.stringify({
              chat_id: env.TELEGRAM_CHAT_ID,
              text: telegramMessage,
            }),
          }
        );
      }

      // 邮件通知(使用 SendGrid)
      if (env.SENDGRID_API_KEY && env.NOTIFICATION_EMAIL) {
        await fetch("https://api.sendgrid.com/v3/mail/send", {
          method: "POST",
          headers: {
            Authorization: `Bearer ${env.SENDGRID_API_KEY}`,
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            personalizations: [
              {
                to: [{ email: env.NOTIFICATION_EMAIL }],
                subject: "新留言通知",
              },
            ],
            from: { email: env.FROM_EMAIL || env.NOTIFICATION_EMAIL },
            content: [
              {
                type: "text/plain",
                value: content,
              },
            ],
          }),
        });
      }
    } catch (notifyError) {
      // 通知失败不影响主流程,只记录错误
      console.error("通知发送失败:", notifyError);
    }

    return new Response(
      JSON.stringify({ success: true }),
      {
        headers: {
          "Content-Type": "application/json",
          ...corsHeaders,
        },
      }
    );
  },
};

机器人和邮箱通知可选,因为要用我们自己的 Github 账号提交 Issues,还需要在 Worker 里配置变量

GITHUB_REPO是保存 Issues 的项目名,GITHUB_TOKEN需要在 github 生成 token

Github Token

登录 github,打开Settings/Developer settings,选择Personal access tokens/Token(classic),

点击Generate new token/Generate new token(classic),填写 token 备注,选择有效时长,不需要勾选其他权限,创建 token,复制 token 到上一步设置的GITHUB_TOKEN变量

前端提交

前端核心就是调用 cloudflare 创建的 worker api 地址

php 复制代码
  const response = await fetch("https://comment.staryou.workers.dev", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      content: content,
    }),
  });

前端调用 api 成功,worker 获取前端提交 content 内容,将 issues 状态设为 pending(github 自行设定),我们可以修改 issues 状态,方便前端筛选留言

前端调用留言 api,这样就实现了留言过滤和分页显示

perl 复制代码
https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/issues?labels=approved&state=open&per_page=${ISSUES_PER_PAGE}&page=${page}

其他

worker 中仓库名配置错误,前端控制台报错

生成 github token 最好选择 classic,避免权限问题

相关推荐
卸任10 小时前
Vibe coding工具使用心得
前端·ai编程
brien911 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance11 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M11 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写11 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李11 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易12 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马12 小时前
前沿视界
前端
kyriewen12 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易12 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员