在博客搭建中,实现动态内容(如说说、动态)的自动同步与部署是提升用户体验的重要功能。本文将详细介绍如何从 API 拉取动态数据,同步到 Hexo 博客的说说页面,并通过 GitHub Actions 实现自动构建部署,同时覆盖本地开发与服务器部署的完整流程,以及过程中常见问题的解决方法。
需求分析
我们需要实现以下功能:
- 从指定 API 拉取全量动态数据
- 将数据同步到本地和服务器的 Hexo 源码中
- 自动生成静态的说说页面(essay/index.html)
- 部署到 GitHub Pages 并确保服务器能自动获取最新页面
- 全程自动化,减少人工干预
准备工作
环境要求
- Python 3.7+(用于编写同步脚本)
- Node.js 18.x(LTS 版本,用于 Hexo 构建)
- Hexo 博客环境(已安装主题支持说说功能)
- GitHub 账号(用于代码托管和 Pages 服务)
- 服务器(用于部署最终页面)
所需依赖
# Python依赖
pip install ruamel.yaml requests
# Hexo依赖
npm install hexo-cli -g
实现步骤
1. 数据同步脚本开发
同步脚本是核心组件,负责从 API 拉取数据并同步到 Hexo 的配置文件中。我们需要分别实现服务器端和本地(Windows)的同步脚本。
服务器端同步脚本(sync_essay_to_github.py)
该脚本需实现:从 API 拉取数据、转换格式、全量同步到 essay.yml、推送到 GitHub 仓库。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
服务器动态数据同步脚本(API全量同步版)
功能:从API拉取全量动态 → 转换格式 → 完全替换本地数据 → 推送到GitHub
核心逻辑:本地数据完全跟随API,API有则本地有,API无则本地无
"""
import requests
import json
import datetime
import os
import subprocess
from ruamel.yaml import YAML # 用于保留YAML注释和格式
# ====================== 配置参数(根据实际环境修改) ======================
# 敏感信息:从环境变量读取
ESSAY_JWT_TOKEN = os.getenv("ESSAY_JWT_TOKEN") # API访问令牌
GITHUB_PAT = os.getenv("GITHUB_PAT") # GitHub推送令牌
# 固定路径
MOMENTS_API_URL = "https://mm.demius.tech/api/memo/list" # 动态数据API地址
ESSAY_YML_PATH = "/opt/blog-source/source/_data/essay.yml" # essay.yml存放路径
GITHUB_REPO_DIR = "/opt/blog-source" # GitHub仓库本地目录
LOG_FILE = "/var/log/sync_essay_py.log" # 日志文件路径
# ===========================================================================
# 初始化YAML解析器(保留注释和格式)
yaml = YAML()
yaml.preserve_quotes = True
yaml.indent(mapping=2, sequence=4, offset=2)
def write_log(content, level="INFO"):
"""写入日志到文件和控制台"""
log_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_msg = f"[{log_time}] [{level}] {content}"
print(log_msg)
with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(log_msg + "\n")
def check_environment():
"""检查运行环境是否就绪"""
if not ESSAY_JWT_TOKEN:
write_log("未找到ESSAY_JWT_TOKEN环境变量!", "ERROR")
return False
if not GITHUB_PAT:
write_log("未找到GITHUB_PAT环境变量!", "ERROR")
return False
if not os.path.exists(ESSAY_YML_PATH):
write_log(f"essay.yml文件不存在:{ESSAY_YML_PATH}", "ERROR")
return False
if not os.path.exists(GITHUB_REPO_DIR):
write_log(f"GitHub仓库目录不存在:{GITHUB_REPO_DIR}", "ERROR")
return False
write_log("环境检查通过")
return True
def fetch_moments_from_api():
"""从API拉取全量动态数据(以API返回的所有数据为准)"""
headers = {
"x-api-token": ESSAY_JWT_TOKEN,
"Content-Type": "application/json"
}
# 拉取所有动态(size设为极大值,确保获取API返回的全部数据)
payload = {
"page": 1,
"size": 10000, # 足够大的值覆盖所有动态
"showType": 1
}
try:
response = requests.post(
MOMENTS_API_URL,
headers=headers,
data=json.dumps(payload),
timeout=10
)
response.raise_for_status() # 抛出HTTP错误
resp_json = response.json()
moments_list = resp_json.get("data", {}).get("list", [])
write_log(f"成功从API拉取{len(moments_list)}条动态数据(全量拉取)")
return moments_list
except requests.exceptions.Timeout:
write_log("API请求超时(10秒)", "ERROR")
return []
except requests.exceptions.HTTPError as e:
write_log(f"API请求失败(HTTP错误):{str(e)}", "ERROR")
return []
except Exception as e:
write_log(f"API请求异常:{str(e)}", "ERROR")
return []
def parse_date(created_at):
"""专门处理日期格式化(确保与API时间一致)"""
try:
if not created_at:
return datetime.datetime.now().strftime("%Y/%m/%d")
# 处理带时区的ISO格式(如2025-09-18T10:02:31.275248334+08:00)
dt = datetime.datetime.fromisoformat(created_at.replace("Z", "+00:00"))
return dt.strftime("%Y/%m/%d")
except Exception as e:
write_log(f"日期解析失败({created_at}):{str(e)}", "WARNING")
return "2025/01/01" # 异常时的默认日期
def convert_to_essay_format(item):
"""将API数据转换为essay.yml支持的格式(严格按API字段映射)"""
essay_item = {}
# 核心字段(与API一一对应)
essay_item["content"] = item.get("content", "").replace("\n", "<br>") # 处理换行
essay_item["date"] = parse_date(item.get("createdAt", "")) # 日期格式化
essay_item["from"] = item.get("user", {}).get("nickname", "默认作者") # 作者
# 可选字段(API有则保留,无则不添加)
if item.get("location") and item["location"].strip():
essay_item["address"] = item["location"].strip() # 地点
if item.get("externalUrl") and item["externalUrl"].strip():
essay_item["link"] = item["externalUrl"].strip() # 外部链接
# 解析扩展字段(音乐、视频等)
ext_str = item.get("ext", "{}")
try:
ext_data = json.loads(ext_str)
except json.JSONDecodeError as e:
write_log(f"解析ext字段失败:{str(e)}", "WARNING")
ext_data = {}
# 音乐播放器(aplayer)
if ext_data.get("music"):
music_info = ext_data["music"]
if music_info.get("id") and music_info.get("server"):
essay_item["aplayer"] = {
"server": music_info["server"],
"id": str(music_info["id"]) # 确保ID为字符串
}
# 视频链接
if ext_data.get("video"):
video_value = ext_data["video"].get("value")
if video_value and video_value.strip():
essay_item["video"] = [video_value.strip()] # 列表格式
# 图片链接(整合单图和多图)
images = []
# 处理单图字段
imgs = item.get("imgs")
if imgs and imgs.strip():
images.append(imgs.strip())
# 处理多图配置
img_configs = item.get("imgConfigs") or [] # 确保是列表
for img_cfg in img_configs:
img_url = img_cfg.get("url") if isinstance(img_cfg, dict) else None
if img_url and img_url.strip() and img_url.strip() not in images:
images.append(img_url.strip())
if images:
essay_item["image"] = images
return essay_item
def update_essay_yml(api_items):
"""以API数据为基准,全量替换本地essay_list(API有则本地有,API无则本地无)"""
try:
# 读取现有essay.yml(保留配置和注释)
with open(ESSAY_YML_PATH, "r", encoding="utf-8") as f:
data = yaml.load(f)
# 验证格式
if not isinstance(data, list) or len(data) == 0 or not isinstance(data[0], dict):
write_log("essay.yml格式错误!需为列表结构,第一项为配置字典", "ERROR")
return False
# 提取配置(保留标题、limit等设置,仅更新essay_list)
config = data[0]
old_count = len(config.get("essay_list", [])) # 原有数据量
# 1. 将API数据转换为essay格式
converted_items = [convert_to_essay_format(item) for item in api_items]
# 2. 按日期倒序排列(最新的在前面)
converted_items.sort(key=lambda x: x["date"], reverse=True)
# 3. 直接替换本地essay_list(完全以API数据为准)
config["essay_list"] = converted_items
# 4. 写入文件(保留配置和注释)
with open(ESSAY_YML_PATH, "w", encoding="utf-8") as f:
yaml.dump(data, f)
new_count = len(converted_items)
write_log(f"成功同步API数据:API返回{new_count}条,本地已更新(原{old_count}条)")
return True
except Exception as e:
write_log(f"更新essay.yml失败:{str(e)}", "ERROR")
return False
def push_to_github_repo():
"""将更新推送到GitHub仓库"""
original_dir = os.getcwd()
try:
# 切换到仓库目录
os.chdir(GITHUB_REPO_DIR)
write_log(f"已切换到GitHub仓库目录:{GITHUB_REPO_DIR}")
except Exception as e:
write_log(f"切换仓库目录失败:{str(e)}", "ERROR")
return False
# 计算essay.yml的相对路径
rel_essay_path = os.path.relpath(ESSAY_YML_PATH, GITHUB_REPO_DIR)
# Git命令列表(确保与远程同步)
git_commands = [
["git", "config", "user.name", "demius782"],
["git", "config", "user.email", "demius@qq.com"],
["git", "pull", "origin", "main", "--no-rebase"], # 拉取远程最新代码
["git", "diff", "--quiet", rel_essay_path], # 检查文件是否有变化
["git", "add", rel_essay_path],
["git", "commit", "-m", f"全量同步API动态数据:{datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}"],
["git", "push",
f"https://demius782:{GITHUB_PAT}@github.com/demius782/blog-source.git",
"main"]
]
try:
for idx, cmd in enumerate(git_commands):
# 处理git diff --quiet(检查是否有修改)
if idx == 3:
result = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8"
)
# 0=无修改,1=有修改
if result.returncode == 0:
write_log("essay.yml无修改,无需提交", "INFO")
return True
elif result.returncode == 1:
write_log("essay.yml有修改,开始提交流程", "INFO")
continue
else:
write_log(f"Git命令失败:{' '.join(cmd)}", "ERROR")
write_log(f"错误详情:{result.stderr}", "ERROR")
return False
# 执行其他命令
result = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8"
)
if result.returncode != 0:
if "nothing to commit" in result.stderr or "nothing to commit" in result.stdout:
write_log("无修改内容,无需推送", "INFO")
return True
write_log(f"Git命令失败:{' '.join(cmd)}", "ERROR")
write_log(f"错误详情:{result.stderr}", "ERROR")
return False
if "commit" in cmd or "push" in cmd:
write_log(f"Git命令成功:{' '.join(cmd)}")
write_log("✅ 成功推送到GitHub仓库")
return True
except Exception as e:
write_log(f"Git推送异常:{str(e)}", "ERROR")
return False
finally:
os.chdir(original_dir) # 恢复原目录
def main():
"""脚本主入口"""
write_log("=" * 60)
write_log("开始执行动态数据同步流程(API全量同步模式)")
# 1. 环境检查
if not check_environment():
write_log("环境检查失败,同步终止", "ERROR")
write_log("=" * 60)
return
# 2. 拉取API数据
api_data = fetch_moments_from_api()
if not api_data:
write_log("未获取到有效API数据,同步终止", "ERROR")
write_log("=" * 60)
return
# 3. 更新本地essay.yml(全量替换)
if not update_essay_yml(api_data):
write_log("更新essay.yml失败,同步终止", "ERROR")
write_log("=" * 60)
return
# 4. 推送到GitHub
if not push_to_github_repo():
write_log("Git推送失败,同步终止", "ERROR")
else:
write_log("同步流程完成", "INFO")
write_log("=" * 60)
if __name__ == "__main__":
main()
Windows 本地同步脚本(sync_essay_local.py)
本地脚本主要用于开发环境,实现从 API 拉取数据并同步到本地 Hexo 源码。
import requests
import json
import datetime
import os
from ruamel.yaml import YAML # 用于保留注释的YAML库
# ====================== 配置参数(请修改) ======================
MOMENTS_API_URL = "https://mm.demius.tech/api/memo/list"
JWT_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsInVzZXJuYW1lIjoibXp4aSJ9.sxRPRWK-qOaSoJDI49FJnNEAjBGfUMuw6E58_4kLQfo"
ESSAY_YML_PATH = "E:/网站/hexo/blog/source/_data/essay.yml" # Windows路径
# ==============================================================
headers = {
"x-api-token": JWT_TOKEN,
"Content-Type": "application/json"
}
def fetch_moments_data():
"""获取API全量动态数据(拉取所有数据)"""
try:
# 拉取全量数据(size设为极大值)
payload = {"page": 1, "size": 10000, "showType": 1}
response = requests.post(
MOMENTS_API_URL,
headers=headers,
data=json.dumps(payload)
)
response.raise_for_status()
resp_json = response.json()
moments_list = resp_json.get("data", {}).get("list", [])
print(f"✅ 成功获取{len(moments_list)}条动态(全量拉取)")
return moments_list
except Exception as e:
print(f"❌ API请求失败:{str(e)}")
return []
def parse_special_content(item):
"""解析扩展内容(音乐、视频、地址)"""
special_fields = {}
ext_str = item.get("ext", "{}")
try:
ext_data = json.loads(ext_str)
except:
ext_data = {}
# 音乐信息
if ext_data.get("music") and ext_data["music"].get("id"):
special_fields["aplayer"] = {
"server": ext_data["music"].get("server", "netease"),
"id": ext_data["music"].get("id", "")
}
# 视频信息
if ext_data.get("video") and ext_data["video"].get("value"):
special_fields["video"] = [ext_data["video"]["value"]]
# 地理位置
if item.get("location") and item["location"].strip():
special_fields["address"] = item["location"].strip()
return special_fields
def convert_to_essay_item(item):
"""转换API数据为essay格式(严格映射)"""
# 处理日期格式
try:
createdAt = item.get("createdAt", "")
dt = datetime.datetime.fromisoformat(createdAt.replace("Z", "+00:00"))
date_str = dt.strftime("%Y/%m/%d")
except Exception as e:
print(f"⚠️ 日期解析失败({createdAt}):{str(e)}")
date_str = "2025/01/01"
# 核心字段
content = item.get("content", "").replace("\n", "<br>")
user_info = item.get("user", {})
essay_item = {
"content": content,
"date": date_str,
"from": user_info.get("nickname", "mzxi")
}
# 合并扩展字段
essay_item.update(parse_special_content(item))
# 处理图片(整合单图和多图)
images = []
# 单图字段
imgs = item.get("imgs")
if imgs and imgs.strip():
images.append(imgs.strip())
# 多图配置
img_configs = item.get("imgConfigs") or [] # 确保是列表
for img_cfg in img_configs:
if isinstance(img_cfg, dict):
img_url = img_cfg.get("url", "").strip()
if img_url and img_url not in images:
images.append(img_url)
if images:
essay_item["image"] = images
# 外部链接
external_url = item.get("externalUrl", "").strip()
if external_url:
essay_item["link"] = external_url
return essay_item
def update_essay_yml(api_items):
"""以API数据为基准,全量替换本地essay_list"""
# 检查文件是否存在
if not os.path.exists(ESSAY_YML_PATH):
print(f"❌ 未找到文件:{ESSAY_YML_PATH}")
return False
# 初始化YAML解析器(保留注释和格式)
yaml = YAML()
yaml.preserve_quotes = True
yaml.indent(mapping=2, sequence=4, offset=2)
# 读取现有文件
try:
with open(ESSAY_YML_PATH, "r", encoding="utf-8") as f:
data = yaml.load(f)
# 验证格式
if not isinstance(data, list) or len(data) == 0 or not isinstance(data[0], dict):
print("❌ 文件格式错误:需为列表且第一项为配置字典")
return False
config = data[0]
old_count = len(config.get("essay_list", [])) # 原有数据量
print(f"✅ 读取到原有{old_count}条短文数据")
except Exception as e:
print(f"❌ 读取文件失败:{str(e)}")
return False
# 1. 转换API数据格式
converted_items = [convert_to_essay_item(item) for item in api_items]
# 2. 按日期倒序排列(最新在前)
converted_items.sort(key=lambda x: x["date"], reverse=True)
# 3. 全量替换本地数据(API有则本地有,API无则本地无)
config["essay_list"] = converted_items
# 4. 写入文件(保留注释和格式)
try:
with open(ESSAY_YML_PATH, "w", encoding="utf-8") as f:
yaml.dump(data, f)
new_count = len(converted_items)
print(f"✅ 全量同步完成:API返回{new_count}条,本地已更新(原{old_count}条)")
return True
except Exception as e:
print(f"❌ 写入文件失败:{str(e)}")
return False
def main():
print("=" * 60)
print(f"📅 同步开始:{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
# 1. 获取API数据
moments_data = fetch_moments_data()
if not moments_data:
print("📅 同步终止(无有效数据)")
return
# 2. 转换并更新本地文件
update_essay_yml(moments_data)
print(f"📅 同步结束:{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("=" * 60)
if __name__ == "__main__":
main()
2. GitHub Actions 自动构建配置
创建 GitHub Actions 工作流配置文件,实现代码推送后自动构建静态页面并部署到 GitHub Pages。
name: 云端生成essay/index.html并部署到GitHub Pages
on:
push:
branches: [ main ]
paths: # 仅当说说相关文件变化时触发(优化性能)
- 'source/_data/essay.yml'
- 'themes/anzhiyu/layout/essay/**' # 说说页面模板
- 'themes/anzhiyu/source/css/essay/**' # 说说页面样式
workflow_dispatch: # 支持手动触发
jobs:
build-and-deploy:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: 拉取仓库代码
uses: actions/checkout@v4
with:
fetch-depth: 0 # 拉取完整历史,避免Hexo构建异常
- name: 安装Node.js 18(LTS稳定版)
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm' # 缓存依赖,加速安装
- name: 安装Hexo依赖
run: |
npm install hexo-cli -g
npm install # 安装package.json中的依赖(含渲染器)
- name: 生成静态文件(essay/index.html)
run: |
hexo clean # 清理旧缓存,确保生成最新文件
hexo generate
# 验证说说页面是否生成
if [ ! -f "public/essay/index.html" ]; then
echo "错误:未找到public/essay/index.html"
exit 1
fi
echo "✅ essay/index.html生成成功"
- name: 准备部署目录(保持路径结构)
run: |
mkdir -p ./deploy/essay
cp public/essay/index.html ./deploy/essay/index.html
- name: 推送到gh-pages分支
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./deploy
publish_branch: gh-pages
配置文件存放路径:必须放在仓库的 .github/workflows/ 目录下,这是 GitHub Actions 识别工作流配置文件的固定路径。
3. 服务器自动下载最新页面脚本
创建服务器端脚本,定期从 GitHub Pages 下载最新生成的 essay/index.html 页面。
#!/bin/bash
# 服务器下载essay/index.html脚本(适配实际路径)
LOG_FILE="/var/log/download_essay_html.log"
# 云端下载地址(与Actions部署路径一致)
GITHUB_PAGES_URL="https://demius782.github.io/blog-source/essay/index.html"
# 服务器本地实际路径(你的真实路径)
LOCAL_ESSAY_PATH="/opt/1panel/apps/openresty/openresty/www/sites/blog.mzxi.cn/index/public/essay/index.html"
# 临时文件(避免替换时中断访问)
TEMP_FILE="/tmp/essay_index_temp.html"
# 写入日志函数
write_log() {
local LEVEL=$1
local CONTENT=$2
local TIME=$(date "+%Y-%m-%d %H:%M:%S")
echo "[$TIME] [$LEVEL] $CONTENT"
echo "[$TIME] [$LEVEL] $CONTENT" >> "$LOG_FILE"
}
# 1. 检查本地essay目录是否存在(不存在则创建,避免下载失败)
if [ ! -d "$(dirname "$LOCAL_ESSAY_PATH")" ]; then
write_log "INFO" "本地essay目录不存在,自动创建:$(dirname "$LOCAL_ESSAY_PATH")"
mkdir -p "$(dirname "$LOCAL_ESSAY_PATH")"
fi
# 2. 下载文件(重试3次,超时10秒)
write_log "INFO" "开始下载:$GITHUB_PAGES_URL"
if ! curl -L --retry 3 --max-time 10 --fail -o "$TEMP_FILE" "$GITHUB_PAGES_URL"; then
write_log "ERROR" "下载失败:网络异常或URL无效"
rm -f "$TEMP_FILE"
exit 1
fi
# 3. 验证文件有效性(避免404/空文件)
if [ ! -f "$TEMP_FILE" ] || [ $(wc -c < "$TEMP_FILE") -lt 100 ]; then
write_log "ERROR" "文件无效(大小<100字节,可能是404页面)"
rm -f "$TEMP_FILE"
exit 1
fi
# 4. 原子替换本地文件(直接覆盖原index.html,确保访客访问的是最新内容)
mv -f "$TEMP_FILE" "$LOCAL_ESSAY_PATH"
if [ $? -eq 0 ]; then
write_log "INFO" "✅ 成功更新:$LOCAL_ESSAY_PATH"
else
write_log "ERROR" "替换文件失败"
rm -f "$TEMP_FILE"
exit 1
fi
exit 0
常见问题及解决方法
1. 脚本运行时出现 “NoneType’ object is not iterable” 错误
错误原因:API 返回的imgConfigs字段为None,导致脚本尝试遍历None对象。
解决方法:在处理imgConfigs时确保其为可迭代对象,修改代码:
# 原代码
img_configs = item.get("imgConfigs", [])
# 修改后
img_configs = item.get("imgConfigs") or []
2. Git 拉取时出现 “untracked working tree files would be overwritten by merge” 错误
错误原因:本地存在未被 Git 跟踪的文件,与远程仓库文件冲突。
解决方法:
# 进入仓库目录
cd /opt/blog-source
# 将文件添加到Git跟踪
git add .github/workflows/generate-essay.yml
git commit -m "add generate-essay.yml"
# 拉取远程更新
git pull origin main --no-rebase
3. 去重逻辑不准确导致数据重复或遗漏
错误原因:基于内容和日期的去重方式不够精准,可能因内容相似或日期格式问题导致误判。
解决方法:采用以 API 返回的createdAt时间戳作为唯一标识的去重方式,或直接使用全量替换逻辑,确保本地数据与 API 完全一致。
4. GitHub Actions 构建失败
可能原因:
- Node.js 版本不兼容:Hexo 对最新 Node.js 版本可能存在兼容性问题
- 依赖安装不完整:缺少必要的 Hexo 插件或主题依赖
- 路径错误:生成的静态文件路径与预期不符
解决方法:
- 使用 Node.js 18.x LTS 版本
- 确保
npm install命令正确执行并安装所有依赖 - 添加构建前的路径验证步骤,确保生成的文件存在
5. 服务器下载脚本无法正常工作
可能原因:
- 网络问题:服务器无法访问 GitHub Pages
- 权限不足:脚本没有写入目标目录的权限
- 文件验证失败:下载的文件为空或为 404 页面
解决方法:
- 检查服务器网络连接和 DNS 配置
- 确保运行脚本的用户有足够权限:
chmod +x download_essay_html.sh - 增加
curl命令的--fail参数,增强错误检测
自动化配置
为实现全程自动化,需设置定时任务:
- 服务器同步脚本:每 10 分钟执行一次
crontab -e
# 添加以下内容
*/10 * * * * /usr/bin/python3 /opt/scripts/sync_essay_to_github.py
- 服务器下载脚本:每 5 分钟执行一次
crontab -e
# 添加以下内容
*/5 * * * * /bin/bash /opt/scripts/download_essay_html.sh
总结
通过本文介绍的方法,我们实现了从 API 拉取动态数据到最终展示在博客上的完整自动化流程:
- 同步脚本从 API 拉取全量数据并转换格式
- 数据被全量同步到 Hexo 的 essay.yml 配置文件
- 提交更新到 GitHub 后,GitHub Actions 自动构建静态页面
- 服务器定期从 GitHub Pages 下载最新页面,确保访客看到的是最新内容
这种方案不仅减少了人工干预,还保证了数据的一致性和及时性。在实际使用中,可根据具体需求调整同步频率和数据处理逻辑。