在博客搭建中,实现动态内容(如说说、动态)的自动同步与部署是提升用户体验的重要功能。本文将详细介绍如何从 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参数,增强错误检测

自动化配置

为实现全程自动化,需设置定时任务:

  1. 服务器同步脚本:每 10 分钟执行一次
crontab -e
# 添加以下内容
*/10 * * * * /usr/bin/python3 /opt/scripts/sync_essay_to_github.py
  1. 服务器下载脚本:每 5 分钟执行一次
crontab -e
# 添加以下内容
*/5 * * * * /bin/bash /opt/scripts/download_essay_html.sh

总结

通过本文介绍的方法,我们实现了从 API 拉取动态数据到最终展示在博客上的完整自动化流程:

  1. 同步脚本从 API 拉取全量数据并转换格式
  2. 数据被全量同步到 Hexo 的 essay.yml 配置文件
  3. 提交更新到 GitHub 后,GitHub Actions 自动构建静态页面
  4. 服务器定期从 GitHub Pages 下载最新页面,确保访客看到的是最新内容

这种方案不仅减少了人工干预,还保证了数据的一致性和及时性。在实际使用中,可根据具体需求调整同步频率和数据处理逻辑。