引言

在 Hexo Butterfly 主题页面中,“说说”功能允许用户发布简短的消息,类似于微博或朋友圈。在主题官网中提到,说说可以使用 Artitalk HexoPlusPlus Talk 实现动态发布管理,但前者需要涉及到注册 LeanCloud 数据库账号,后者则已经在 3 年前存档,停止维护。如何接入说说 API 成了其中一个难题

本文将介绍如何利用 Cloudflare D1 数据库 Cloudflare Worker,创建一个免费的说说 API,并将其接入 Hexo Butterfly 主题,实现动态发布与管理说说功能。

原理

Butterfly 主题官网上提到说说的数据来源可以通过以下两个方法获取:

  1. 本地生成

    在 Hexo 根目录中的 source/_data(如果没有 _data 文件夹,请自行创建),创建一个文件 shuoshuo.yml

  2. 远程拉取

    在説説页面 Markdown 里的 front-matter 添加远程链接

    1
    shuoshuo_url: xxx

其中第二种方法可以做到从 API 动态获取说说数据,只需符合 Butterfly 主题要求的 JSON 格式即可:

1
2
3
4
5
6
7
8
9
10
[
{
"author": "Echomirix",
"avatar": "https://example.com/avatar.jpg",
"date": "2025-09-30 xx:xx:xx",
"content": "说说内容",
"key": "评论编号",
"tags": ["标签1", "标签2"]
}
]
参数 解释
author 【可选】作者名称
avatar 【可选】作者头像
date 【必需】日期
content 【必需】内容(Markdown 格式或 Html 格式)
key 【可选】评论的唯一标识,开启评论必须配置
tags 【可选】标签

Cloudflare 免费套餐中提供了 D1 数据库 Worker 的使用权限,可以利用这两个服务来创建一个简单的 API,实现说说的动态发布与管理。

实现

创建 Cloudflare Workers

打开Cloudflare 控制台,选择侧边栏中的 Workers,点击创建应用程序选择模板Worker + D1 Database,将仓库 fork 到自己的 GitHub 账号下并创建 Worker

创建Worker

更改创建 D1 数据库选项中的数据库名称与数据库位置,然后点击创建并部署按钮。

编辑 D1 数据库

接下来需要创建一个数据表来存储说说数据。返回 Cloudflare 控制台,选择侧边栏中的存储与数据D1 SQL 数据库,点击刚刚创建的数据库,点击 Explore Data 按钮,进入数据浏览页面。点击侧边栏的”+” 按钮,进入 Create table 页面。

选择D1数据库

键入你数据库名称,依次添加上文表格中的各个字段。最后保存更改。字段具体参考下表:

字段名称 类型 允许空值
author TEXT
avatar TEXT
date INTENGER
content TEXT
key TEXT
tags TEXT

这样就创建好了一个用于存储说说数据的表。

编辑 Worker 代码

重新返回刚才创建的 Worker 应用程序页面,点击右上角code-icon按钮,进入代码编辑页面。将以下代码复制粘贴到代码编辑器中,替换掉原有代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });

// 主Worker逻辑
var index_default = {
async fetch(request, env) {
// CORS Headers
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization, x-ijt',
};

if (request.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}

const url = new URL(request.url);
const pathname = url.pathname;

try {
// 说说API - 获取
if (pathname === '/blog/get-shuoshuo-list' && request.method === 'GET') {
if (!env.DB) {
return new Response(JSON.stringify({ error: 'D1数据库绑定缺失,请检查wrangler.toml配置' }), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
});
}
const { results } = await env.DB.prepare(
`SELECT author, avatar, date, content, key, tags FROM shuoshuo ORDER BY date DESC`
).all();
const journals = results.map(row => {
let tags = undefined;
if (row.tags) {
try {
tags = JSON.parse(row.tags);
if (!Array.isArray(tags)) tags = [row.tags];
} catch {
tags = row.tags.split(',').map(t => t.trim());
}
}
const obj = {
author: row.author,
avatar: row.avatar,
date: formatDate(row.date),
content: row.content,
key: row.key,
tags: tags
};
Object.keys(obj).forEach(k => obj[k] === undefined && delete obj[k]);
return obj;
});
return new Response(JSON.stringify(journals), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
});
}

// 说说API - 新增
if (pathname === '/blog/create-shuoshuo' && request.method === 'POST') {
const SECRET_KEY = '[替换成你的密钥,也可以留空]'; // 这里替换成你自己的密钥,留空则不验证
if (!SECRET_KEY || request.headers.get('Authorization') !== `Bearer ${SECRET_KEY}`) {
return new Response(JSON.stringify({ error: '密钥错误' }), {
status: 401,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
});
}
if (!env.DB) {
return new Response(JSON.stringify({ error: 'D1数据库绑定缺失,请检查wrangler.toml配置' }), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
});
}
let data;
try {
data = await request.json();
} catch {
return new Response(JSON.stringify({ error: '请求体需为JSON格式' }), {
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
});
}

// 字段校验
if (!data.date) {
data.date = Math.floor(Date.now() / 1000);
}
// 类型转换
const dateInt = typeof data.date === 'number' ? data.date : Date.parse(data.date);
if (!Number.isInteger(dateInt)) {
return new Response(JSON.stringify({ error: 'date必须为时间戳整数' }), {
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
});
}
// tags处理为字符串
let tagsValue = null;
if (Array.isArray(data.tags)) {
tagsValue = JSON.stringify(data.tags);
} else if (typeof data.tags === 'string') {
tagsValue = JSON.stringify([data.tags]);
}

// 插入数据库
const { success } = await env.DB.prepare(
`INSERT INTO shuoshuo (author, avatar, date, content, key, tags) VALUES (?, ?, ?, ?, ?, ?)`
).bind(
data.author || null,
data.avatar || null,
dateInt,
data.content,
data.key || null,
tagsValue
).run();

if (!success) {
throw new Error('说说创建失败');
}

return new Response(JSON.stringify({ message: '说说创建成功' }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
});
}
} catch (err) {
return new Response(JSON.stringify({ error: err.message }), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
});
}
},
};

function formatDate(ts) {
// 如果是10位(秒),转为13位(毫秒)
if (ts.toString().length === 10) ts = ts * 1000;
// 手动加8小时 = 8 * 60 * 60 * 1000 ms
const d = new Date(ts + 8 * 60 * 60 * 1000);
const pad = n => n.toString().padStart(2, '0');
return `${d.getUTCFullYear()}-${pad(d.getUTCMonth()+1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`;
}

export {
index_default as default
};

这样就成功添加了一个用于获取与创建说说的 API。你可以将 https://example.com/blog/get-shuoshuo-list 作为说说页面的shuoshuo_url,通过 GET 请求获取说说列表;通过 POST 请求向/blog/create-shuoshuo发送 JSON 数据,即可创建新的说说。

请务必将代码中的SECRET_KEY替换为你自己的密钥,以防止未授权的访问。
由于 Cloudflare Worker 域名被墙,建议使用自定义域名绑定 Worker

进阶操作

在接入获取 API 后,你可以尝试更改位于themes/butterfly/layout/includes/page/shuoshuo.pug模板文件,让说说界面可以直接通过 POST 请求创建说说。你可以参考我的说说页面的实现方式。

说说页面