Rokcso's blog

在 Hugo 复刻 Bear Blog 的 Upvote Button

我的博客最开始使用的是 Hugo Bear Blog 这个主题,它移植于 Bear Blog,我注意到 Hugo Bear Blog 的 GitHub 仓库里 第一个 Issue 就是 Add »Toast this post«-button,这是想复刻 Bear Blog 的文章点赞功能,感兴趣可以访问 Bear Blog 创始人 Herman 的博客 看看效果。

这个 Issue 时隔 4 年多都没有被解决,所以我决定一试。

通过一番检索,我发现了 Emactionhugo-cf-worker 这两个项目,它们都是给博客文章增加类似点赞的功能,并且都使用了 Cloudflare Workers + D1,我也打算采用类似的思路。

接口设计

我设计了两个接口:

  1. 接口 A:当用户访问文章时,返回该文章的点赞数量、该用户对该文章的点赞状态
  2. 接口 B:当用户对文档点赞时,上报一条点赞记录,返回该文章的最新点赞数量、该用户对该文章的最新点赞状态

由于接口非常简单,数据结构也非常简单,使用 Cloudflare D1 多少有点大材小用了,使用 Cloudflare KV 则刚刚好,所以我设计了两个 KV 空间:

KV 空间 作用 key value
UPVOTE_COUNT 存储每篇文章的点赞数量 count:${postId} 点赞数量(Int
UPVOTE_RECORD 存储每一条点赞记录 upvote:${postId}:${ip} 点赞操作(Int,只记录 0 或 1)

因为需要记录用户对文章的点赞状态,就需要能够识别用户,考虑到用户隐私以及实现复杂性,我选择了最简单但是不稳定的 IP 作为用户标识,其实也够用了~

设计好接口后就可以编写 Workers 代码了,代码相对简单,并且 KV 空间的操作语法也是简单到极致(至少比数据库操作省事很多),就一个 .get().put() 方法就够了。

具体的代码我已经上传到 GitHub,并且附带了详细的部署指南。

前端接入

开发好接口后还需要在主题前端中接入接口,首先在主题用来渲染文章页面的 HTML 代码中插入一个点赞按钮:

<div class="upvote-container">
    <small class="upvote">
        <button class="upvote-btn" id="upvote-btn">
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" class="css-i6dzq1">
                <polyline points="17 11 12 6 7 11"></polyline>
                <polyline points="17 18 12 13 7 18"></polyline>
            </svg>
            <span class="upvote-count" id="upvote-count">0</span>
        </button>
    </small>
</div>

然后直接在这个 HTML 中引入一个 JavaScript 脚本来调用接口并根据接口返回结果更新 DOM 元素即可,比如:

// 获取 Upvote 数量的方法,支持设置重试次数,默认不重试
async function getCount(slug, retryCount = 0) {
    try {
        const response = await fetch('{{ .Site.Params.upvoteURL }}count?post=' + slug, {
            method: 'GET',
            headers: {
                'Content-Type': 'application/json',
            },
        });
        const data = await response.json();

        if (data.code === 0) {
            const count = data.data.count;
            upvoteCount.innerText = count;
            hasUpvoted = data.data.hasUpvoted;
            if (hasUpvoted) {
                upvoteBtn.classList.add('upvoted');
            } else {
                upvoteBtn.classList.remove('upvoted');
            }
        } else {
            console.error('Failed to get upvote count: ', data.msg);
        }
    } catch (error) {
        console.error('Error: ', error);
        if (retryCount > 0) {
            setTimeout(() => {
                getCount(slug, retryCount - 1);
            }, 1000);
        }
    }
}

完整代码依然已上传至 GitHub

感觉这个项目的后端(接口)部分的方案超适合做一些有趣的 API 服务,Cloudflare 真好用!

我已经将这个功能整合到 Hugo Bear Neo 这个主题中了,这个主题的目标依然是与 Bear Blog(以及 Hugo Bear Blog)尽可能保持一致,但提供更丰富的功能,且所有功能均可通过配置开启或关闭,我还会尽可能遵守 Bear Blog 的理念 来维护这个主题。

如果你也在使用 Hugo 博客框架,欢迎试试这个主题。

#dev