用GitHub热力图记录阅读:纯前端实现与自动部署指南
2026/8/30 3:47:35 网站建设 项目流程

我翻了上百个首页,发现最容易让人想每天“登录”的功能,居然不是复杂的数据大屏,而是 GitHub 主页那一排排绿色格子。很多人嘴上说这是“程序员种菜”,但手却很诚实,为了不让绿点断掉,硬是逼自己每天提交一次代码。这个设计真正厉害的地方,不是它炫耀了提交次数,而是把一年 365 天压缩进一屏,用颜色深浅告诉你:你哪些天在行动,哪些天在偷懒。这种“可视化 + 连续反馈”的机制,完全可以迁移到读书这件事上。

这篇文章要做的,就是实现一个 GitHub Heatmap for Reading:把 GitHub 贡献热力图的视觉效果和交互思维,搬到一个纯前端的“阅读记录热力图”页面上。你可以用它记录每天的阅读分钟数和页数,生成类似 GitHub 主页的绿点矩阵,并通过 GitHub Pages 免费部署,再配合 GitHub Actions 自动更新数据。

读完这篇文章,你会得到三样东西:第一,理解 GitHub 热力图(Contribution Heatmap)背后的数据结构和渲染原理;第二,拿到一套可以直接复制运行的 HTML / CSS / JavaScript 代码,以及配套的 JSON 数据文件;第三,掌握从本地预览、推送仓库到开启 Pages、配置定时任务的全流程,并知道常见的坑在哪里。

1. 为什么要做一张“阅读热力图”

很多人试过用打卡 App 或者手账记录阅读,但能坚持过三个月的并不多。原因在于,列表式记录给不了反馈。你打开 App 看到的是“今天读完 1 章”“本周阅读 3 次”,这些数字冷冰冰的,一旦断了一两天,很容易产生“干脆算了”的破罐子心理。

但 GitHub 热力图不一样。每次看到那一整年的格子,你会自动去寻找“连续绿了几天”的链条。这种 streak(连续记录)机制给大脑的暗示是:你已经坚持了 12 天,今天断掉太可惜。哪怕当天只提交一行文档修改,也会保住链条,于是行为就持续下去了。

把这套逻辑搬到阅读上,思路很清晰:

GitHub 贡献热力图阅读热力图
每天提交代码次数每天阅读分钟数 / 页数
提交次数决定颜色深浅阅读时长决定颜色深浅
没有提交 = 灰色格子没有阅读 = 灰色格子
连续提交天数(streak)连续阅读天数
一年 365 天矩阵一年 365 天矩阵

这个工具本质上不是帮你读书,而是帮你看见自己的阅读轨迹。当你把一年压缩到一张图里,你会清楚地看到自己哪几个月是“读书高峰”,哪几个月是“彻底停摆”。有了这种反馈,你才更容易做出调整。

它适合谁?适合想养成阅读习惯的开发者,适合正在学习前端想拿真实场景练手的人,也适合对 GitHub 生态感兴趣、想体验 Actions 和 Pages 的人。它不适合把“点亮格子”当成唯一目标而忽视真实阅读的人——热力图只能记录行为,不能替代行为。

2. GitHub Heatmap 的核心原理

先明确一个概念:GitHub 主页上的 Contribution Graph,严格来说不是统计学意义上的“热力图(heatmap)”,而是一张矩阵日历图(calendar heatmap)。它是一个 7 行、约 53 列的网格。

  • 行:代表星期几。第一行是周日。
  • 列:代表一年里的某一周。
  • 每个格子:代表一天。
  • 颜色深浅:代表当天贡献的多少。

GitHub 默认将颜色分成 5 档:0 提交是浅灰色;1 到 3 次提交是很浅的绿色;4 到 6 次是中等绿色;7 到 9 次是深绿色;10 次以上是最深的绿色。这个分档不是固定编码写死的,而是根据当天的提交数量划分区间,本质是把数值映射为颜色等级。

对于阅读热力图,我们做同样的映射:

  • 30 分钟以上阅读:最深色
  • 20 到 29 分钟:较深色
  • 10 到 19 分钟:中等绿色
  • 1 到 9 分钟:浅绿色
  • 0 分钟:灰色

从前端实现的角度,渲染逻辑可以拆成三步:

  1. 生成一年里所有的日期。
  2. 找到这一年第一天所在周的周日,作为矩阵的第一个格子,防止第一列日期错乱。
  3. 按日期顺序逐个创建格子,根据当天数据判断颜色等级,然后填充样式。

