Hexo 生成的网站全部是静态文件,部署简单、访问速度快,也不需要维护数据库和后端服务。但静态网站有一个明显的限制:页面中的数据通常只能写在 Markdown 或配置文件中,每次修改都要编辑代码、提交仓库并重新构建。

像运动记录、账目、书单、项目进度这类经常变化的数据,如果每次都直接修改 Markdown,维护起来并不方便。Google Sheets 本身就是一个容易编辑的在线表格,因此可以把它当作一个轻量的数据管理后台,在构建 Hexo 时读取表格,再生成供页面使用的静态 JSON。

整体流程

这里所说的“动态数据”,是指数据不再直接维护在博客源码中,而是维护在 Google Sheets 中。网站本身依然是纯静态的,数据只会在 Hexo 重新构建后更新。

整体流程如下:

1
2
3
4
5
6
7
Google Sheets
↓ Hexo 构建时读取
var/sheet_<name>.json
↓ Hexo Generator
public/data/sheet_<name>.json
↓ 浏览器 Fetch
页面表格和图表

这种方式保留了静态网站的优点:

  • 不需要数据库和常驻后端;
  • Google API 密钥只在构建环境中使用,不会发送到浏览器;
  • 页面读取的是同域静态 JSON,没有跨域和 API 配额问题;
  • 即使 Google API 临时不可用,也可以继续使用上一次的缓存。

代价是数据更新频率取决于构建频率。如果需要秒级实时数据,这个方案并不适合。

准备 Google Service Account

首先在 Google Cloud Console 中创建项目,启用 Google Sheets API,然后创建一个 Service Account 并下载 JSON 密钥。

还需要把目标 Spreadsheet 共享给 JSON 文件中 client_email 对应的邮箱,并授予查看权限。否则即使密钥本身有效,API 仍然会返回无权访问。

本地开发时,我把密钥文件放在:

1
.env/google-service-account.json

整个 .env/ 目录必须加入 .gitignore,避免把私钥提交到仓库。

在 GitHub Actions 中,则把 JSON 文件的完整内容保存为 Repository Secret:

1
GOOGLE_SERVICE_ACCOUNT_JSON

脚本优先读取环境变量,环境变量不存在时再读取本地文件:

1
2
3
4
5
6
7
8
function loadGoogleCredentials() {
if (process.env.GOOGLE_SERVICE_ACCOUNT_JSON) {
return JSON.parse(process.env.GOOGLE_SERVICE_ACCOUNT_JSON);
}
return JSON.parse(
fs.readFileSync('.env/google-service-account.json', 'utf8')
);
}

密钥只能用于构建过程,不能写入 source/var/public/

在 Hexo 配置中声明 Spreadsheet

Spreadsheet ID 不应写死在插件代码中。我把它放在 Hexo 的 _config.yml

1
2
google_sheets:
spreadsheet_id: YOUR_SPREADSHEET_ID

例如下面这个地址:

1
https://docs.google.com/spreadsheets/d/YOUR_SPREADSHEET_ID/edit

其中 /d//edit 之间的部分就是 Spreadsheet ID。插件加载后可以通过 hexo.config.google_sheets.spreadsheet_id 读取。

把每个子 Sheet 转换为 JSON

一个 Spreadsheet 可以包含多个子 Sheet。为了让读取逻辑可以复用,插件不再只处理固定的 sports,而是先读取 Spreadsheet 元数据,获得全部子 Sheet 名称,再逐一读取。

每个子 Sheet 遵循同一套约定:

  1. 第一行是标题栏;
  2. 标题栏会成为 JSON 对象的 key;
  3. 空单元格转换为 null
  4. 数字转换为 JSON number;
  5. 完全空白的行会被忽略。

例如表格内容:

type time duration distance
Swimming 7/17/2026 53 1.14

会转换为:

1
2
3
4
5
6
7
8
9
10
11
{
"updated": "2026-07-19",
"records": [
{
"type": "Swimming",
"time": "7/17/2026",
"duration": 53,
"distance": 1.14
}
]
}

