Hexo 生成的网站全部是静态文件,部署简单、访问速度快,也不需要维护数据库和后端服务。但静态网站有一个明显的限制:页面中的数据通常只能写在 Markdown 或配置文件中,每次修改都要编辑代码、提交仓库并重新构建。
像运动记录、账目、书单、项目进度这类经常变化的数据,如果每次都直接修改 Markdown,维护起来并不方便。Google Sheets 本身就是一个容易编辑的在线表格,因此可以把它当作一个轻量的数据管理后台,在构建 Hexo 时读取表格,再生成供页面使用的静态 JSON。
整体流程
这里所说的“动态数据”,是指数据不再直接维护在博客源码中,而是维护在 Google Sheets 中。网站本身依然是纯静态的,数据只会在 Hexo 重新构建后更新。
整体流程如下:
1 | Google Sheets |
这种方式保留了静态网站的优点:
- 不需要数据库和常驻后端;
- 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 | function loadGoogleCredentials() { |
密钥只能用于构建过程,不能写入 source/、var/ 或 public/。
在 Hexo 配置中声明 Spreadsheet
Spreadsheet ID 不应写死在插件代码中。我把它放在 Hexo 的 _config.yml:
1 | google_sheets: |
例如下面这个地址:
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 遵循同一套约定:
- 第一行是标题栏;
- 标题栏会成为 JSON 对象的 key;
- 空单元格转换为
null; - 数字转换为 JSON number;
- 完全空白的行会被忽略。
例如表格内容:
| type | time | duration | distance |
|---|---|---|---|
| Swimming | 7/17/2026 | 53 | 1.14 |
会转换为:
1 | { |
核心转换逻辑可以概括为:
1 | function rowsToRecords(rows) { |
实际实现还需要校验空标题、重复标题和行宽,防止没有标题的列被静默丢弃。
为了避免 Sheet 名称产生不安全的文件路径,我只允许名称包含英文字母、数字和下划线:
1 | /^[A-Za-z0-9_]+$/ |
最终文件名由 Sheet 名称决定:
1 | sports → var/sheet_sports.json |
完整读取逻辑位于 scripts/update-google-sheets.js。
接入 Hexo 构建生命周期
Hexo 会自动加载项目根目录 scripts/ 中的 JavaScript 文件。插件在 before_generate 阶段更新缓存:
1 | hexo.extend.filter.register('before_generate', async function () { |
这样只有执行 hexo generate 或 npm run build 时才访问 Google API。hexo server 的监听刷新不会反复请求外部服务。
缓存生成后,再注册一个 Generator,把 var/ 中的 JSON 输出到 public/data/:
1 | hexo.extend.generator.register('google-sheets-data', function () { |
这里不是把文件复制到 source/static/,而是由 Hexo Generator 直接创建静态路由。
在页面中加载数据
页面不需要知道 Google API,也不需要任何密钥,只读取构建结果:
1 | fetch('/data/sheet_sports.json', { cache: 'no-store' }) |
拿到 records 后,可以生成普通表格,也可以按月份汇总,再使用 ECharts 绘制柱状图和折线图。
需要注意的是,Google Sheets 返回的日期通常是当前表格中的显示格式,例如 7/17/2026,不一定是 ISO 日期。页面端应该明确支持所使用的日期格式,避免直接进行字符串排序。
在 GitHub Actions 中自动更新
构建步骤需要把 GitHub Secret 注入环境变量:
1 | - name: Generate static files |
构建完成后,可以把 var/ 缓存提交回仓库:
1 | - name: Commit updated build artifacts |
保留缓存有两个好处:
- 外部 API 临时失败时,仍然可以生成上一次的数据;
- 本地没有密钥时,也可以使用已有缓存预览页面。
再配合 GitHub Actions 的 schedule,就可以定时读取 Google Sheets、生成 JSON 并重新部署网站。
适用场景
这个方案比较适合:
- 运动、阅读、账目等个人记录;
- 友链、项目列表、公开数据目录;
- 更新频率不高的小型仪表盘;
- 希望由非开发工具维护的数据。
它不适合需要用户提交数据、复杂查询、权限隔离或实时更新的应用。随着数据量和交互复杂度增加,最终还是应该使用正式的数据库和后端 API。
对于个人静态博客来说,Google Sheets 刚好处在“手写 Markdown”和“搭建后台系统”之间:编辑体验足够简单,又不会破坏静态部署的轻量结构。