为什么不需要后端?因为所有数据都可以存到一个 JSON 文件里。前端通过 fetch 读取这个 JSON,再用 DOM 操作生成格子。页面不需要数据库、不需要服务器,甚至不需要 Node.js 环境,一个静态服务器就够了。这也正是它能部署到 GitHub Pages 的原因。

3. 环境准备与前置条件

这个项目的环境要求非常低,即使你之前没部署过任何网站,也可以按下面的清单准备:

依赖说明是否必须
浏览器Chrome / Edge / Firefox 均可必须
GitHub 账号托管代码、开启 Pages必须
Git 客户端命令行推送代码强烈建议
文本编辑器VS Code、Sublime Text、Vim 均可建议
Python 3可选,用于启动本地 HTTP 服务或生成数据可选

版本方面不用纠结。这个方案不依赖某个特定版本的框架,Git 使用 2.x 主流版本即可;Python 不是必须项,如果本机没有,可以直接用 VS Code 的 Live Server 插件替代本地服务器。

有一点需要提前说明:如果直接用浏览器双击打开index.html,页面的基础布局能显示,但 fetch 本地data.json会被浏览器的 file:// 策略拦截,控制台会报Failed to fetch。所以,本地预览建议统一使用本地 HTTP 服务,后面第 4 章会给出启动命令。

4. 核心流程拆解

整个项目的流程可以拆成 8 个步骤。我把每一步的“做什么”和“为什么”都说清楚,避免你跟着敲完代码却不知道自己在哪一步。

4.1 第一步:在 GitHub 创建远程仓库

登录 GitHub 后,点击右上角加号,选择 New repository。仓库名可以填reading-heatmap,可见性建议先选 Private(私有),等你确定内容没问题再改成 Public。不要勾选 “Add a README file”,因为我们要从本地推送代码,空仓库可以避免初始提交冲突。

这个仓库的作用有两个:一是保存项目的源码和阅读数据;二是开启 GitHub Pages 后,它会把仓库里的静态文件发布成一个可访问的网页。

4.2 第二步:在本地创建项目目录

在本地新建一个文件夹,名字建议与远程仓库保持一致,例如reading-heatmap。在这个目录下,我们需要准备这些文件:

reading-heatmap/ ├── index.html ├── style.css ├── app.js ├── data.json ├── reading.log ├── .github/ │ └── workflows/ │ └── update-heatmap.yml └── scripts/ └── generate_data.py

reading.log是人类的原始阅读记录,generate_data.py会把它转换成前端读取的data.json。如果暂时不想用 Python 脚本,也可以直接编辑data.json

4.3 第三步:编写页面三件套

index.html负责页面结构,style.css负责热力图样式,app.js负责读取数据和渲染格子。三者通过文件路径引用关联,文件必须放在同一层级。

4.4 第四步:准备阅读记录数据

你可以直接在data.json里写当天记录,也可以在reading.log里按行写入,然后由脚本自动生成 JSON。两种方式最终产出的数据结构一样。

4.5 第五步:本地预览

在项目目录打开终端,执行:

python -m http.server 8080

然后浏览器访问http://localhost:8080。如果看到满屏灰色格子,以及底部四个统计数字,说明页面已经跑通了。

4.6 第六步:推送代码到 GitHub 并开启 Pages

用 Git 把本地文件提交并推送到远程仓库。然后进入仓库的 Settings → Pages,将 Source 设置为 Deploy from a branch,分支选择 main,路径选择 /root,保存即可。

4.7 第七步:用 GitHub Actions 自动更新数据(可选)

如果你希望“每天自动把 reading.log 里新增的天数同步到 data.json 并提交到仓库”,可以添加一个定时 workflow。它会在每天固定时间运行一次 Python 脚本,然后自动提交变化。这个步骤不是必须的,但它是理解 GitHub Actions 用法的一个很好的练手项目。

4.8 第八步:持续记录与迭代

之后你每天只需要做一件事:往reading.log里追加一行,比如2025-06-10 35 18,代表当天阅读 35 分钟、18 页。剩下的渲染和部署都是自动的。

5. 完整代码实现

下面是整个项目最小的可运行实现,文件都标注了路径。你可以照着创建,先让它跑起来,再去改颜色和数据。

5.1 index.html

