✨我是辰海星的「文章捕手」,擅长在文字的星海中打捞精华。

这篇文章主要是在讲:怎样把 Hexo 博客适配到 shokax-can / shokaX 这一套主题环境中,并进一步完成版本回退、依赖调整、主题配置、功能增强、页面美化、留言板与说说页面接入,以及最后用 GitHub Actions 自动部署。整体上,它更像是一份“博客搭建与个性化改造记录”,内容偏实操,适合照着一步步配置。 🛠️ 1. 拉取主题代码 📦 先通过 git clone 拉取 shokax-can 仓库,并且使用浅克隆方式,减少下载内容,加快获取速度。 2. 回退到 0.5.1 版本 🔁 文章特别说明了需要把 hexo-theme-shokax 回退到 0.5.1。 同时还要: 1)删除不需要的包 hexo-renderer-aether 2)把主题版本改成 0.5.1 3)补充安装 hexo-blog-encrypt、hexo-deployer-git、hexo-renderer-multi-next-markdown-it 等依赖 这一步的重点是保证主题兼容性和后续功能正常运行。 3. 配置 _config.shokax.yml 和 _config.yml ⚙️ 文章建议下载主题默认配置文件后再做个性化修改,比如设置图片源 image_server。 另外,在站点的 _config.yml 里加入 markdown 渲染设置,包括: 1)是否允许 HTML 2)换行、链接自动识别等规则 3)目录与锚点插件 4)多行表格插件 5)注音插件 6)剧透插件 这一部分的作用是让文章渲染效果更符合 shoka 系主题需求。 4. 增加适配脚本 💻 这是文章里很重要的一部分,主要加了几个 scripts 脚本: 1)hotReload.js 实现 Hexo 本地开发时的热更新。 它会轮询页面内容变化,发现更新后自动刷新,并且还能恢复页面滚动位置。 还提到支持加密页面。 2)patchSiteInit.js 自动修改主题里的 siteInit.ts 文件,注入 hexo-blog-decrypt 事件监听。 主要用来修复密码保护文章在解密后渲染异常的问题。 3)sakura.js 往主题头部注入樱花特效模板文件,让页面能显示飘落樱花效果。 4)summaryTyping.js 给文章标题和摘要添加打字机动画,同时还能显示 AI 模型徽章。 比如会读取配置中的模型名,默认是 gpt-5-nano。 5. 自定义 source/data 资源目录 🎨 文章展示了 source/data 下的目录结构,包括头像、颜色样式、布局样式、自定义样式、多语言文件等。 这里的核心是把主题进一步“改成自己的样子”。 6. 自定义样式优化页面外观 ✨ 文中写了很多 stylus 样式,主要包括: 1)修改首页文章标题颜色 2)修改选中文字时的背景色 3)让个人描述文字显示渐变动画 4)调整 AI 总结区域文字颜色 5)修改首页文章摘要颜色 6)修复页面滑动条卡顿问题 7)让 AI 总结支持换行显示 8)让导航栏更透明 9)调整头像边框样式 10)给站点标题增加循环打字机动画 11)适配移动端,避免小屏显示异常 12)修改首页轮播图、页头、工具栏等高度 这部分说明作者不仅关注功能,也很重视视觉效果。 7. 修改语言提示内容 🌟 通过 languages.yml,可以自定义网页标签页失焦和重新聚焦时显示的文字。 例如: 显示时提示“你回来啦” 隐藏时提示“别走” 这种小细节能提升博客的趣味性。 8. 接入樱花特效页面组件 🌸 在 views 下创建 sakura.pug,并引入外部樱花脚本。 还能配置樱花数量、速度、方向、层级等参数。 作用就是让页面背景更有动态感。 9. 调整文章默认模板 📝 修改 scaffolds/post.md,让新建文章时自动带上: 1)标题 2)日期 3)分类 4)置顶字段 5)描述 6)sitemap 字段 这样每次写新文章时就不用重复手动补这些内容。 10. 配置留言板 💬 文章提到留言板功能可参考 Twikoo 来实现。 也就是说,博客可以加入评论或留言互动模块。 11. 配置说说页面 📱 文章还介绍了“说说”页面的接入方式,参考的是 daodao 项目。 主要做法是: 1)在页面中引入对应 JS 脚本 2)添加容器 div 3)配置头像、名称、数量限制、接口地址、标题等信息 这样就能拥有类似动态、碎碎念的独立页面。 12. 使用 GitHub Actions 自动部署 🚀 最后一部分是自动化部署配置。 主要流程包括: 1)监听 master 分支的指定文件变化 2)拉取仓库和子模块 3)安装 Node.js 20 4)安装 pnpm 和 hexo-cli 5)安装项目依赖 6)执行 hexo clean、hexo generate、hexo algolia 7)配置 Git 用户信息 8)将生成的静态文件自动部署到 GitHub Pages 仓库 这一部分的意义是:以后只要推送代码,就能自动发布博客。 重点来说,这篇文章最核心的内容有三类: 1)把 shokaX 主题稳定跑起来,包括版本和依赖修正 2)通过脚本补齐热更新、加密页面兼容、摘要打字机等增强功能 3)通过样式、自定义页面和自动部署,把博客从“能用”改造成“好看又好维护”的状态 ✨ 整体看下来,这是一篇偏完整的 Hexo 博客个性化改造记录,既包含基础安装,也包含进阶美化和自动化部署。

