使用 Cloudflare R2 + Pages 搭建免费图床,并集成到 WordPress 评论系统

使用 Cloudflare R2 + Pages 搭建免费图床,并集成到 WordPress 评论系统

文章目录
文章目录
  1. 1. 最终效果展示
  2. 2. 使用 CloudFlare R2 + Pages 搭建图床
  3. 3. 将 ImgBed 图床集成到 WordPress
  4. 4. 常见问题与故障排查

在去年的博客年度更新日志中,我提到本站的评论框功能过于单薄,只能输入纯文本,我一直希望能为它加入更丰富的交互体验——比如支持加粗、斜体、删除线,能上传图片、添加 Emoji 符号等。

一年后的这个国庆假期,我终于抽时间把它完善了。最终实现的效果比我最初设想的更优雅,也更具可维护性、可扩展性。本文将记录我为 WordPress 评论框增加 Emoji 弹层、以及利用 Cloudflare R2 + Pages 搭建免费图床并无缝集成到评论系统 的完整过程。

当然,除了这两个主要功能,我还在前台增加了评论的删除、编辑和点赞功能。点赞其实更像是“排序工具”——当一些文章下的评论数量过多、质量参差时,我可以通过点赞将优质评论手动置顶。至于这一部分的实现细节,后面再说~

1. 最终效果展示

各位可以先看看下面这个视频的实际展示。当然,如果你愿意的话,也推荐直接在评论区亲手试一试,并欢迎提出建议或改进意见。这次改造,我把 Emoji 弹层图床上传功能 所依赖的 JavaScript 脚本全部拆分成了一个独立模块文件。
这样做的好处是:页面在初始加载时几乎不受任何额外脚本影响,保持简洁与快速。

当访客点击评论框上方的表情按钮 😊 或图片按钮 🖼️ 时,浏览器才会按需加载对应模块,实现延迟加载(Lazy Loading)效果。脚本中设置了文件大小与格式校验,符合要求的图像会自动上传到 Cloudflare 图床,上传成功后返回图片 URL,并自动在评论框中插入带有文章标题的 alt 属性的 <img> 标签。

WordPress 评论系统集成 CF 图床的实际演示

这么做的好处是能让前台 HTML 结构保持优雅简洁,所有功能模块都按需加载,减少首屏体积,从而显著提升访问速度。评论区的图片本身并非核心内容,将它们上传到“赛博善人” Cloudflare 提供的免费 R2 存储,不仅能节省服务器带宽与流量成本,还能借助 Cloudflare 的全球 CDN 加速,让图片加载更快、更稳定。

2. 使用 CloudFlare R2 + Pages 搭建图床

使用 Cloudflare R2 + Pages 搭建图床的整个过程不需要花钱,但需要具备访问外网的条件,以及一张可用的外币信用卡用于验证身份。我最终搭建好的图床地址是 img.shephe.com,目前开放上传功能,大家可以随意体验,甚至收藏备用作为临时图床使用。

我使用的图床程序是开源项目 Cloudflare ImgBed,它是专为 Cloudflare 平台设计的轻量化图床方案,虽然界面简洁,但功能十分完整——支持图片的上传、管理、读取、删除等全链路操作,覆盖文件的完整生命周期。同时提供了鉴权、分目录存储、图片审核、随机图等实用功能,基本满足个人博客或中小站点的使用需求。

下面我将简单记录一下使用 Cloudflare R2 与 Pages 搭建图床的流程,主要包括:创建对象存储(R2 Bucket)、部署 Pages 项目、激活运行环境、以及绑定自定义域名等环节。整个过程相对直观,跟着操作一般不会出错。

2.1 创建 Cloudflare R2 对象存储

R2 是 Cloudflare 推出的对象存储服务,对于个人博客或中小型网站来说,它最大的吸引力在于 免费额度高、带宽不限速、不怕被刷流量。Cloudflare 的基础设施全球加速、稳定可靠,这使得 R2 成为非常理想的“自建图床底层”。

💡 免费额度参考(截至 2025 年):每月前 10 GB 存储免费、每月前 100 万次 A 类请求免费、出口流量 1TB/月免费(超出部分也极为便宜),这对于一个自用图床来说几乎可以忽略成本。


