← 返回文章

Folio:从零打造一套 Hexo 自定义主题

为什么要自己写主题?

用 Hexo 搭个人站时,现成主题很多:Landscape、NexT、Butterfly……它们功能齐全,却也往往太重——开关、插件、侧栏、评论、动画一层叠一层。

我更想要的是:

  • 像作品集一样干净的首页
  • 文章列表一眼扫完
  • 深浅色切换不闪屏
  • 部署到 GitHub Pages 足够轻量

所以我做了 Folio——当前 oksite 正在使用的自定义主题。这篇文章会从「Hexo 主题是什么」讲起,再拆开 Folio 的实现,最后补上 GitHub Pages 的构建与发布流程。


Hexo 主题到底是什么?

Hexo 把内容皮肤分开:

部分 职责
source/_posts/*.md 文章正文与 front-matter
站点根目录 _config.yml 站点信息、permalink、部署、启用哪个主题
themes/<name>/ 模板、样式、脚本、主题级配置

生成站点时,Hexo 大致做三件事:

  1. 读 Markdown,渲染成 HTML 片段
  2. 把片段塞进主题的 EJS 布局
  3. 拷贝 themes/*/source/ 与站点 source/ 里的静态资源到 public/

因此,自定义主题 = 约定好的目录结构 + 几套布局模板 + 一套 CSS/JS。不需要改 Hexo 内核。

主题最小骨架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
themes/folio/
├── _config.yml # 主题配置(菜单、简介、页脚…)
├── layout/
│ ├── layout.ejs # 总壳:html / head / header / footer
│ ├── index.ejs # 首页
│ ├── post.ejs # 文章页
│ ├── page.ejs # 普通页面(如关于)
│ ├── archive.ejs # 归档
│ ├── tag.ejs
│ ├── category.ejs
│ └── _partials/ # 可复用碎片
└── source/
├── css/style.css
└── js/site.js

在站点 _config.yml 里写一行即可启用:

1
theme: folio

Folio 设计目标

Folio 名字来自 portfolio / folio:更像独立开发者的作品夹,而不是功能堆叠的 CMS 皮肤。

设计原则很简单:

  1. 少页面、强信息架构:首页 Hero + 最近文章;归档按年分组;文章页窄栏阅读
  2. 配置驱动:名字、简介、菜单、页脚文案都写在主题 _config.yml
  3. CSS 变量管主题色:亮/暗模式只换 token,不写两套布局
  4. 零构建依赖主题侧:单文件 CSS + 十几行原生 JS,不绑 React/Vite
  5. 可访问与移动端:语义标签、ARIA、窄屏汉堡菜单、prefers-reduced-motion

强调色是 #d94f32,品牌标记是「大圆圈 + 中心小圆点」——克制,但有辨识度。


Folio 实现了什么

1. 配置驱动的个人主页

主题配置(themes/folio/_config.yml)大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
profile:
name: oksite
role: 独立开发者 / 产品构建者
bio: 记录软件、产品与独立创造……
github: https://github.com/oksite

menu:
首页: /
文章: /archives/
关于: /about/

footer:
note: 保持好奇,持续构建。

首页 index.ejs 直接读取 theme.profile.*

  • 左侧:HELLO 文案、姓名、角色、简介、CTA
  • 右侧:identity 卡片——姓名前两字大写 + 网格背景 + 旋转方框装饰

这样换站点时,几乎不用改模板,只改 YAML。

2. 页面类型齐全,模板保持薄

模板 作用
layout.ejs 文档外壳,挂 head / header / footer / site.js
index.ejs Hero + 最近文章列表
post.ejs 标题、日期、估算阅读时长、正文、标签、上下篇
page.ejs 关于等独立页
archive.ejs 全部文章,按年份插入小标题
tag.ejs / category.ejs 复用 archive 布局

列表项抽成 _partials/post-card.ejs:日期、标题、摘要、标签、箭头入口,首页和归档共用,避免复制粘贴。

文章页有一个小巧但实用的细节——阅读时长估算

1
<%= Math.max(1, Math.ceil(strip_html(page.content).length / 400)) %> 分钟阅读

按纯文本字数粗算,不引入额外插件。

3. 导航、激活态与品牌标

header.ejs 根据 theme.menu 循环生成链接,并用 page.path 判断当前页:

1
2
3
4
5
6
<% Object.entries(theme.menu).forEach(([label, path]) => { %>
<a href="<%- url_for(path) %>"
class="<%= /* is-active 条件 */ %>">
<%= label %>
</a>
<% }) %>

路径一律走 url_for(),这样以后若把站点挂在子路径(root: /blog/)也不会硬编码炸链。

品牌区是内联 SVG,而不是外链图片——少一次请求,也方便用 CSS 变量上色:

1
2
3
4
<svg viewBox="0 0 24 24" width="34" height="34">
<circle cx="12" cy="12" r="9" class="brand-ring"/>
<circle cx="12" cy="12" r="3.2" class="brand-core"/>
</svg>

外圈描边、内点填充,颜色都指向 --accent

4. 亮 / 暗主题:系统偏好 + 手动切换 + 防闪屏

CSS 用 token 定义两套语义色:

1
2
3
4
5
6
:root {
--bg: #f5f5f2;
--ink: #171916;
--accent: #d94f32;
/* … */
}

优先级大致是:

  1. 用户点切换 → document.documentElement.dataset.theme = 'dark' | 'light'
  2. 写入 localStorage
  3. 未手动选择时,跟随 prefers-color-scheme
  4. head同步读 localStorage,避免首屏先亮后暗:
1
2
3
4
5
6
<script>
try {
document.documentElement.dataset.theme =
localStorage.getItem('theme') || '';
} catch (e) {}
</script>

交互逻辑集中在 source/js/site.js:主题按钮 + 移动端菜单展开,总共不到 20 行。

5. 排版与响应式

  • 全局 shell 限宽(约 1120px),文章页用 narrow 更适合阅读
  • 首页双栏 Hero,小屏改单列
  • sticky 顶栏 + backdrop-filter 毛玻璃
  • 文章正文衬线字体,引用条用强调色左边线
  • @media (max-width: 760px) 收起导航为汉堡菜单
  • prefers-reduced-motion 时关掉过渡,照顾晕动敏感用户

样式刻意做成单文件、无预处理器依赖(主题侧不强制 Stylus 流水线),方便改、方便部署。

6. SEO 与基础元信息

_partials/head.ejs 处理:

  • title 拼接:文章标题 / 站点名
  • description 回退链:页面描述 → 站点描述 → profile bio
  • canonical 使用 full_url_for
  • viewport / theme-color

没有上复杂的 SEO 插件,但对个人站已经够用。


目录与数据流(串起来看)

整站生成可以看成五步:

  1. Markdown 文章:写在 source/_posts/
  2. hexo generate:启动构建
  3. generators:由 index / archive / tag / category 等生成器产出页面数据
  4. EJS 模板:套用 themes/folio/layout/*,模板内读取 configthemepagesite
  5. 发布产物:输出到 public/(HTML + css / js / images),再经 hexo-deployer-git 推到 GitHub Pages

模板里常用的数据角色:

对象 来源 典型用途
config 站点 _config.yml url、title、theme…
theme 主题 themes/folio/_config.yml profile、menu、footer…
page 当前页数据 标题、正文、日期、标签…
site.posts 全站文章集合 列表、归档、统计

写主题时优先用这些对象,以及 url_for / partial / paginator,不要自己硬拼相对路径。


从仓库到 GitHub Pages

本站使用的是经典路径:本地 Hexo 生成 → git 推送到 Pages 仓库

1. 站点配置要点

1
2
3
4
5
6
7
8
9
# _config.yml(节选)
title: oksite
url: http://oksite.net
theme: folio

deploy:
type: git
repo: https://github.com/oksite/oksite.github.io.git
branch: master

依赖里需要:

  • hexo + 各 generator
  • hexo-renderer-ejs / hexo-renderer-marked
  • hexo-deployer-git(一键部署)

package.json 脚本:

1
2
3
4
5
6
7
8
{
"scripts": {
"build": "hexo generate",
"clean": "hexo clean",
"deploy": "hexo deploy",
"server": "hexo server"
}
}

2. 本地开发

1
2
3
npm install
npx hexo server
# 或 npm run server

themes/folio 下的 EJS/CSS/JS,保存后刷新即可。写文章放在 source/_posts/,front-matter 指定 layout: post、标题、分类与标签。

3. 构建与发布

1
2
3
npx hexo clean
npx hexo generate # 输出到 public/
npx hexo deploy # 推送到配置的 git 仓库

hexo-deployer-git 会把 public/ 内容提交到目标仓库的指定分支。若仓库开启 GitHub Pages 并指向该分支,几分钟后站点更新。

用户站 oksite.github.io 与自定义域名 oksite.net 是常见组合:仓库提供托管,域名在 DNS 做 CNAME/A 记录即可。

4. 可选:GitHub Actions 自动构建

当前仓库也可以改成「源码进 main,CI 负责 generate 并推送到 Pages」。思路是:

  1. 源码仓保留 Hexo 项目(含 themes/folio
  2. Workflow 里 npm cinpx hexo generate
  3. actions/upload-pages-artifact + actions/deploy-pages 发布

好处是换电脑也能发文章;Folio 本身无特殊构建步骤,非常适合 CI


自己做一个 Folio 式主题时,可以怎么起步

若你也想从零做一个「够用、好改」的主题,建议顺序:

  1. 先定信息架构:首页 / 文章 / 归档 / 关于,别一上来加搜索评论
  2. 搭 layout 壳:header + main + footer,确认 url_for 静态资源能加载
  3. 做 post 与 index:列表卡片抽 partial
  4. 用 CSS 变量定品牌色,再补 dark mode
  5. 补 archive / page,tag、category 先复用 archive
  6. 最后才加动画、评论、阅读统计

Folio 刻意停在第 4~5 步:功能闭环,代码量可控,以后要加东西也不费劲。


和「套一个现成主题」比,多出来的价值

现成主题 Folio 这类自研主题
上手速度 需要写模板
定制成本 覆盖样式容易「打补丁」 改结构即改源码
体积与性能 常带大量开关与依赖 可压到极简
品牌一致 容易和别人撞脸 布局即个人风格
可维护性 升级主题可能冲突 完全自己掌握

对我来说,博客本身就是产品的一部分。NOTEYOU 用本地优先讲工具观,Folio 则用页面结构讲审美与取舍——少而准,胜过大而全。


小结

  • Hexo 自定义主题本质是:布局模板 + 主题配置 + 静态资源
  • Folio 围绕独立开发者站点场景,实现了:配置驱动主页、薄模板多页面、阅读时长、亮暗色与防闪屏、响应式导航、Git 一键部署友好
  • GitHub Pages 侧:hexo generate 产出静态文件,再经 hexo-deployer-git 或 Actions 发布即可

如果你也在搭个人站,不妨先问自己一句:首页打开的三秒内,访客应记住什么?
Folio 的答案是:人是谁、在做什么、最近写了什么——其余的,能省则省。


相关链接