# 拉取 shokax-can

git clone https://github.com/theme-shoka-x/shokax-can --depth=1

# 回退 0.5.1

删除 package.json 中的包

"hexo-renderer-aether": "^0.0.6",

修改包

"hexo-theme-shokax": "0.5.1",

添加包

"hexo-blog-encrypt": "^3.1.9",
"hexo-deployer-git": "^4.0.0",
"hexo-renderer-multi-next-markdown-it": "^0.2.1",

# 配置_config.shokax.yml 和 _config.yml

下载 _config.shokax.yml ,个性化配置

image_server: "https://t.alcy.cc/moez"

在_config.yml 中添加

markdown:
  render: # 渲染器设置
    html: false # 过滤 HTML 标签
    xhtmlOut: true # 使用 '/' 来闭合单标签 (比如 <br />)。
    breaks: true # 转换段落里的 '\n' 到 <br>。
    linkify: true # 将类似 URL 的文本自动转换为链接。
    typographer:
    quotes: "“”‘’"
  plugins: # markdown-it 插件设置
    - plugin:
        name: markdown-it-toc-and-anchor
        enable: true
        options: # 文章目录以及锚点应用的 class 名称,shoka 系主题必须设置成这样
          tocClassName: "toc"
          anchorClassName: "anchor"
    - plugin:
        name: markdown-it-multimd-table
        enable: true
        options:
          multiline: true
          rowspan: true
          headerless: true
    - plugin:
        name: ./markdown-it-furigana
        enable: true
        options:
          fallbackParens: "()"
    - plugin:
        name: ./markdown-it-spoiler
        enable: true
        options:
          title: "你知道得太多了"

# 适配脚本

scripts/
├─ hotReload.js
├─ patchSiteInit.js
├─ sakura.js
└─ summaryTyping.js
/**
 * Hexo 开发模式热更新脚本
 * 原理:轮询检测服务器返回的内容变化,刷新页面并恢复滚动位置
 * 仅在 hexo server 模式下生效
 * 支持密码保护页面(hexo-blog-encrypt)
 */