登录 Cloudflare 控制台后,确保你的账户已完成支付方式绑定(验证信用卡)。在左侧菜单中选择 「R2 对象存储」,然后点击右上角的 「创建存储桶」 来新建一个 Bucket。以下是关键参数设置示例(可根据截图参考):

创建 Cloudflare R2 对象存储
创建 Cloudflare R2 对象存储
  • 存储桶名称:自行命名(例如 shephe-imgimgbed
  • 位置:选择 亚太地区北美洲西部,两者在国内访问速度差别不大
  • 默认存储类:选择 标准(Standard) ❗注意:R2 的免费额度仅适用于标准存储类,“不频繁访问”类无法享受免费额度。

点击 「创建存储桶」 按钮后即可成功创建。咱暂时不需要立即配置域名或访问策略,可以在下一小节部署 Cloudflare Pages 模板 时统一设置。R2 只需要保持为一个空的、可用的存储桶即可备用。

2.2 创建 Cloudflare Pages 项目

Cloudflare Pages 是 Cloudflare 推出的前端托管与持续部署平台,专门用于部署静态网站或轻量 Web 应用。它支持从 GitHub、GitLab 等代码仓库直接拉取项目并自动构建发布,与 Cloudflare 的全局 CDN 网络无缝结合。对于图床这样的静态服务来说,Pages 提供了 零运维成本、自动构建、HTTPS 一键启用 的完美方案。


在创建 Pages 项目前,需要先把开源项目 Cloudflare-ImgBed 复制到自己的 GitHub 账号下。在 GitHub 上打开该项目主页后,点击右上角的 「Fork」 按钮。这样你就可以在自己的空间中独立修改、部署,而不会影响原作者的项目。这里的“Fork”直译为“叉”,理解上更像是“复制仓库到自己的账户”。


完成 Fork 后,进入 Cloudflare 控制台 → 计算(Workers) → Workers 和 Pages → 创建应用程序。选择 「Pages」 选项卡,然后点击 「导入现有 Git 存储库」。系统会提示你授权访问 GitHub,选择刚刚 Fork 的 Cloudflare-ImgBed 仓库。
随后配置如下:

  1. 项目名称:可自定义,如 imgbedshephe-imgbed
  2. 生产分支:保持默认 main
  3. 构建命令npm install
  4. 构建输出目录:不填,使用默认根目录
使用 Cloudflare R2 + Pages 搭建免费图床,并集成到 WordPress 评论系统
记得输入构建命令哦

点击 「保存并部署」,Cloudflare 会自动拉取你的 GitHub 仓库并开始首次构建。几分钟后,你就会得到一个以 .pages.dev 结尾的测试域名,例如:test-38av.pages.dev

这时,你的图床前端已经可以通过这个域名访问(通常很慢)。接下来我们将进入关键步骤 —— 激活项目、绑定 R2 存储与自定义域名,让上传功能真正可用。

2.3 绑定 KV 数据库和 R2 对象存储

完成 Pages 项目的初次部署后,我们需要为它绑定两个核心组件:

  • 一个是 KV 数据库(Key-Value 存储),用于记录图片的元数据与索引信息
  • 另一个是前面创建的 R2 对象存储,用于保存实际的图片文件

首先,在 Cloudflare 仪表盘左侧菜单中找到 Workers KV,点击右上角的 「Create Instance」 来新建一个命名空间。命名建议使用:img_url,建议保持这个命名不变,因为 Cloudflare-ImgBed 项目默认配置中使用的就是这个名称。如果使用相同的名称,就无需再修改项目文件或环境变量,节省调试时间。💡


创建好 KV 命名空间后,返回 Pages 项目详情页 → 设置(Settings) → 绑定(Bindings)。在这里我们需要添加两个绑定项,分别是数据库和存储:

使用 Cloudflare R2 + Pages 搭建免费图床,并集成到 WordPress 评论系统
绑定 KV 数据库和 R2 存储桶
  1. KV 命名空间绑定
    • 绑定类型:KV Namespace
    • Variable name:img_url
    • 命名空间:选择刚刚创建的 img_url
  2. R2 存储绑定
    • 绑定类型:R2 Bucket
    • Variable name:R2
    • 存储桶:选择前面创建的 R2 存储(如 img_r2shephe-img

保存绑定后,Pages 项目就能在运行时通过环境变量自动调用这两个资源了。接下来,可以顺手在 自定义域(Custom Domains) 一栏中添加你自己的图床域名(包括域名解析),完成域名绑定后,回到 Pages 项目概览页,点击 「重新部署(Redeploy)」,等待几分钟部署完成即可。

不出意外的话,你现在就能通过自定义域名访问你的图床首页了 🎉。至于 ImgBed 的图床设置与使用非常简单,支持上传、删除、目录、鉴权等功能,可以参考 官方文档 获取更详细说明。

3. 将 ImgBed 图床集成到 WordPress

通过上文第 2 步,我们已经有了可用的免费图床。Cloudflare ImgBed 免费开源但功能完整(WebDAV、API 等),官方文档也清晰,因此很容易结合接口快速把“评论图片上传 + 自动插入 URL”做进 WordPress 评论框。

3.1 HTML 结构调整(comments.php)

在评论框的外层容器中预留一个绝对定位的小工具栏(初始隐藏,聚焦后显示)。下面是可直接参考的结构要点:

'comment_field' =>
    '<div class="grve-form-textarea grve-border">
        <div class="grve-form-inner">
            <label for="comment">本站评论均由人工审核,请耐心等待…!</label>
            <textarea class="grve-form-input-item" id="comment" name="comment" cols="45" rows="10" aria-required="true"></textarea>
            <div id="comment-toolbar" class="hidden">
                <button type="button" id="emoji-btn" class="cmt-btn"><i class="fa-regular fa-face-laugh-squint"></i></button>
                <button type="button" id="img-btn" class="cmt-btn" title="插入图片..."><i class="fa-regular fa-images"></i></button>
            </div>
        </div>
    </div>',
  • .grve-form-innerposition:relative; 以便工具栏与 Emoji 弹层的绝对定位。
  • #comment-toolbar 初始隐藏,聚焦 textarea 后再显示
  • Emoji 弹层由 JS 生成(无需写死 HTML)。

3.2 前端交互逻辑(延迟加载 JS 模块)

先在 footer.php 里临时写入这段脚本进行验证(稳定后可迁至主题核心 JS)。这段脚本实现只有用户点击工具栏按钮时,才按需加载模块,从而保持首屏轻量。以下是我的最终版:

document.addEventListener("DOMContentLoaded", function () {
  fitText(".grve-single-simple-title", 40, 50, 1);
});
window.addEventListener("resize", function () {
  fitText(".grve-single-simple-title", 40, 50, 1);
});
document.addEventListener("DOMContentLoaded", function () {
  const commentBox = document.querySelector("#comment");
  const toolbar = document.querySelector("#comment-toolbar");
  if (!commentBox || !toolbar) return;
  commentBox.addEventListener("focus", () => toolbar.classList.add("active"));
  toolbar.addEventListener("mousedown", (e) => e.preventDefault());
  commentBox.addEventListener("blur", () => {
    if (!commentBox.value.trim()) toolbar.classList.remove("active");
  });
});
let enhancerLoaded = false;
["#emoji-btn", "#img-btn"].forEach((sel) => {
  const btn = document.querySelector(sel);
  if (!btn) return;
  btn.addEventListener("click", async () => {
    if (!enhancerLoaded) {
      enhancerLoaded = true;
      const { default: initCommentEnhancer } = await import(
        "/wp-content/themes/impeka/js/comment-enhancer.js"
      );
      initCommentEnhancer();
      btn.click();
    }
  });
});

3.3 打通图床和 WordPress

这是本节的关键步骤,用来让 ImgBed 图床真正与 WordPress 评论系统互通。简单来说,当用户在评论框中点击图片按钮、选择本地图片后,浏览器会将图片文件上传到你搭建好的 Cloudflare 图床,图床返回图片的外链地址,最后这段链接再自动插入评论输入框中,从而实现“上传即嵌入”的效果。实现上有两条路径可以选择:

  • 前端直连 Cloudflare Worker
    即图片直接通过浏览器上传到 Cloudflare 的 API 接口。这种方式不依赖你的网站后端,理论上速度最快、逻辑最简单。但在国内或部分地区,Cloudflare 的 dev 域名(*.workers.dev*.pages.dev)访问并不稳定,时快时慢,甚至可能无法打开,用户体验受限。此外,API Key 必须暴露在前端,这会带来一定安全风险。
  • 通过 WordPress Ajax 代理转发上传
    这也是我最终采用的方案。思路是让浏览器先把图片上传到自己服务器的一个 Ajax 接口(例如 wp-admin/admin-ajax.php?action=image_upload_proxy),由该接口再通过服务器端的 cURL 请求转发到 Cloudflare 图床。这样可以利用服务器网络带宽和稳定的国际线路,大幅提高成功率;同时,API Key 被保存在服务器端,不会暴露给前端用户。

我目前采用的第二种方式,虽然这种方式会消耗服务器少量性能和上行流量,但对于大多数中小博客来说负担完全可以接受,换来的是稳定性、安全性和可控性。另外,这种方式还能方便地加入防刷机制(如登录校验、上传大小限制等),后期如果想扩展后台上传记录、统计上传次数,也更容易实现。

以下是需要添加进 functions.php 中的本地代理脚本,其中的 api_key 和上传地址根据自己的设置:

// 评论框上传到图床的本地代理
add_action('wp_ajax_image_upload_proxy', 'shephe_image_upload_proxy');
add_action('wp_ajax_nopriv_image_upload_proxy', 'shephe_image_upload_proxy');
function shephe_image_upload_proxy() {
    if (empty($_FILES['file'])) {
        status_header(400);
        header('Content-Type: application/json; charset=utf-8');
        echo json_encode(['error' => 'NO_FILE']);
        wp_die();
    }
    $file = $_FILES['file'];
    $api_key = 'xxx'; // 你的图床 Key(需含 upload 权限)
    $endpoint = 'https://img.shephe.com/upload';
    $query = http_build_query([
        'returnFormat' => 'full',
        'uploadFolder' => 'comments',
        'authCode'     => $api_key,
    ]);
    $url = $endpoint . '?' . $query;
    $ch = curl_init();
    $postFields = [
        'file' => new CURLFile($file['tmp_name'], $file['type'] ?: 'application/octet-stream', $file['name'])
    ];
    curl_setopt_array($ch, [
        CURLOPT_URL => $url,
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => $postFields,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => [
            'Accept: application/json',
            'User-Agent: WP-ImgProxy/1.0'
        ],
        CURLOPT_TIMEOUT => 30,
    ]);
    $body = curl_exec($ch);
    $errno = curl_errno($ch);
    $error = curl_error($ch);
    $code  = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    header('Content-Type: application/json; charset=utf-8');
    if ($errno) {
        status_header(502);
        echo json_encode(['error' => 'CURL_ERROR', 'message' => $error]);
        wp_die();
    }
    if ($code < 200 || $code >= 300) {
        status_header($code);
        echo json_encode(['error' => 'HTTP_'.$code, 'raw' => $body]);
        wp_die();
    }
    echo $body;
    wp_die();
}

以下是我完整的 comment-enhancer.js 文件。这个文件可以看作是评论区功能的「增强模块」,将所有与评论框相关的交互逻辑集中管理,包括 Emoji 弹层、图片上传、文件校验、提示框显示、样式注入 等等。

整个脚本的设计思路是 按需加载 + 模块化封装。默认情况下,页面不会主动加载任何与表情或图像上传相关的脚本,只有当用户真正点击评论框右上方的表情按钮 😊 或图片按钮 🖼️ 时,浏览器才会异步加载该模块文件。这样既能保持前台的极简与快速,又能在用户需要时即时启用完整功能。

// /wp-content/themes/impeka/js/comment-enhancer.js
export default function initCommentEnhancer() {
  const CONFIG = {
    buttonEmoji: '#emoji-btn',
    buttonImage: '#img-btn',
    textarea: '#comment',
    // 使用 WordPress 代理上传
    apiProxy: '/wp-admin/admin-ajax.php?action=image_upload_proxy',
    allowedTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/gif'],
    maxSizeMB: 2,
    stitchTool: 'https://www.shephe.com/tools/img-stitching/'
  };

  /* ---------- Emoji 样式内联 ---------- */
  (function injectEmojiStyles() {
    if (document.querySelector('#comment-enhancer-style')) return;
    const style = document.createElement('style');
    style.id = 'comment-enhancer-style';
    style.textContent = `
    .emoji-picker{
      position:absolute;
      top:44px;
      right:10px;
      width:240px;
      padding:8px;
      background:#fff;
      border:1px solid #e5e7eb;
      border-radius:10px;
      box-shadow:0 8px 24px rgba(0,0,0,.12);
      z-index:1000;
      display:none;
    }
    .emoji-picker.active{display:flex;}
    .emoji-grid{
      display:flex;
      flex-wrap:wrap;
      gap:6px;
    }
    .emoji-item{
      display:inline-flex;
      align-items:center;
      justify-content:center;
      width:32px;
      height:32px;
      font-size:20px;
      line-height:1;
      border:0;
      background:transparent;
      border-radius:6px;
      cursor:pointer;
      transition:transform .12s ease,background .12s ease;
    }
    .emoji-item:hover{
      background:#f3f4f6;
      transform:scale(1.1);
    }`;
    document.head.appendChild(style);
  })();


  /* ---------- 工具函数 ---------- */
  function insertAtCursor(el, html) {
    const start = el.selectionStart ?? el.value.length;
    const end = el.selectionEnd ?? el.value.length;
    const before = el.value.slice(0, start);
    const after = el.value.slice(end);
    el.value = before + html + after;
    el.selectionStart = el.selectionEnd = start + html.length;
    el.focus();
  }

  /* ---------- 自定义提示框 ---------- */
  function showNotice(msg, options = {}) {
    const { type = 'info', link = null } = options;
    let box = document.querySelector('.comment-notice');
    if (!box) {
      box = document.createElement('div');
      box.className = 'comment-notice';
      document.body.appendChild(box);
      const style = document.createElement('style');
      style.textContent = `
        .comment-notice {
          position: fixed;
          top: 35%; left: 50%;
          transform: translateX(-50%);
          background: #fff;
          border: 1px solid #e5e7eb;
          box-shadow: 0 6px 20px rgba(0,0,0,.12);
          padding: 14px 20px;
          border-radius: 10px;
          font-size: 15px;
          z-index: 2000;
          display: none;
          color: #333;
          line-height: 1.5;
        }
        .comment-notice.show { display: block; animation: fadeIn .25s ease; }
        .comment-notice a { border-bottom: 1px dotted #b10b14;color: #b10b14;padding: 0 3px 2px 3px; }
        .comment-notice a:hover { border-bottom: 1px solid #b10b14 }

        @keyframes fadeIn { from {opacity:0; transform:translate(-50%, -10px);} to {opacity:1; transform:translate(-50%, 0);} }
      `;
      document.head.appendChild(style);
    }
    box.innerHTML = msg + (link ? ` <a href="${link}" target="_blank">在线压缩、拼接或转换图片</a>` : '');
    box.classList.add('show');
    setTimeout(() => box.classList.remove('show'), 5000);
  }

  /* ---------- Emoji Picker ---------- */
  (function initEmojiPicker() {
    const btn = document.querySelector(CONFIG.buttonEmoji);
    const textarea = document.querySelector(CONFIG.textarea);
    if (!btn || !textarea) return;
    const container = btn.closest('.grve-form-inner') || textarea.closest('.grve-form-inner');
    if (!container) return;

    let picker = container.querySelector('.emoji-picker'); 
    if (!picker) {
      const emojis = ["😊","😂","😍","🤔","😎","😢","😡","🤮","🙏","👍","👎","🎉","👄","💔","🌹","🐱","🐶","🍕","☕","🌈","👌","🐱","🍉","🚗"];
      picker = document.createElement('div');
      picker.className = 'emoji-picker';
      picker.innerHTML = `
        <div class="emoji-grid">
          ${emojis.map(e => `<button type="button" class="emoji-item" data-emoji="${e}">${e}</button>`).join('')}
        </div>`;
      container.appendChild(picker);
    }

    let closeTimer = null;
    let isHovering = false;

    function openPicker() {
      clearTimeout(closeTimer);
      picker.classList.add('active');
      picker.style.display = 'flex';
    }
    function scheduleClose() {
      clearTimeout(closeTimer);
      closeTimer = setTimeout(() => {
        if (!isHovering) {
          picker.classList.remove('active');
          picker.style.display = 'none';
        }
      }, 2000);
    }

    picker.addEventListener('mouseenter', () => { isHovering = true; clearTimeout(closeTimer); });
    picker.addEventListener('mouseleave', () => { isHovering = false; scheduleClose(); });

    btn.addEventListener('click', (e) => {
      e.stopPropagation();
      if (picker.classList.contains('active')) {
        picker.classList.remove('active');
        picker.style.display = 'none';
      } else { openPicker(); scheduleClose(); }
    });

    picker.addEventListener('click', (e) => {
      const item = e.target.closest('.emoji-item');
      if (!item) return;
      const emoji = item.dataset.emoji;
      const start = textarea.selectionStart;
      const end = textarea.selectionEnd;
      textarea.setRangeText(emoji, start, end, 'end');
      textarea.dispatchEvent(new Event('input', { bubbles: true }));
      textarea.focus();
      openPicker(); scheduleClose();
    });

    document.addEventListener('click', (e) => {
      if (!container.contains(e.target)) {
        picker.classList.remove('active');
        picker.style.display = 'none';
      }
    });
  })();

/* ---------- 图片上传(WP 代理,严格容错稳定版) ---------- */
(function initImageUploader() {
  const btn = document.querySelector(CONFIG.buttonImage);
  const ta  = document.querySelector(CONFIG.textarea);
  if (!btn || !ta) return;

  btn.addEventListener('click', () => {
    const input = document.createElement('input');
    input.type = 'file';
    input.accept = 'image/*';

    input.onchange = async (e) => {
      const file = e.target.files && e.target.files[0];
      if (!file) return;

      if (!CONFIG.allowedTypes.includes(file.type)) {
        showNotice('仅支持 Jpg/Jpeg/Png/Webp/Gif 格式图片');
        return;
      }

      const sizeMB = file.size / 1024 / 1024;
      if (sizeMB > CONFIG.maxSizeMB) {
        showNotice(`最大支持 2MB 图片,当前 ${sizeMB.toFixed(2)}MB<br>👉 `, {
          link: CONFIG.stitchTool
        });
        return;
      }

      const formData = new FormData();
      formData.append('file', file);

      const prevText = btn.textContent;
      btn.disabled = true;
      btn.textContent = '⏳ 上传中…';

      try {
        const resp = await fetch(CONFIG.apiProxy, { method: 'POST', body: formData });
        const text = await resp.text();

        if (!resp.ok) {
          showNotice(`❌ 上传失败:HTTP ${resp.status}`);
          console.warn('Raw response:', text);
          return;
        }

        let data;
        try { data = JSON.parse(text); }
        catch { showNotice('返回数据解析失败'); console.warn('Raw response:', text); return; }

        // 只在明确拿到 src/url 时插入,避免把错误文本当路径
        let imgUrl = null;
        if (Array.isArray(data)) {
          imgUrl = data[0]?.src || data[0]?.url || null;
        } else if (data && typeof data === 'object') {
          const d = Array.isArray(data.data) ? data.data[0] : data.data;
          imgUrl = d?.src || d?.url || data.src || data.url || null;
        }

        if (!imgUrl) {
          showNotice('上传成功但未检测到图片地址 src/url');
          console.warn('Parsed object:', data);
          return;
        }

        if (!/^https?:\/\//i.test(imgUrl)) {
          imgUrl = new URL(imgUrl, 'https://img.shephe.com').toString();
        }

        const pageTitle = document.title.replace(/<[^>]*>?/gm, '').trim() || 'Kevin\'s Space';
        insertAtCursor(ta, `<img src="${imgUrl}" alt="${pageTitle}">`);

        showNotice('✅ 图片上传成功');
      } catch (err) {
        showNotice('⚠ 上传异常:' + err.message);
        console.error(err);
      } finally {
        btn.disabled = false;
        btn.textContent = prevText;
        input.value = '';
        formData.delete('file');
      }
    };

    input.click();
  });
})();
}

在功能层面上,comment-enhancer.js 主要做了以下几件事:

  1. 动态创建 Emoji 弹层
    • 点击表情按钮时,自动在评论框右上角生成弹层。
    • 支持鼠标悬停不关闭、点击插入表情符号、自动淡出等交互。
    • 样式通过脚本内联注入,不依赖外部 CSS 文件。
  2. 图片上传与插入
    • 点击图片按钮后弹出文件选择框,校验文件格式与大小(≤2MB)。
    • 通过 WordPress Ajax 代理上传到 Cloudflare 图床。
    • 上传成功后自动返回图片外链,并在评论框中插入 <img> 标签。
    • alt 属性自动填充为当前文章标题,兼顾可读性与 SEO。
  3. 错误与状态提示
    • 自定义了一个轻量的提示框组件(取代浏览器 alert)。
    • 提示信息会自动淡出,并可带跳转链接(如图片过大时指向图片拼接工具)。
  4. 安全与性能优化
    • 模块加载后自动释放文件对象,避免内存堆积。
    • API Key 保留在服务器端,前端仅调用本地 Ajax 接口,确保安全性。
    • 所有 DOM 注入和样式注册均带防重复判断,避免二次加载冲突。

总结来说,这个文件的定位是一个评论区扩展核心模块,它让原本只能输入纯文本的 WordPress 评论框拥有了轻量级的富媒体能力,同时保持页面整洁、加载迅速、维护方便,后期如果要增加其他富文本格式也很方便。

4. 常见问题与故障排查

在图床与 WordPress 评论框集成的过程中,虽然整体流程不复杂,但由于涉及到前端上传、跨域访问、接口代理和 Cloudflare 的安全机制,不可避免会遇到一些常见问题。比如:

4.1 CORS 报错处理(Worker / 代理)

问题表现:前端控制台出现类似以下错误:

Access to fetch at 'https://img.shephe.com/upload' 
from origin 'https://www.shephe.com' has been blocked by CORS policy

原因分析:
CORS(跨域资源共享)问题通常出现在浏览器直连图床接口的情况下。Cloudflare Pages 或 Workers 默认并不会自动允许其他域名访问接口,当你的图床域名和主站域名不同(如 img.shephe.comwww.shephe.com),浏览器就会拦截请求。

解决方案:

  • 方案 A:使用 Cloudflare Transform Rules
    在 Cloudflare 控制台中,进入 Rules → Transform Rules → HTTP Response Header Modification
    为图床域名添加响应头: Access-Control-Allow-Origin: https://www.shephe.com Access-Control-Allow-Methods: GET,POST,OPTIONS Access-Control-Allow-Headers: Authorization,Content-Type 让主站域名可以合法访问接口。
  • 方案 B:使用 Cloudflare Pages _headers 文件
    在 Pages 项目的根目录下新建 _headers 文件,内容如下: /* Access-Control-Allow-Origin: https://www.shephe.com Access-Control-Allow-Methods: GET,POST,OPTIONS Access-Control-Allow-Headers: Authorization,Content-Type
  • 方案 C:使用 WordPress Ajax 代理(推荐)
    如果网络对 Cloudflare 的访问不稳定,建议直接使用服务器代理。所有上传请求由你自己的域名转发到 Cloudflare,CORS 问题自然消失。这是我最终采用的方案,也是最稳定、安全的做法。

4.2 R2 权限配置问题

问题表现:上传时返回 403 Forbidden 或提示 AccessDenied

原因分析:Cloudflare R2 默认不对外开放写入权限。如果 Pages 项目没有正确绑定 R2 存储桶(Bucket),或者绑定时未选择正确的命名变量名,脚本调用时就无法访问 R2 API。

解决方案:

  1. 打开 Cloudflare 控制台 → Workers 和 Pages → 你的 Pages 项目 → 设置 → Bindings
  2. 检查绑定项是否存在:
    • 类型为 R2 Bucket
    • 变量名为 R2(需与项目代码一致)
  3. 确认所选存储桶为你创建的 R2(如 imgbed)。
  4. 若仍提示无权限,进入 R2 存储设置 → Access Policies,确认 R2 绑定的 API Key 拥有 PutObject 权限。
  5. 重新部署项目,等待生效。

4.3 405 错误的原因与解决

问题表现:上传时浏览器控制台提示:

上传失败:HTTP 405

原因分析:
405 表示 “Method Not Allowed”。这是 Cloudflare ImgBed 的常见错误类型之一。出现该问题通常是以下几种情况:

  • 上传接口调用错误(如请求方法不是 POST);
  • 使用了错误的鉴权方式(应为 authCode 参数,而不是 HTTP Header);
  • 图床部署在 Pages 环境中,而不是 Worker 环境时,未设置正确的路径;
  • 代理层传递文件时未使用 multipart/form-data 格式。

解决方案:

  1. 确保上传时使用 POST 方法。
  2. 检查上传 URL 是否包含正确的参数,例如: https://img.shephe.com/upload?returnFormat=full&uploadFolder=comments&authCode=你的 KEY
  3. 若使用 WordPress Ajax 代理,请确保后端代码使用 curl_file_create() 生成文件字段。
    PHP 示例: 'file' => new CURLFile($file['tmp_name'], $file['type'], $file['name'])
  4. 确认 Pages 项目已绑定 KV 与 R2,并重新部署。

4.4 上传崩溃(fetch 内存溢出)修复思路

问题表现:
浏览器在选择图片上传后,页面卡死或直接崩溃(Chrome、Edge 均会闪退)。

原因分析:

  • 该问题一般与 fetch 上传大文件相关。当使用 fetch 携带 FormData 时,某些浏览器会在序列化过程中占用大量内存;
  • 若同时调用 response.json() 自动解析响应体,就可能导致瞬时内存暴涨,尤其是当返回的错误信息或 HTML 页面内容较大时。

修复思路:

  1. 避免直接使用 response.json()
    改为先读取 response.text(),再尝试手动 JSON.parse(),出现异常时安全退出。
  2. 改用原生 XMLHttpRequest 上传
    传统的 xhr.send(formData) 在流式传输大文件时更稳定,不会因为流序列化而崩溃。
    这也是我后来采用的稳定方案。
  3. 减少临时对象引用
    上传完成后立即释放文件引用,例如: input.value = ''; formData.delete('file');
  4. 文件大小预检查
    通过前端脚本提前阻止大于 2MB 的文件上传,从源头避免卡死。

通过以上几项调整,整个系统运行已非常稳定。无论是从国内访问还是海外访问,图片上传都能快速完成,接口响应时间稳定在 200~400ms 左右。这也验证了:在 Cloudflare Pages + R2 架构下,通过 WordPress Ajax 代理上传是一种性能、安全性、可维护性都兼顾的实现方式。

「使用 Cloudflare R2 + Pages 搭建免费图床,并集成到 WordPress 评论系统」有 10 条评论
  • 的头像
    obaby
    10/20/2025 at 09:05

    看到了,你的这俩东西,开了广告拦截就加载不出来了,😂

    • 的头像
      Kevin
      10/20/2025 at 10:11

      额,是你拦截器不允许这种js加载方式。。

  • 的头像
    obaby
    10/19/2025 at 19:44

    没看到穿图的地方啊?

    • 的头像
      Kevin
      10/19/2025 at 20:28

      看到了么???🌚右边
      使用 Cloudflare R2 + Pages 搭建免费图床,并集成到 WordPress 评论系统

  • hary
    10/19/2025 at 19:28

    单独这个图床部署不错,晚会搞搞试试

    • 的头像
      Kevin
      10/19/2025 at 21:16

      搞起来搞起来,单图床没啥可搞的,一会儿就成功了~

  • 夜未央
    10/19/2025 at 17:25

    你说不复杂,但我看着部署过程好复杂。不过这个用法很好,我一直怕图片体积太大,所以压缩,同时减少发图数量,但这样就得不断取舍,你解决了这个纠结。试了下,你支持2m图片,很奢侈了,我平时都是压缩到300K左右上传的。

    • 的头像
      Kevin
      10/19/2025 at 18:48

      我这是上传到图片的图片尺寸,我WordPress内的实际没有限制大小,不过大多数不会超过2mb,我一般会转成webp再传;

      使用 Cloudflare R2 + Pages 搭建免费图床,并集成到 WordPress 评论系统

  • 的头像
    acevs
    10/19/2025 at 14:57

    加强版使用方法 不错。

发表评论

请输入关键词…