Hexo 博客 GitHub 贡献图加载失败排查与修复技术文档

1. 问题描述

在 Hexo 博客(Butterfly 主题)中使用 hexo-filter-gitcalendar 插件时,贡献图无法正常显示。表现为:

  • 控制台报错 TypeError: can't access property "date", git_lastweek[git_thisdayindex] is undefined
  • 统计区域显示“过去一年提交 [object Object]”,而不是数字。
  • 日历网格有时能渲染部分格子,但布局错乱。

2. 环境与工具

  • Hexo 版本:8.1.1
  • 主题:Butterfly 5.5.4
  • 插件:hexo-filter-gitcalendar 1.0.11
  • API 后端:自建 Flask API,部署于 Vercel
  • 数据源:https://github-contributions-api.jogruber.de/v4/{username}
  • 浏览器:Chrome / Firefox

3. 排查过程

3.1 初步检查 API 可用性

使用 curl 测试 API:

1
curl https://api.abbkirito.online/Sunrisepeak

返回了包含 contributions 数组的 JSON,但 contributions 是扁平的一维数组(每项包含 date 和 count)。API 本身工作正常,无 CORS 错误。

3.2 观察浏览器控制台错误

打开博客页面,F12 开发者工具发现错误:

1
TypeError: can't access property "date", git_lastweek[git_thisdayindex] is undefined

说明插件在访问 git_lastweek 数组的某个索引时,该位置不存在。这通常是由于 contributions 数组长度不足或结构不符合预期。

3.3 分析插件源码

查看 gitcalendar.js 源码发现插件期望的 data.contributions 是一个二维数组,外层长度至少为 53(代表一年 53 周),内层每个数组长度为 7(代表一周 7 天)。插件会通过以下方式提取数据:

1
2
3
git_lastweek = data.contributions[52];  // 第53周
git_thisdayindex = git_lastweek.length - 1;
git_thisday = git_lastweek[git_thisdayindex].date;

如果 contributions 不是二维数组,或者长度不足 53,就会导致越界错误。

3.4 检查 API 返回格式

原始 API 返回的是扁平数组(第三方 API 的原始格式),与插件要求的二维数组不符。虽然尝试过返回二维数组,但未补足到 53 周,导致 git_lastweek 可能为 undefined

3.5 统计部分显示 [object Object] 的原因

统计部分(“过去一年提交”)显示 [object Object],是因为插件从 data.total 读取总提交数,但该字段可能被错误赋值为一个对象(例如,{"total": 0} 中的 total 是数字,但如果 API 返回的 total 字段是嵌套对象或未正确解析,就会显示 [object Object])。实际上,第三方 API 返回的 total 是正确的数字,但在某些情况下(如 API 包装时未正确处理)可能导致类型错误。

3.6 最终定位

  • 根本原因:API 返回的 contributions 格式不符合插件要求(需要严格 53 周的二维数组),且 total 字段可能因数据源问题被错误处理。
  • 关键错误:插件访问 git_lastweek[git_thisdayindex] 时越界,导致整个渲染流程中断。

4. 解决方案

4.1 修正 API 返回格式

修改 API 代码,确保:

  • contributions 为二维数组,且长度至少为 53 周,每周 7 天。
  • 不足的周在前面补空数据(日期递减,count = 0)。
  • 最后一周不足 7 天时,在后面补空数据(日期递增,count = 0)。
  • total 字段手动计算(对扁平数组的 count 求和),确保为整数。

4.2 完整 API 代码

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
# -*- coding: UTF-8 -*-
# api/index.py - GitHub Contributions API for hexo-filter-gitcalendar

from flask import Flask, jsonify, request
from flask_cors import CORS
import requests
from datetime import datetime, timedelta

app = Flask(__name__)
CORS(app) # 允许所有跨域请求

def list_split(items, n):
"""将列表按每 n 个元素分割成子列表"""
return [items[i:i + n] for i in range(0, len(items), n)]