核心转换逻辑可以概括为:

1
2
3
4
5
6
7
8
9
10
function rowsToRecords(rows) {
const headers = rows[0].map((header) => String(header).trim());

return rows.slice(1).map((row) => Object.fromEntries(
headers.map((header, index) => [
header,
parseCell(row[index])
])
));
}

实际实现还需要校验空标题、重复标题和行宽,防止没有标题的列被静默丢弃。

为了避免 Sheet 名称产生不安全的文件路径,我只允许名称包含英文字母、数字和下划线:

1
/^[A-Za-z0-9_]+$/

最终文件名由 Sheet 名称决定:

1
2
sports → var/sheet_sports.json
money → var/sheet_money.json

完整读取逻辑位于 scripts/update-google-sheets.js

接入 Hexo 构建生命周期

Hexo 会自动加载项目根目录 scripts/ 中的 JavaScript 文件。插件在 before_generate 阶段更新缓存:

1
2
3
4
5
6
7
8
9
10
11
hexo.extend.filter.register('before_generate', async function () {
if (process.env.SKIP_GOOGLE_SHEETS === '1') return;

const cmd = process.argv[2];
if (cmd !== 'generate' && cmd !== 'g') return;

await updateGoogleSheets({
spreadsheetId: hexo.config.google_sheets.spreadsheet_id,
logger: hexo.log
});
});

这样只有执行 hexo generatenpm run build 时才访问 Google API。hexo server 的监听刷新不会反复请求外部服务。

缓存生成后,再注册一个 Generator,把 var/ 中的 JSON 输出到 public/data/

1
2
3
4
5
6
hexo.extend.generator.register('google-sheets-data', function () {
return readSheetCaches().map((cache) => ({
path: `data/sheet_${cache.name}.json`,
data: JSON.stringify(cache.data, null, 2)
}));
});

这里不是把文件复制到 source/static/,而是由 Hexo Generator 直接创建静态路由。

在页面中加载数据

页面不需要知道 Google API,也不需要任何密钥,只读取构建结果:

1
2
3
4
5
6
7
8
fetch('/data/sheet_sports.json', { cache: 'no-store' })
.then((response) => {
if (!response.ok) throw new Error('Data unavailable');
return response.json();
})
.then((data) => {
renderSportsDashboard(data.records);
});

拿到 records 后,可以生成普通表格,也可以按月份汇总,再使用 ECharts 绘制柱状图和折线图。

需要注意的是,Google Sheets 返回的日期通常是当前表格中的显示格式,例如 7/17/2026,不一定是 ISO 日期。页面端应该明确支持所使用的日期格式,避免直接进行字符串排序。

在 GitHub Actions 中自动更新

构建步骤需要把 GitHub Secret 注入环境变量:

1
2
3
4
- name: Generate static files
run: npm run build
env:
GOOGLE_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_JSON }}

构建完成后,可以把 var/ 缓存提交回仓库:

1
2
3
4
5
- name: Commit updated build artifacts
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "chore: update build artifacts"
file_pattern: var

保留缓存有两个好处:

  1. 外部 API 临时失败时,仍然可以生成上一次的数据;
  2. 本地没有密钥时,也可以使用已有缓存预览页面。

再配合 GitHub Actions 的 schedule,就可以定时读取 Google Sheets、生成 JSON 并重新部署网站。

适用场景

这个方案比较适合:

  • 运动、阅读、账目等个人记录;
  • 友链、项目列表、公开数据目录;
  • 更新频率不高的小型仪表盘;
  • 希望由非开发工具维护的数据。

它不适合需要用户提交数据、复杂查询、权限隔离或实时更新的应用。随着数据量和交互复杂度增加,最终还是应该使用正式的数据库和后端 API。

对于个人静态博客来说,Google Sheets 刚好处在“手写 Markdown”和“搭建后台系统”之间:编辑体验足够简单,又不会破坏静态部署的轻量结构。