<!-- 文件路径:reading-heatmap/index.html --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>阅读热力图 Reading Heatmap</title> <link rel="stylesheet" href="style.css" /> </head> <body> <main class="container"> <header class="page-header"> <h1>Reading Heatmap</h1> <p class="subtitle">用 GitHub 风格热力图记录每天的阅读时间</p> </header> <section class="heatmap-card"> <div class="heatmap-meta"> <span id="yearLabel">2025</span> <span class="legend"> <span>少</span> <span class="legend-level"></span> <span class="legend-level"></span> <span class="legend-level"></span> <span class="legend-level"></span> <span>多</span> </span> </div> <div id="heatmap" class="heatmap"></div> </section> <section class="stats"> <div class="stat-item"> <strong id="totalDays">0</strong> <span>累计阅读天数</span> </div> <div class="stat-item"> <strong id="totalMinutes">0</strong> <span>累计阅读分钟</span> </div> <div class="stat-item"> <strong id="streak">0</strong> <span>当前连续天数</span> </div> <div class="stat-item"> <strong id="avgMinutes">0</strong> <span>日均分钟</span> </div> </section> </main> <script src="app.js"></script> </body> </html>

页面结构不复杂:一个标题区、一个热力图卡片、四个统计卡片。热力图容器#heatmap是空的,由 JavaScript 动态填充格子。

5.2 style.css

/* 文件路径:reading-heatmap/style.css */ * { box-sizing: border-box; } body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif; background: #f6f8fa; color: #24292f; } .container { max-width: 920px; margin: 40px auto; padding: 0 20px; } .page-header h1 { margin: 0 0 8px; font-size: 32px; } .subtitle { color: #57606a; margin: 0 0 24px; } .heatmap-card { background: #fff; border: 1px solid #d0d7de; border-radius: 12px; padding: 20px; overflow-x: auto; } .heatmap-meta { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; font-size: 14px; color: #57606a; } .heatmap { display: grid; grid-auto-flow: column; grid-template-rows: repeat(7, 12px); gap: 3px; } .day-cell { width: 12px; height: 12px; border-radius: 2px; background-color: #ebedf0; } .day-cell.l1 { background-color: #c6e48b; } .day-cell.l2 { background-color: #7bc96f; } .day-cell.l3 { background-color: #196c2e; } .day-cell.l4 { background-color: #0f4a1d; } .legend { display: flex; align-items: center; gap: 4px; } .legend-level { width: 12px; height: 12px; border-radius: 2px; background-color: #ebedf0; } .legend-level:nth-child(2) { background-color: #c6e48b; } .legend-level:nth-child(3) { background-color: #7bc96f; } .legend-level:nth-child(4) { background-color: #196c2e; } .legend-level:nth-child(5) { background-color: #0f4a1d; } .stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-top: 20px; } .stat-item { background: #fff; border: 1px solid #d0d7de; border-radius: 12px; padding: 16px; text-align: center; } .stat-item strong { display: block; font-size: 28px; margin-bottom: 4px; } .stat-item span { font-size: 14px; color: #57606a; } @media (max-width: 600px) { .stats { grid-template-columns: repeat(2, 1fr); } }

这里最关键的是.heatmap容器的 CSS Grid 写法。grid-template-rows: repeat(7, 12px)保证了每一列有 7 行,对应周一到周日;grid-auto-flow: column让子元素按“先填满一整列,再进入下一列”的顺序排列,正好与 GitHub 的布局一致。手机端可能出现横向滚动,所以给.heatmap-card加了overflow-x: auto

5.3 app.js