hexo.extend.filter.register('after_render:html', function(str) {
  // 仅开发模式启用
  if (process.env.NODE_ENV === 'production') return str;
  const script = `
    <script>
      (function() {
        if (location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') return;
        const POLL_INTERVAL = 1000;
        const STORAGE_KEY = '_hotReloadScroll';
        const contentSelector = '.body.md';
        let lastContent = null; // null 表示尚未初始化
        // 恢复滚动位置
        const saved = sessionStorage.getItem(STORAGE_KEY);
        if (saved) {
          sessionStorage.removeItem(STORAGE_KEY);
          requestAnimationFrame(() => window.scrollTo(0, parseInt(saved, 10)));
        }
        // 从 HTML 字符串中提取内容用于比较
        function extractContent(html) {
          const parser = new DOMParser();
          const doc = parser.parseFromString(html, 'text/html');
          const el = doc.querySelector(contentSelector);
          if (!el) return '';
          // 使用 textContent 忽略 HTML 结构差异
          return el.textContent.replace(/\\s+/g, ' ').trim();
        }
        async function checkUpdate() {
          try {
            const res = await fetch(location.href, { cache: 'no-store' });
            const html = await res.text();
            const newContent = extractContent(html);
            // 首次运行,记录初始内容
            if (lastContent === null) {
              lastContent = newContent;
              console.log('[HotReload] 已初始化');
              return;
            }
            if (newContent !== lastContent) {
              console.log('[HotReload] 检测到内容变化,刷新页面');
              sessionStorage.setItem(STORAGE_KEY, window.scrollY.toString());
              location.reload();
              return;
            }
          } catch (e) {
            console.error('[HotReload] 检查更新失败:', e);
          }
        }
        // 立即执行一次获取初始内容,然后开始轮询
        checkUpdate();
        setInterval(checkUpdate, POLL_INTERVAL);
        console.log('[HotReload] 已启用');
      })();
    </script>
  `;
  return str.replace('</body>', script + '</body>');
});
/**
 * 自动为 hexo-theme-shokax 的 siteInit.ts 注入 hexo-blog-decrypt 事件监听
 * 解决密码保护页面解密后渲染错误的问题
 */
const fs = require('fs');
const path = require('path');
hexo.on('generateBefore', function() {
  const siteInitPath = path.join(
    hexo.base_dir,
    'node_modules/hexo-theme-shokax/source/js/_app/pjax/siteInit.ts'
  );
  // 检查文件是否存在
  if (!fs.existsSync(siteInitPath)) {
    hexo.log.warn('[PatchSiteInit] 未找到 siteInit.ts,跳过补丁');
    return;
  }
  let content = fs.readFileSync(siteInitPath, 'utf8');
  // 检查是否已经包含补丁代码
  if (content.includes("window.addEventListener('hexo-blog-decrypt'")) {
    hexo.log.debug('[PatchSiteInit] 补丁已存在,跳过');
    return;
  }
  // 查找插入位置:在 resize 事件监听之后
  const resizeListenerPattern = /window\.addEventListener\('resize',\s*resizeHandle,\s*\{[\s\S]*?passive:\s*true[\s\S]*?\}\)/;
  if (!resizeListenerPattern.test(content)) {
    hexo.log.warn('[PatchSiteInit] 未找到 resize 事件监听器,无法注入补丁');
    return;
  }
  // 注入代码
  const patchCode = `
  window.addEventListener('hexo-blog-decrypt', siteRefresh, {
    passive: true
  })`;
  content = content.replace(resizeListenerPattern, (match) => {
    return match + patchCode;
  });
  // 写回文件
  fs.writeFileSync(siteInitPath, content, 'utf8');
  hexo.log.info('[PatchSiteInit] 已成功注入 hexo-blog-decrypt 事件监听');
});
hexo.extend.filter.register('theme_inject', function(injects) {
    injects.head.file('sakura', 'views/sakura.pug', {}, {cache: true});
});
/**
 * Hexo 过滤器:为文章标题和摘要添加打字机效果,并显示 AI 模型标识
 */
