一、前言
Eurkon 风格文章头部的吸引力来自封面主色、标题层级、分类标签和动态波浪,但直接迁移整套主题会与现有 Butterfly 定制发生大量冲突。本篇采用“增强层”方案:保留 Butterfly 原模板,在构建期只给文章页注入资源,再由浏览器重组现有 DOM。
这不是完整 Eurkon 主题迁移,也不修改 node_modules/hexo-theme-butterfly/。以后升级 Butterfly 时,站点级增强文件仍然独立存在。
二、交互演示:文章头部怎样逐步增强
Hexo 博客根目录 ├─ scripts/post-header-effect.js # 判断文章并注入资源 ├─ source/js/post-header-eurkon.js # DOM 增强、取色与 PJAX └─ source/css/post-header-eurkon.css # 布局、主色和波浪
|

三、第一步:确认不修改主题依赖
不要编辑:
node_modules/hexo-theme-butterfly/layout/includes/header/index.pug
|
依赖升级会覆盖这里的修改。我们只读取 Butterfly 已经生成的结构,例如:
<header id="page-header" class="post-bg"> <div id="post-info"> <h1 class="post-title">文章标题</h1> <div id="post-meta">...</div> </div> </header>
|
四、第二步:只给文章页注入资源
新建 scripts/post-header-effect.js:
'use strict'
const STYLE_URL = '/css/post-header-eurkon.css?v=20260808-1' const SCRIPT_URL = '/js/post-header-eurkon.js?v=20260808-1'
hexo.extend.filter.register('after_render:html', (html, locals) => { const page = locals?.page const isPost = page?.layout === 'post' || page?.type === 'post'
if (!isPost || !page.cover) return html
const stylesheet = `<link id="post-header-eurkon-style" rel="stylesheet" href="${STYLE_URL}">` const script = `<script src="${SCRIPT_URL}" defer data-pjax></script>`
return html .replace('</head>', `${stylesheet}</head>`) .replace('</body>', `${script}</body>`) }, 35)
|
这样分类页、标签页和普通独立页不会加载文章头部资源。
五、第三步:找到并标记原始头部
新建 source/js/post-header-eurkon.js:
(() => { const initPostHeader = () => { const header = document.querySelector('#page-header.post-bg') const postInfo = header?.querySelector('#post-info')
if (!header || !postInfo || header.dataset.eurkonBound === 'true') return header.dataset.eurkonBound = 'true' header.classList.add('eurkon-post-header')
enhanceStructure(header, postInfo) enhanceCover(header) addWaves(header) } })()
|
data-eurkon-bound 防止 PJAX 多次进入文章时重复添加封面和波浪。
六、第四步:重组标题、分类、标签和元信息
不要重新生成另一套标题,而是移动原节点:
const enhanceStructure = (header, postInfo) => { const title = postInfo.querySelector('.post-title') const meta = postInfo.querySelector('#post-meta') const tags = document.querySelector('.post-meta__tag-list')
const shell = document.createElement('div') shell.className = 'eurkon-post-info'
if (tags) shell.append(tags) if (title) shell.append(title) if (meta) shell.append(meta)
postInfo.replaceChildren(shell) }
|
真实站点中,分类和标签选择器要根据当前 Butterfly 输出确认。若找不到某个节点,跳过即可,不要让整个标题消失。
七、第五步:把封面放进可控制的 img
背景图不方便做取色、解码状态和错误回退。可以读取头部计算样式中的 URL,再建立图片:
const coverFromHeader = header => { const background = getComputedStyle(header).backgroundImage const matched = background.match(/url\(["']?(.*?)["']?\)/) return matched?.[1] || '' }
const enhanceCover = header => { const url = coverFromHeader(header) if (!url) return
const image = new Image() image.className = 'eurkon-post-cover' image.alt = '' image.decoding = 'async' image.src = url header.prepend(image) }
|
图片失败时不要隐藏原背景,保持 Butterfly 自己的兜底。
八、第六步:使用 Canvas 提取封面主色
图片加载完成后缩小到 32×32 采样:
const extractAccent = image => { const canvas = document.createElement('canvas') canvas.width = 32 canvas.height = 32 const context = canvas.getContext('2d', { willReadFrequently: true })
try { context.drawImage(image, 0, 0, 32, 32) const pixels = context.getImageData(0, 0, 32, 32).data return dominantColor(pixels) } catch { return '' } }
|
跳过透明度过低、接近纯黑或纯白的像素,再对 RGB 分桶,选择出现次数最多的颜色。
应用到 CSS 变量:
image.addEventListener('load', () => { const accent = extractAccent(image) if (accent) header.style.setProperty('--post-accent', accent) }, { once: true })
|
九、第七步:处理跨域取色失败
当封面来自图床且没有允许跨域读取像素时,Canvas 会被污染。可以尝试:
image.crossOrigin = 'anonymous'
|
但最终是否成功取决于图床响应头。失败时应回退:
.eurkon-post-header { --post-accent: var(--theme-color, #6b66d8); }
|
取色失败只影响配色,不应该影响文章标题和阅读。
十、 第八步:添加动态波浪
创建三个装饰层:
const addWaves = header => { const waves = document.createElement('div') waves.className = 'eurkon-waves' waves.setAttribute('aria-hidden', 'true') waves.innerHTML = '<i></i><i></i><i></i>' header.append(waves) }
|
CSS:
.eurkon-waves { position: absolute; z-index: 4; right: 0; bottom: -1px; left: 0; height: 48px; overflow: hidden; pointer-events: none; }
.eurkon-waves i { position: absolute; right: -10%; bottom: -22px; left: -10%; height: 54px; border-radius: 48% 52% 0 0; background: var(--card-bg); animation: eurkon-wave 9s linear infinite alternate; }
@media (prefers-reduced-motion: reduce) { .eurkon-waves i { animation: none; } }
|
十一、第九步:建立文章头部布局
.eurkon-post-header { position: relative; min-height: 460px; overflow: hidden; isolation: isolate; }
.eurkon-post-cover { position: absolute; z-index: -2; width: 100%; height: 100%; object-fit: cover; }
.eurkon-post-header::after { position: absolute; z-index: -1; inset: 0; background: linear-gradient( 90deg, color-mix(in srgb, var(--post-accent) 82%, #111), transparent 76% ); content: ''; }
.eurkon-post-info { position: absolute; right: min(8vw, 100px); bottom: 88px; left: min(8vw, 100px); color: #fff; }
|
十二、第十步:保留评论数与原主题能力
Butterfly 或 Twikoo 可能在页面加载后异步写入评论数。因为我们移动的是原有元信息节点,而不是复制文本,后续更新仍能进入正确位置。
不要把评论数量在构建期写死,也不要因为初始值为空就删除该节点。
十三、第十一步:处理 PJAX
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initPostHeader, { once: true }) } else { initPostHeader() }
document.addEventListener('pjax:complete', initPostHeader)
|
如果从文章切到非文章页,还要确保上一个页面的计时器、观察器和临时状态被清理。
十四、第十二步:构建和验证
npm run clean npm run build npm run verify hexo server
|
至少检查:
- 有封面的文章启用增强头部;
- 无封面文章仍能正常显示标题;
- 分类页、标签页和归档页未加载专用资源;
- 跨域封面取色失败时回退主题色;
- 图片 404 时原背景与标题仍存在;
- PJAX 往返不会增加重复波浪;
- 深色模式和手机端文字可读;
- 开启“减少动态效果”后动画停止。
十五、常见问题
1. 标题出现两份
你复制了节点而不是移动原节点。使用 append() 移动现有 .post-title 和 #post-meta。
2. Canvas 报安全错误
图床不允许跨域读取像素。保留取色 try/catch 和主题色回退,不要为了取色关闭网站安全策略。
3. PJAX 后波浪越来越多
初始化前检查 data-eurkon-bound,并保证每次只创建一组 .eurkon-waves。
4. 标题被封面遮住
检查层级与 isolation: isolate,保证封面、遮罩、文字和波浪处于明确的 z-index 顺序。
十六、维护入口与结语
- 构建期注入:
scripts/post-header-effect.js
- DOM、取色和 PJAX:
source/js/post-header-eurkon.js
- 布局和波浪:
source/css/post-header-eurkon.css
这种实现保留了 Butterfly 的模板、元信息和评论能力,只把文章头部外观拆成可独立维护的增强层。主题升级时更容易检查和回退,也避免整套迁移带来的长期维护成本。