前言
最初自建主题时,我更关心把博客需要的功能跑起来:目录、搜索、评论、加密、外链跳转、图片资源……随着功能逐渐补齐,页面也慢慢积累了另一类问题:星空仍在,但组件各自带着一套颜色和边框;夜间阅读尚可,日间模式却没有真正的层次;固定侧边栏、页脚和文章滚动之间的关系,在窄屏和短窗口中也容易失衡。
这次维护没有把它当作“换一套配色”来做,而是尝试把主题重新收束为一套可维护的视觉系统。目标很简单:保留蓝黑星空这一核心识别,让白天与夜晚成为同一片天空的两种状态;同时让模板、样式、浏览器脚本和 Hexo 构建期逻辑各归其位,之后继续增加功能时不再靠局部补丁维持页面。
本文记录这轮主题样式优化的判断过程、落地方式和最后留下的约束。它不是一份通用设计教程,而是一篇围绕现有 Hexo 主题的开发复盘。
一、从“能配置”到“能维护”
1.1 旧配置的问题不在于颜色不够多
旧主题把背景、正文卡片、侧边栏、按钮、代码块、边框和阴影都暴露为单独的配置项。开始看上去很灵活,但实际维护时有两个问题。
第一,颜色的语义被组件名替代了。修改夜间背景时,往往还要记得同步改侧栏、卡片和按钮;新增一个搜索弹窗或评论组件时,也很容易漏掉日间颜色。第二,用户配置越多,主题内部越难保证对比度、层级和动效一致。配置文件成了“把所有 CSS 变量再写一遍”,却没有真正降低维护成本。
因此这次做了破坏性收缩,只保留真正适合站点维护者调整的入口:外观策略、两枚强调色、基础字号和两个布局尺度。
yaml123456789style: appearance: night # night | day | system accent: "#E7A63A" accent_secondary: "#73C4F5" typography: font_size: "16px" layout: content_width: "46rem" sidebar_width: "18rem"
其余颜色不再按“哪个组件用什么色”暴露,而是由主题内部的语义令牌统一推导。例如画布、文章表面、顶栏/侧栏、次级控制器、正文、弱文字、边框、遮罩与滚动条各有明确角色。组件只消费角色,而不直接拥有调色板。
text12345678--dt-canvas 页面天空与底色 --dt-surface 文章等主要表面 --dt-chrome 顶栏、侧栏、页脚的低对比度表面 --dt-control 卡片内的次级按钮或 chip --dt-text / muted 正文与弱文字 --dt-border 共享边界 --dt-accent 当前状态和主操作 --dt-accent-secondary 链接与辅助定位
这样做的重点并非把颜色藏起来,而是让“昼夜切换”成为令牌层的变化。搜索、目录、评论、代码块和静态页面可以自动获得同一套层级,不需要逐个组件补一段日间 CSS。
1.2 主题仍然以星空为中心
夜间不是深色页面上点缀几个星星,而是主题的默认状态:深靛蓝的天幕、低密度星光、流星,以及月光金和冷蓝的交互强调。日间也没有简单反转成纯白底,而是采用偏暖的天空白、日晕、云层光尘和沉稳的金色。两种状态共享卡片、文字、边框和交互层级,只是同一片天空处于不同时间。
外观策略支持 night、day 和 system。前两者固定初始状态,system 读取浏览器的 prefers-color-scheme。页头的圆形日月按钮只保存浏览器本地偏好,不回写主题 YAML;这样站点默认策略和访问者个人选择可以共存。
日月本身也不再是两个始终并列的图标。背景中使用一个超出屏幕的轨道作为旋转面,切换时月亮沿轨道下沉、太阳从另一侧进入;按钮则只保留当前可见的一个符号。日间画布补充缓慢移动的云层和日晕,夜间保留星空与流星。持续动画在页面不可见、用户启用减少动效或屏幕过窄时会降低或停止,避免把装饰变成阅读负担。
二、先整理边界,再调整视觉
2.1 Pug 只描述页面结构
这一轮曾暴露出一个很典型的问题:页面模板一边负责输出 HTML,一边绑定点击事件、拼接小段脚本。短期很快,长期会让行为散落在搜索、文章、侧栏和静态页面中,难以测试,也很难判断某次样式调整是否改坏了交互。
因此模板只保留语义结构、可访问性属性和模块所需的数据属性。例如搜索区域作为页面根层的 dialog 输出,而不是被锁在 header 内;目录/站点概览切换按钮只提供当前标签和状态;加密文章只输出密码表单与错误区域。事件绑定、焦点管理和状态同步都交给浏览器模块。
这种边界带来了几个直接收益:
- 搜索对话框可以在全屏遮罩上居中,打开时隔离背景焦点,按
Escape关闭,并把焦点还给触发按钮。 - 外链跳转拦截可以在全局注入脚本中一次处理,而不是在每篇文章生成时改写链接或重复绑定监听器。这样不会把正常外链在构建期改成重定向地址,避免影响文章的原始链接和 SEO。
- 二维码和文章解密等行为可以独立加载。模板不再内嵌密钥相关的运行时逻辑,错误提示也使用页面内的
role="alert",而不是浏览器原生弹窗。
2.2 浏览器代码按职责拆分
浏览器端仍以 main.js 为入口,但入口只负责初始化。背景、外观、header、侧栏、二维码、加密、对话框和外链处理分别位于对应模块中;通用的焦点隔离和对话框生命周期归入 utils/。
text1234source/js/ ├── layout/ # 背景、外观、顶栏、侧栏等页面骨架 ├── features/ # 二维码、加密等可选能力 └── utils/ # dialog、外链拦截、滚动等通用行为
这不是为了追求目录数量,而是为了让每一层的依赖方向保持单向:Pug 输出结构,CSS 消费语义 class/id 和令牌,JS 为结构绑定行为;Hexo 的过滤器和 injector 只在构建期工作。之后修改一个按钮的视觉时,不需要顺手触碰模板脚本,也不会把构建期状态泄漏到最终静态资源中。
三、重新组织页面骨架
3.1 让页脚固定,文章独立滚动
传统文档流中,页脚会跟随文章内容向下移动。对长文来说没有问题,但在这个主题里,顶栏、侧栏、阅读进度和回到顶部都已经是页面框架的一部分;继续让整个 body 滚动,容易造成组件各自监听不同滚动源。
最终布局采用固定视口 shell:主容器占用 100dvh,顶栏与页脚保留在框架中间,#content-wrapper 以 flex: 1、min-height: 0 承接文章滚动。阅读进度、目录高亮和回顶都以同一个内容区域为事件源。
text12345678┌──────────────────────────────────────────┐ │ header │ ├──────────────┬───────────────────────────┤ │ sidebar │ content-wrapper(滚动) │ │ │ └── article stage │ ├──────────────┴───────────────────────────┤ │ footer │ └──────────────────────────────────────────┘
这里要特别区分宽度与高度。屏幕窄不等于窗口矮:小于桌面阈值时,侧栏转换为抽屉或隐藏入口;高度较短时,则只压缩顶栏、页脚和辅助内容,不能误把桌面窗口切成手机布局。触摸设备通过 pointer: coarse 增大操作目标,而不是只凭宽度猜测输入方式。
3.2 目录不再用符号模拟层级
文章目录曾在最左边使用 >> 作为前导符。它有明显的旧式终端感,但放在新的低对比度轨道中显得过于突兀,也无法自然表达当前阅读位置。
新目录使用一条弱对比度的垂直轨道和圆点节点:普通节点保持弱文字颜色,hover 和当前章节使用强调色与轻微的光晕。层级仍由缩进和编号配置表达,不再依赖重复的装饰符号。桌面侧栏中的目录与站点概览共享同一个切换入口,按钮文字随当前视图变化,避免“按钮写着站点概览,下面却显示文章目录”的状态错位。
3.3 不把天体避让做成内容位移
背景右侧的日月天体需要空间,但内容卡片不能因此看起来向侧边栏倾斜。这个问题在宽屏上尤其明显:第一次实现使用了安全区加 translateX 左移,虽然右侧空出来了,文章舞台却不再处于主容器中心。
最终改为在宽度预算中处理天体区,而不是移动内容。设当前主容器可用宽度为 W,主题可读宽度为 S,单侧天体安全距离为 C,则桌面舞台宽度为:
text1stage = min(S, W - 2C - gutter)
左右对称扣除 C 后,卡片仍使用 margin: auto 居中。右侧不会贴近天体,左侧也不会被推向侧边栏;在侧栏隐藏的窄屏断点,安全距离归零,恢复普通单列宽度。4K 宽度下再设置单独的舞台上限,避免长文一行过长。
这个修正看起来只是一个 CSS 公式,但也提醒我:遇到布局问题时,先确认它究竟是位置模型错误还是尺寸模型错误。若根因是可用空间计算不对,继续叠加位移只会让每个断点都出现新的例外。
四、把“全站一致”落实到附加功能
首页和文章页往往最先得到关注,但主题真正容易出问题的地方是低频页面和可选功能。这次样式令牌与页面骨架调整后,也逐项检查了以下内容:
| 范围 | 本轮关注点 |
|---|---|
| 本地/Algolia 搜索 | 对话框置于全局层;两种后端共享触发入口、结果表面、背景遮罩、模糊、焦点隔离与语言包文案,不让本地搜索残留中文固定文案 |
| 评论 | Gitment、Valine、Twikoo 等第三方容器使用统一表面、文字与控制器令牌 |
| 加密文章 | 密码表单、错误提示和解密后的内容延续文章表面层级 |
| 404、隐私、条款与重定向 | 使用相同的标题、空状态和弱文字规则,不再像独立页面 |
| 二维码与赞赏 | 提示文案允许回退到当前语言,避免主题默认中文覆盖英文页面 |
| 滚动条 | 内容区与侧栏分别使用昼夜令牌,而不是浏览器默认白色滚动条 |
国际化同样不能只翻导航菜单。页面标题、搜索占位符、空结果、倒计时、加密提示、二维码和赞赏提示都应该优先读取语言包;配置留空时才回退到该语言的默认文本。这样站点可以用一份主题配置切换语言,而不会在英文页面里突然出现“本地搜索”几个中文字。
五、验证不是只运行一段 JavaScript
主题包含 Pug、Stylus、浏览器 ES Module、Hexo filter、injector 和 generator。直接执行某个 JS 文件,无法证明 Hexo 的配置合并、页面生成、资源注入和最终选择器仍然正确。因此这轮验证一直以 Hexo CLI 为核心。
bash123456# 在博客根目录 npm run clean npm run build # 在主题目录 node tests/theme-contract.test.js
契约测试覆盖了子路径资源、外观策略、搜索与静态页面开关、加密、站点地图、robots、本地搜索、语言包和 themeinit 配置初始化等场景。它不替代浏览器测试,但可以防止模板/配置/注入层在重构后悄悄失配。
浏览器端则分别检查首页、长文、带目录文章、归档、标签/分类、搜索、404、加密和重定向页面,并覆盖夜间、日间和系统跟随模式。重点视口包括常规桌面、短高度窗口、横向平板、手机和 4K 宽屏。对于天体安全区这类几何问题,还需要在真实构建页面中检查卡片中心线、侧栏距离和天体边界,而不能只相信 CSS 的字面公式。
六、这次重构留下的原则
经过这一轮调整,主题没有变成一个需要大量设计工具才能维护的系统;相反,它把几个容易反复出错的边界固定下来:
- 配置只暴露稳定、用户确实需要决定的选项;组件细节由语义令牌统一管理。
- Pug 负责结构,浏览器模块负责行为,Hexo 脚本负责构建期工作,不让三者互相越界。
- 先为所有同类组件定义共同视觉语言,再处理具体页面,避免“修一个按钮、补一段颜色”的局部堆叠。
- 响应式规则按宽度、高度和输入能力分别设计;手机不是缩小的桌面,短窗口也不是手机。
- 构建期测试与实际页面测试缺一不可。前者验证 Hexo 生命周期,后者验证阅读和交互的最终结果。
星空仍然是主题最重要的视觉基调,但它不应该压过文章。白天的太阳、夜间的月亮、侧栏的轨道、搜索的遮罩和滚动条都只是阅读环境的一部分。对一个偏技术记录的博客来说,最理想的状态不是每个元素都抢眼,而是读者需要时能找到功能,阅读时又几乎感觉不到它们的存在。