hexo.extend.filter.register('after_render:html', function(str) {
  // 获取 AI 模型名称配置
  const modelName = hexo.theme.config?.summary?.model
    || hexo.config?.summary?.model
    || 'gpt-5-nano';
  // 客户端脚本:实现打字机效果和模型标识
  const script = `
    <script>
      document.addEventListener('DOMContentLoaded', function() {
        // 1. 为文章标题添加打字机效果
        const headline = document.querySelector('h1[itemprop="name headline"]');
        if (headline && !headline.dataset.typed) {
          const text = headline.textContent;
          headline.textContent = '';
          headline.dataset.typed = '1';
          let index = 0;
          (function typeHeadline() {
            if (index < text.length) {
              headline.textContent += text[index++];
              setTimeout(typeHeadline, 50);
            }
          })();
        }
        // 2. 为摘要标签页的段落添加打字机效果
        document.querySelectorAll('.tab[data-id="summary"] p').forEach(paragraph => {
          if (paragraph.dataset.typed) return;
          const text = paragraph.textContent;
          paragraph.textContent = '';
          paragraph.dataset.typed = '1';
          let index = 0;
          (function typeParagraph() {
            if (index < text.length) {
              paragraph.textContent += text[index++];
              setTimeout(typeParagraph, 30);
            }
          })();
        });
        // 3. 添加 AI 模型标识徽章
        const specialList = document.querySelector('ul.special');
        if (specialList && !specialList.parentElement.querySelector('.model-badge')) {
          const badge = document.createElement('div');
          badge.className = 'model-badge';
          badge.textContent = '${modelName}';
          badge.style.cssText =
            'position:absolute;' +
            'top:8px;' +
            'right:8px;' +
            'font-size:11px;' +
            'color:var(--text-color,#666);' +
            'opacity:0.6;' +
            'padding:2px 6px;' +
            'border:1px solid currentColor;' +
            'border-radius:3px;' +
            'z-index:10';
          specialList.parentElement.style.position = 'relative';
          specialList.parentElement.appendChild(badge);
        }
      });
    </script>
  `;
  // 在 </body> 标签前插入脚本
  return str.replace('</body>', script + '</body>');
});
source/data/
├─ assets/
│  └─ avatar.jpeg
├─ css/
│  ├─ colors.styl
│  └─ layout.styl
├─ custom.styl
└─ languages.yml

image-20260225003010063

// 首页文章标题颜色
[data-theme="dark"]:root {
  --color-red: #0fd8fee6
  --color-pink: #38a6e5cc
  --color-blue: #f3cdce
}
// 选中文字的背景颜色
::selection {
  background: #0fd8fee6 !important;
}
// 个人描述颜色 - 5 色渐变
.description{
    background: linear-gradient(135deg, #0fd8fe, #38a6e5, #8b5cf6, #f472b6, #fb923c) !important;
    background-clip: text !important;
    -webkit-background-clip: text !important;
    -webkit-text-fill-color: transparent !important;
    color: transparent !important;
    background-size: 200% 200% !important;
    animation: gradient-shift 3s ease-in-out infinite !important;
}
// 渐变色动画效果
@keyframes gradient-shift {
    0%, 100% {
        background-position: 0% 50%;
    }
    50% {
        background-position: 100% 50%;
    }
}
// 文章中的 AI 总结字体颜色
.tab[data-id="summary"] p {
  color #1ebde6
}
// 首页的文章内容颜色
.excerpt {
  color #ca88b6
}
// 修复卡滑动条的 bug
main {
  >.inner {
    min-height: 90vh;
  }
}
//AI 总结能换行
.tab[data-id="summary"] p {
  white-space: pre-line;
}
// 导航栏透明
#nav {
  backdrop-filter: saturate(100%) blur(0px) !important;
}
// 头像框样式
.overview .author .image {
  border-radius: 10% !important;
  box-shadow: 0 0 0rem .0rem !important;
  border: 0 !important;
  max-width: 100% !important;
}
// 循环打字机动画效果
@keyframes typewriter-cycle {
  0% {
    width: 0;
  }
  25% {
    width: 100%;
  }
  75% {
    width: 100%;
  }
  100% {
    width: 0;
  }
}
@keyframes blink-cursor {
  from, to {
    border-color: transparent;
  }
  50% {
    border-color: var(--text-color, #333);
  }
}
// 只作用于 .logo 内的 title 打字机效果
.logo .title {
  overflow: hidden;
  white-space: nowrap;
  border-right: 2px solid var(--text-color, #333);
  animation: typewriter-cycle 6s ease-in-out infinite, blink-cursor 0.75s step-end infinite;
  animation-delay: 0.8s, 0.8s;
  width: 0;
}
// 响应式调整
@media (max-width: 768px) {
  .logo .artboard, .logo .title {
    white-space: normal;
    overflow: visible;
    border-right: none;
    animation: none;
    width: auto;
  }
}
// 修改首页轮播图大小
#brand {
  height: 75vh !important;
}
#header {
  height: 85vh !important;
}
#imgs {
  height: 100vh !important;
}
#tool {
  top: 90vh !important;
}
.waves {
  margin-bottom: -.6875rem important;
  position: absolute important;
}
@import "css/colors.styl"
@import "css/layout.styl"
# language
zh-CN:
  # items
  favicon:
    show: 🌟你回来啦~喵😘
    hide: 🧊别走~喵😭