// 文件路径:reading-heatmap/app.js const HEATMAP_YEAR = 2025; function loadData() { return fetch("data.json") .then((response) => { if (!response.ok) { throw new Error("data.json 加载失败,请确认文件存在且路径正确"); } return response.json(); }) .then((json) => json.records || []); } function formatDate(date) { const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, "0"); const d = String(date.getDate()).padStart(2, "0"); return `${y}-${m}-${d}`; } function buildDayMap(records) { const map = {}; records.forEach((item) => { const date = item.date; const minutes = Number(item.minutes) || 0; const pages = Number(item.pages) || 0; if (map[date]) { map[date].minutes += minutes; map[date].pages += pages; } else { map[date] = { minutes, pages }; } }); return map; } function getLevel(minutes) { if (minutes <= 0) return 0; if (minutes < 10) return 1; if (minutes < 20) return 2; if (minutes < 30) return 3; return 4; } function renderHeatmap(dayMap) { const container = document.getElementById("heatmap"); const start = new Date(HEATMAP_YEAR, 0, 1); const end = new Date(HEATMAP_YEAR, 11, 31); // 回退到 1 月 1 日所在周的周日,保证第一列完整 const firstCell = new Date(start); firstCell.setDate(firstCell.getDate() - firstCell.getDay()); const cells = []; const cursor = new Date(firstCell); while (cursor <= end) { cells.push(new Date(cursor)); cursor.setDate(cursor.getDate() + 1); } const fragment = document.createDocumentFragment(); cells.forEach((date) => { const cell = document.createElement("div"); cell.className = "day-cell"; if (date.getFullYear() === HEATMAP_YEAR) { const key = formatDate(date); const rec = dayMap[key]; const minutes = rec ? rec.minutes : 0; const level = getLevel(minutes); if (level > 0) { cell.classList.add("l" + level); } cell.title = `${key},阅读 ${minutes} 分钟${ rec && rec.pages ? "," + rec.pages + " 页" : "" }`; } fragment.appendChild(cell); }); container.innerHTML = ""; container.appendChild(fragment); } function computeStreak(dayMap) { const today = new Date(); let streak = 0; const cursor = new Date(today); while (true) { const key = formatDate(cursor); const rec = dayMap[key]; if (rec && rec.minutes > 0) { streak++; cursor.setDate(cursor.getDate() - 1); } else { break; } } return streak; } function renderStats(dayMap, records) { const dayKeys = Object.keys(dayMap); const totalDays = dayKeys.filter((k) => dayMap[k].minutes > 0).length; const totalMinutes = records.reduce( (sum, r) => sum + (Number(r.minutes) || 0), 0 ); const streak = computeStreak(dayMap); document.getElementById("totalDays").textContent = totalDays; document.getElementById("totalMinutes").textContent = totalMinutes; document.getElementById("streak").textContent = streak; document.getElementById("avgMinutes").textContent = totalDays ? Math.round(totalMinutes / totalDays) : 0; } async function init() { try { const records = await loadData(); const dayMap = buildDayMap(records); renderHeatmap(dayMap); renderStats(dayMap, records); } catch (err) { document.getElementById("heatmap").innerHTML = '<p style="color:#cf222e">加载失败:' + err.message + "</p>"; } } init();

这段代码里有两个容易出错的点,需要重点解释。

第一,firstCell的回退逻辑。如果年份的第一天不是周日,直接把它放在矩阵第一列,会导致这一列排不满 7 个格子,后续所有日期都会错位一格。所以代码先new Date(HEATMAP_YEAR, 0, 1),再用setDate(getDate() - getDay())回退到所在周的周日。getDay()返回 0 表示周日,1 表示周一,依次类推。

第二,while (cursor <= end)的循环会生成 365 到 371 个日期,这是因为矩阵的起始点会向前回退几天,保证整个年份能占满完整列。这也是 GitHub 热力图实际渲染时一年总是 53 列左右的原因。

统计部分的“当前连续天数”从今天开始向前数,遇到没有阅读记录的天就停止。这里需要注意:如果今天还没有记录,streak 会直接显示 0。设计上可以接受,因为它忠实反映“目前链条是否断掉”。

5.4 data.json

{ "year": 2025, "records": [ { "date": "2025-01-01", "minutes": 25, "pages": 12 }, { "date": "2025-01-02", "minutes": 40, "pages": 20 }, { "date": "2025-01-05", "minutes": 60, "pages": 30 }, { "date": "2025-01-06", "minutes": 15, "pages": 8 } ] }

数据结构很简单,records是一个数组,每个元素表示某一天的阅读情况。date使用YYYY-MM-DD格式,这样可以避免浏览器解析日期的时区差异。minutes是阅读分钟数,pages是阅读页数,页数不是必须项,但保留它可以让 title 提示更丰富。

5.5 GitHub Actions 工作流

如果你希望每天自动生成 data.json,需要把原始记录放到reading.log,然后在.github/workflows/下创建下面的文件。

# 文件路径:reading-heatmap/.github/workflows/update-heatmap.yml name: Update Reading Heatmap on: schedule: - cron: "0 16 * * *" workflow_dispatch: permissions: contents: write jobs: update: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.12" - name: Generate data.json run: python scripts/generate_data.py - name: Commit changes run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add data.json git diff --quiet && git diff --cached --quiet || git commit -m "chore: update reading data" git push