def getdata(name):
try:
# 从第三方 API 获取数据(扁平数组)
url = f"https://github-contributions-api.jogruber.de/v4/{name}"
resp = requests.get(url, timeout=10)
resp.raise_for_status()
data = resp.json()
days = data.get("contributions", []) # 扁平数组,每个元素包含 date 和 count

# 手动计算总贡献数(确保是整数)
total = sum(day.get("count", 0) for day in days)

# 按7天分割成周
weekly = list_split(days, 7)

# 补足到至少53周(前面补空周,日期递减)
while len(weekly) < 53:
# 获取当前第一周的日期
first_week_start = datetime.strptime(weekly[0][0]["date"], "%Y-%m-%d")
# 计算前一周的日期范围
prev_week_start = first_week_start - timedelta(weeks=1)
new_week = []
for i in range(7):
date = prev_week_start + timedelta(days=i)
new_week.append({"date": date.strftime("%Y-%m-%d"), "count": 0})
weekly.insert(0, new_week)

# 确保最后一周有7天(后面补空,日期递增)
last_week = weekly[-1]
if len(last_week) < 7:
last_date = datetime.strptime(last_week[-1]["date"], "%Y-%m-%d")
for i in range(1, 7 - len(last_week) + 1):
new_date = last_date + timedelta(days=i)
last_week.append({"date": new_date.strftime("%Y-%m-%d"), "count": 0})

# 只取前53周(防止超出)
return {
"total": total,
"contributions": weekly[:53]
}
except Exception as e:
return {"error": str(e)}

@app.route('/')
def home():
return jsonify({
"message": "GitHub Calendar API",
"usage": [
"/?username - 例如 /?Sunrisepeak",
"/api?username=<name> - 同上",
"/<username> - 路径参数"
]
})

@app.route('/api', strict_slashes=False)
@app.route('/', strict_slashes=False)
def get_calendar():
username = request.args.get('username')
if not username:
qs = request.query_string.decode('utf-8')
if qs and '=' not in qs:
username = qs
if not username:
return jsonify({"error": "Missing username"}), 400
return jsonify(getdata(username))

@app.route('/<username>', strict_slashes=False)
def get_calendar_by_path(username):
return jsonify(getdata(username))

app = app

4.3 部署配置

  • requirements.txt
1
2
3
Flask
requests
flask-cors
  • vercel.json(可选):
1
2
3
{
"rewrites": [{ "source": "/(.*)", "destination": "/api/index" }]
}

4.4 Hexo 插件配置

_config.yml 中确保配置正确:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
gitcalendar:
enable: true
priority: 5
enable_page: /
layout:
type: id
name: recent-posts
index: 0
user: Sunrisepeak
apiurl: 'https://api.abbkirito.online' # 末尾不要加斜杠
minheight:
pc: 280px
mobile: 0px
color: "['#d9e0df', '#c6e0dc', '#a8dcd4', '#9adcd2', '#89ded1', '#77e0d0', '#5fdecb', '#47dcc6', '#39dcc3', '#1fdabe', '#00dab9']"
container: '.recent-post-item' # 纯 CSS 选择器
gitcalendar_css: https://npm.elemecdn.com/hexo-filter-gitcalendar/lib/gitcalendar.css
gitcalendar_js: https://npm.elemecdn.com/hexo-filter-gitcalendar/lib/gitcalendar.js

5. 验证与效果

重新部署 API 和 Hexo 后:

  • 贡献图日历网格正常渲染,显示月份和格子。
  • 统计数字正确显示(如“过去一年提交 20”),不再是 [object Object]
  • 控制台无 JavaScript 错误。

6. 总结

  • 核心教训:使用第三方插件时,必须严格遵循其期望的数据格式。hexo-filter-gitcalendar 要求 contributions 为 53 周二维数组,total 为整数。
  • 排查方法:从外部 API 开始检查,逐步深入插件源码,结合浏览器错误信息定位问题。
  • 解决技巧:在 API 层面适配插件格式,补足数据并手动计算 total,避免依赖第三方数据的潜在问题。

通过本次修复,不仅解决了贡献图加载问题,也为今后类似问题提供了清晰的排查思路。