views/
└─ sakura.pug
script.
      window.sakuraConfig = {
        sakura: 30,
        xSpeed: 0.5,
        ySpeed: 0.5,
        rSpeed: 0.03,
        direction: "TopRight",
        zIndex: -1
      };
script(src="https://cdn.jsdelivr.net/gh/minz71/sakura-rain/sakura-rain.js" defer)

# 调整默认模板

scaffolds/
└─ post.md
---
title: <!--swig0-->
date: <!--swig1-->
categories:
 -
sticky: false
description:
sitemap:
---

# 配置留言板

参考链接:twikoo

# 配置说说页面

参考链接:daodao

<head>
    <script src="//cdn.jsdelivr.net/gh/Uyoahz26/daodao@main/dist/qexo-dao.min.js"></script>
</head>
<body>
<!-- ... -->
<div id="qexoDaoDao"></div>
<script>
    qexoDaodao?.init({
        el: "#qexoDaoDao",
        avatar: "【头像url】",
        name: "【名称】",
        limit: 5,
        useLoadingImg: false,
        baseURL: "【URL地址】",
        title: "【主题名称】"
    }).then(function (){
        console.log("qexoDaodao加载完成");
    })
</script>
</body>

# Git Actions 同步

.github/
└─ workflows/
   └─ main.yml
name: Deploy Hexo to GitHub Pages
on:
  push:
    branches:
      - master
    paths:
      - '*.json'
      - '**.yml'
      - '**/source/**'
      - '!**/source/_drafts/**'  # 排除目录内文件
      - '!**/source/_drafts/'    # 新增:排除目录本身
      - '**/themes/**'
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
          fetch-depth: 0
      - uses: actions/setup-node@v4
        with:
          node-version: 20
  
      - name: Configure PNPM
        env:
          SHELL: /usr/bin/bash  # 设置 SHELL 环境变量
        run: |
          npm install -g pnpm@latest
          pnpm setup
          PNPM_PATH=$(grep 'export PNPM_HOME=' ~/.bashrc | cut -d'"' -f2)
          echo "$PNPM_PATH" >> $GITHUB_PATH
          echo "PNPM_HOME=$PNPM_PATH" >> $GITHUB_ENV
      - name: Install Hexo CLI
        run: pnpm add -g hexo-cli  
      - name: Install Dependencies
        run: pnpm install
      - name: Generate static files
        run: |
          hexo clean
          hexo generate
          hexo algolia
      - name: Deploy with hexo-deployer-git
        env:
          GIT_USER: $<!--swig2-->
          GIT_EMAIL: $
          GH_TOKEN: $
          DEPLOY_REPO: $
          DEPLOY_BRANCH: main
        run: |
          git config --global user.email "$GIT_EMAIL"
          git config --global user.name "$GIT_USER"
          
          REPO_WITH_TOKEN=${DEPLOY_REPO/https:\/\//https:\/\/${GH_TOKEN}@}
          echo $REPO_WITH_TOKEN
          echo "TOKEN LENGTH: $