前言

Eurkon 的文章头部很有辨识度,但它是 Butterfly 的深度分支,整套覆盖会增加升级成本。本次只移植文章头部这一项:封面渐变、分类标签、动态波浪、主题色和评论统计,其余仍使用当前 Butterfly。

一、为什么不直接修改主题模板

node_modules/hexo-theme-butterfly/ 中的文件会在重新安装依赖时被覆盖。如果直接改 Pug 模板,下一次 npm ci 或主题升级就需要重新合并。

这里采用“生成后增强”:

Butterfly 正常渲染文章 HTML

after_render:html 过滤器识别文章头部

增加 class、data 属性和额外结构

专属 CSS 与运行时脚本接管视觉效果

核心文件都在主题目录之外:

scripts/post-header-effect.js
source/css/post-header-eurkon.css
source/js/post-header-eurkon.js

二、怎样决定一篇文章是否启用

脚本默认效果名为 eurkon

const effect = page.post_header_effect === undefined
? 'eurkon'
: page.post_header_effect

if (effect !== 'eurkon') return html

因此普通文章有封面就会启用。如果某篇文章不需要,在 Front Matter 中写:

post_header_effect: false

文章还必须存在 top_imgcover

const cover = page.top_img || page.cover
if (!cover || cover === false) return html

没有图片时保留 Butterfly 原始头部,避免生成一个没有视觉主体的空效果。

三、如何在原头部上增加能力

过滤器先查找 Butterfly 已生成的头部:

const headerPattern =
/<header class="([^"]*\bpost-bg\b[^"]*)" id="page-header"([^>]*)>/

匹配后增加:

<header
class="post-bg post-header-eurkon"
data-post-header-effect="eurkon"
data-post-header-cover="封面地址"
data-post-header-color="可选颜色">
</header>

class 交给 CSS,data-* 交给浏览器脚本。构建阶段不需要知道图片最终应使用什么颜色。

长标题会额外获得类名:

const longTitleClass =
Array.from(String(page.title || '')).length > 30
? ' post-header-long-title'
: ''

使用 Array.from() 而不是普通字符串长度,对中文和部分 Unicode 字符更稳妥。

四、为什么封面要重新放进一个 img

Butterfly 原头部主要用 background-image。本效果额外插入一层:

<div class="eurkon-post-cover" aria-hidden="true">
<img class="eurkon-post-cover-img" src="封面地址" alt="">
</div>

独立的 <img> 更方便:

  • 使用 object-fit 控制裁切;
  • 通过伪元素叠加渐变;
  • 在浏览器中读取像素并计算主色;
  • 不影响 Butterfly 原来的页面背景兜底。

CSS 使用变量保存主色:

#page-header.post-header-eurkon {
--post-cover-rgb: 22, 119, 179;
--post-cover-bg: rgb(var(--post-cover-rgb));
}

渐变、阴影和波浪都引用同一变量,换色时不必逐条改样式。

五、封面主色是怎样提取的

浏览器创建一个很小的 Canvas,把封面缩放到 48 × 48

canvas.width = 48
canvas.height = 48
context.drawImage(image, 0, 0, 48, 48)
const pixels = context.getImageData(0, 0, 48, 48).data

缩小有两个目的:减少运算量,并让相邻像素自然合并,避免一块小细节决定整张图的主题色。

脚本不是简单求平均,而是先过滤不适合作为主题色的像素:

const brightness = (r * 299 + g * 587 + b * 114) / 1000
const saturation = Math.max(r, g, b) - Math.min(r, g, b)

if (alpha < 200 || brightness < 24 || brightness > 242) {
continue
}
  • 半透明像素跳过;
  • 过黑像素会让头部发闷;
  • 过白像素容易让文字失去对比度。

接着把 RGB 每个通道右移 5 位:

const key = `${r >> 5}-${g >> 5}-${b >> 5}`

原本每个通道有 256 种值,右移后只剩 8 档。相近颜色会进入同一个桶,再按饱和度加权:

const weight = 1 + saturation / 96
bucket.score += weight

得分最高的颜色桶成为主题色。比起纯平均值,这种方法更容易保留封面中有辨识度的颜色。

六、为什么有时自动取色会失败

Canvas 读取跨域图片像素需要图片服务器返回正确的 CORS 响应头。否则浏览器会把 Canvas 标记为“污染”,调用 getImageData() 时抛错。

脚本用 try...catch 兜底,并允许手工指定:

post_header_color: '#1677b3'

运行时优先使用手工颜色:

const configuredColor = parseHexColor(
header.dataset.postHeaderColor
)

if (configuredColor) applyColor(header, configuredColor)
else tryAutomaticColor(header, cover)

因此远程图床暂时没有 CORS,也不会让整个头部脚本中断。

七、动态波浪怎样实现

页面插入一段 SVG,定义一条可重复使用的路径:

<defs>
<path id="eurkon-gentle-wave"
d="M-160 44c30 0 58-18 88-18s58 18 88 18 ...">
</path>
</defs>

随后用四个 <use> 引用同一条路径,设置不同的纵向位置、透明度和动画时长:

<use href="#eurkon-gentle-wave" x="48" y="0"></use>
<use href="#eurkon-gentle-wave" x="48" y="3"></use>
<use href="#eurkon-gentle-wave" x="48" y="5"></use>
<use href="#eurkon-gentle-wave" x="48" y="7"></use>

CSS 只移动波浪组的横向位置,四层速度不同,就形成视差效果。对于设置了“减少动态效果”的用户,媒体查询会关闭动画:

@media (prefers-reduced-motion: reduce) {
.eurkon-wave-parallax > use {
animation: none;
}
}

八、分类和标签怎样移到标题上方

构建阶段直接读取 page.categoriespage.tags,生成带链接的导航:

const categoryLinks = categories.map(category =>
`<a class="post-header-category" href="${url}">...</a>`
).join('')

const tagLinks = tags.map(tag =>
`<a class="post-header-tag" href="${url}">#${name}</a>`
).join('')

新的分类标签插到 #post-info 开头,再移除 Butterfly 原元信息行中的重复分类,避免同一信息出现两次。

九、Twikoo 评论数怎样加载

只有同时满足以下条件才输出评论数容器:

  • 文章没有关闭评论;
  • 当前评论系统是 Twikoo;
  • 配置中存在 Twikoo envId

浏览器端按需加载 Twikoo 客户端,然后调用:

client.getCommentsCount({
envId,
urls: [window.location.pathname],
includeReply: false
})

本地预览时不会请求浏览量计数,避免刷新预览页面污染线上数据。接口失败时显示 ,而不是让加载动画永久旋转。

十、PJAX 为什么需要单独处理

Butterfly 切换文章时可能不刷新整个页面,所以初始化同时监听:

document.addEventListener('DOMContentLoaded', init)
document.addEventListener('pjax:complete', init)

头部使用 data-post-header-ready 防止同一页面重复取色;全局监听器也用变量保证只绑定一次。

十一、维护入口

  • 结构增强、分类标签和资源注入:scripts/post-header-effect.js
  • 封面布局、渐变和波浪:source/css/post-header-eurkon.css
  • 自动取色、Twikoo 和 PJAX:source/js/post-header-eurkon.js
  • 单篇关闭效果:post_header_effect: false
  • 单篇手工颜色:post_header_color: '#1677b3'

结语

把独立功能移植到主题外部,比直接覆盖整个 Eurkon 分支更容易维护。核心思路是让 Butterfly 继续生成可靠结构,再用过滤器和运行时脚本补充效果。