schedule中的 cron 表达式使用 UTC 时间。0 16 * * *表示每天 UTC 16:00 运行一次,对应北京时间次日 0 点。Actions 的定时任务不保证严格准时,尤其在仓库不活跃时可能延迟,但作为每日更新阅读数据这种非实时任务完全够用。

workflow_dispatch是手动触发入口,方便你在不了解 cron 规则时先跑一次流程。permissions里只需要contents: write,这是因为脚本要写回data.json。不要把这里改成过大的权限,也不要在 YAML 里写自己的个人访问令牌。

5.6 scripts/generate_data.py

# 文件路径:reading-heatmap/scripts/generate_data.py import json from datetime import date LOG_FILE = "reading.log" OUTPUT_FILE = "data.json" def parse_log_line(line): parts = line.strip().split() if len(parts) < 2: return None date_str = parts[0] try: date.fromisoformat(date_str) except ValueError: return None minutes = int(parts[1]) if len(parts) > 1 else 0 pages = int(parts[2]) if len(parts) > 2 else 0 return {"date": date_str, "minutes": minutes, "pages": pages} def main(): records = [] with open(LOG_FILE, "r", encoding="utf-8") as f: for line in f: line = line.strip() if not line or line.startswith("#"): continue record = parse_log_line(line) if record: records.append(record) with open(OUTPUT_FILE, "w", encoding="utf-8") as f: json.dump( {"year": date.today().year, "records": records}, f, ensure_ascii=False, indent=2, ) print(f"generated {OUTPUT_FILE}, {len(records)} records") if __name__ == "__main__": main()

reading.log的每行格式为日期 分钟 页数,例如:

2025-06-10 35 18 2025-06-11 20 9

脚本会自动跳过空行和#开头的注释行,方便你留备注。这个脚本在本地也可以运行:只要在项目根目录执行python scripts/generate_data.py,就会重新生成data.json。如果你的 Python 版本较低,date.fromisoformat也能正常工作,因为这里传入的是标准YYYY-MM-DD字符串。

6. 运行结果与效果验证

代码写完后,验证要分三个层次:本地验证、部署验证、自动化验证。每一步的预期结果和排查方向如下。

6.1 本地验证

在项目目录启动本地服务:

python -m http.server 8080

浏览器访问http://localhost:8080。预期看到:

  • 页面标题显示“Reading Heatmap”。
  • 热力图区域出现一整年 7 行多列的格子。
  • 有阅读记录的日期显示为绿色,等级不同颜色不同。
  • 底部统计数字会随着data.json变化。

打开浏览器开发者工具(F12),切到 Console 和 Network。Console 不应报错,Network 里data.json请求状态应该是 200。

如果双击index.html打开,Network 里data.json请求会失败,因为浏览器在file://协议下默认不允许 fetch 本地文件。此时只要改用本地 HTTP 服务即可。

6.2 部署验证

推送代码后,在 GitHub 仓库的 Settings → Pages 中设置:Source 选择 Deploy from a branch,Branch 选择 main,目录选择 /root,点击 Save。等待几分钟后,访问:

https://<你的用户名>.github.io/reading-heatmap/

如果页面 404,先看仓库名是不是reading-heatmap,再看看分支名是不是main,最后确认 index.html 在仓库根目录而不是子目录。Pages 构建通常需要几分钟到十几分钟,不用急着刷新。

6.3 自动化验证

如果配置了 Actions,进入仓库的 Actions 标签页,能看到Update Reading Heatmap这个 workflow。第一次可以直接点右上角的 “Run workflow” 手动触发。运行完成后,点进日志,如果最后出现generated data.json, N records,说明脚本执行成功;之后检查仓库里的data.json是否被更新。

一个容易忽略的点:Actions 更新data.json后,GitHub Pages 不会立刻生效,因为它需要重新构建静态页面。如果你发现数据变了但网页没变,等一会儿再刷新即可。

7. 常见问题与排查思路

在实际操作中,最常见的坑我整理成了一张表。建议收藏这篇文章,遇到问题时按表排查。

问题现象可能原因排查方式解决方案
GitHub 页面打开很慢或超时国内访问 GitHub 网络链路不稳定不同时间段多刷新几次;查看是否能打开其他网站只查看单文件或 README 时,可使用 GitHub 镜像站或代码托管平台预览;克隆大仓库时加--depth 1
本地打开 index.html 页面空白浏览器的 file:// 协议拦截了 fetch 请求F12 打开 Console,看是否有 Failed to fetchpython -m http.server 8080启动本地服务
热力图格子对不上日期起始日期没有回退到所在周的周日检查 1 月 1 日的getDay()使用firstCell.setDate(firstCell.getDate() - firstCell.getDay())
所有格子都是灰色data.json 日期不是当前年份,或分钟数为 0查看 data.json 的 date 字段;在 Console 打印 dayMap更新 HEATMAP_YEAR;把分钟数改为正数
GitHub Pages 访问 404仓库名、分支名或路径配置不对检查 URL 大小写;确认 Pages 设置重新设置 Deploy from a branch 为 main / root
Actions 没有自动运行cron 使用 UTC 时间;或仓库默认分支不是 main查看 Actions 日志先手动 Run workflow,确认脚本可以执行
网页上传大量文件容易失败网页拖拽上传不稳定查看是否上报超时改用 git 命令行推送;大文件先下载 zip 再解压

第 6 个问题需要单独解释一下。Actions 的schedule使用 UTC 时间,而且 GitHub 官方明确说明:定时任务可能因为仓库长时间不活跃而推迟执行。如果你的仓库只有每天一次更新,遇到延迟是正常的,不要因此认为脚本写错了。手动触发一次确认逻辑正确后,剩下的交给它每天跑即可。

8. 最佳实践与工程建议

到这里,项目已经能跑通了。但作为一个长期记录工具,还有几件事值得在正式使用前想清楚。

8.1 数据格式要统一

data.json里的日期必须全部使用YYYY-MM-DD,不要混用2025/1/12025-1-1。JavaScript 的new Date()对不同格式的解析结果可能不同,尤其在本地时区设置不一样时,会出现日期偏移一天的诡异问题。统一格式可以从根上避免这类 bug。

8.2 记录粒度建议只保留一个核心指标

README 和很多示例都喜欢同时记录“分钟数”和“页数”,但你实际使用时,建议只把“分钟数”作为唯一决定颜色的核心指标。页数可以作为辅助字段展示,但如果同时用它来分色,不同书籍的排版差异会让数据失去可比性。比如同样读 30 分钟,一本技术书可能只读 10 页,一本小说能读 40 页,简单用页数画颜色会很不公平。

8.3 仓库可见性与隐私边界

如果阅读记录涉及个人隐私,仓库设为 Private,但你仍然想用 GitHub Pages 公开,这是矛盾的:Pages 只对 Public 仓库开放。所以你有两个选择:一是仓库 Private,只本地访问;二是 Public,接受阅读数据公开。更稳妥的做法是:数据脱敏后再放到 Public,比如data.json里不记录书名和页面内容,只保留日期和分钟数。

8.4 自动化任务的安全边界

在 GitHub Actions 中,尽量使用仓库内置的GITHUB_TOKEN,不要在 YAML 文件里硬编码自己的个人访问令牌。workflow 的permissions要按最小权限声明,我们这个项目只需要contents: write。如果以后把脚本改成发布到其他平台,再根据需求扩展权限,不要一次性写一个很大的 scope。

8.5 跨年后的维护

代码里的HEATMAP_YEAR写死为 2025。跨年后,要么手动改这个变量,要么做成自动获取当前年份。后者实现很简单:把HEATMAP_YEAR改为new Date().getFullYear()。但如果你希望页面默认展示“上一年”而不是“今年”,就需要保留年份切换的功能,这部分已经超出本文范围,可以作为进阶练习。

8.6 不要为了点亮格子而造假

这句话听起来像鸡汤,但它其实是热力图数据质量的底线。如果某天没有阅读,就让它保持灰色。一旦你开始为了 streak 随意填写分钟数,这张图的数据意义就消失了,你会慢慢失去记录的动力。诚实的数据才能给你真实的反馈。

9. 总结

GitHub Heatmap for Reading 不是一个复杂项目,它本质上就是“矩阵日历 + 颜色映射 + 数据文件”。认识到了这一点,你会发现 GitHub 那一整套热力图交互并不神秘。真正有价值的是它背后的行为设计:把时间压缩成一屏,让长期趋势对肉眼可见。你可以用同样的框架记录冥想、锻炼、写作,甚至背单词。

建议你先不要急着加功能,按本文的代码原样部署一个小站,跑通整个流程。等到连续记录一周之后,再考虑增加“最近 30 天摘要”“年度统计”“按书籍维度拆分”之类的扩展。热力图的价值会随着数据积累越来越明显,前期最重要的反而不是功能,而是开始记录。

收藏这篇文章并且动手建一个自己的 `

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询