用 Eleventy 打造一个三栏博客:从零搭建到多平台自动部署
之前我的个人博客用的是 Jekyll + Chirpy 主题,走了本地构建再推送的路,省去了平台构建不一致的麻烦。但那套方案有个绕不过去的限制:Chirpy 主题打包在 Ruby gem 里,能改的只有配置文件和有限的几个 include。如果你想动布局、改色调、换字体——这些在 Chirpy 的体系里都不是"改一行配置"能解决的事。Jekyll 本身又绑定在 Ruby 生态里,插件写起来说实话也不算顺手。
搬到 Eleventy 的理由很简单:它不绑定任何模板语言,不绑定任何 CSS 方法论,连输出格式都不绑定。你想要什么自己搭。副作用是,一切都要自己搭。但正是这种"自己搭"的模式,让 11ty 成了最接近裸写 HTML 的静态站点生成器。你在模板里写什么标签,它就输出什么——不会在你不知情的时候往页面里塞多余的 div、inline script 或 CSS class。这对于一个想精确控制每一行输出的开发者来说,是无可替代的体验。我希望这种对输出的克制,也能通过最终的页面传达给读者。
结构三层:数据、逻辑、表现
我把整个项目划分成三个层次。最底层是数据层,src/_data/ 目录下放着一堆 JSON 和 JS 文件。site.json 定义站点名称、时区、每页文章数等全局参数;author.json 存个人信息和社交链接;comments.json 保存 Giscus 和 Twikoo 的配置;taxonomy.js 做中文字段到 URL-safe slug 的映射。还有两个计算数据文件,sidebarCategories.js 和 sidebarTags.js,它们在构建时扫描所有文章,统计分类和标签的频率,输出给右侧栏使用。
中间是逻辑层,一个 .eleventy.js 文件处理所有构建逻辑。包括插件的注册(RSS 生成、代码高亮)、过滤器(日期格式化、阅读时间估算、HTML 剥离、分页导航的 prev/next 计算)、集合的定义(文章按日期排序、分类聚合、标签聚合),以及最重要的搜索索引生成。
搜索索引是一个特殊的集合——searchIndex。它不在模板中渲染成页面,而是在构建时遍历所有文章,读取每个 Markdown 文件,剥离掉 YAML frontmatter,再清除 HTML 标签和 Markdown 标记,截取前 1500 个字符,最后输出成一个 search.json 供客户端使用。这样做的好处是搜索索引在构建时就确定了,用户打开搜索时只需要下载一个预先生成的 JSON 文件,不需要在服务端跑任何查询。
最上层是表现层。布局模板在 src/_layouts/ 下,页面路由在 src/routes/ 下,组件在 src/_includes/ 下。CSS 全部分散在 src/assets/css/ 下的五个文件里:variables.css 存放所有设计变量(颜色、字体、间距、阴影、圆角)、base.css 做 CSS reset 和排版基调、layout.css 定义三栏网格和页面结构、components.css 覆盖所有组件样式(卡片、徽章、分页、侧边栏、搜索弹窗、文章导航、TOC 等)、theme.css 做表面装饰(噪点纹理、悬浮效果、入场动画、回到顶部按钮)。responsive.css 专门处理响应式断点。
没有用任何 CSS 框架。Bootstrap、Tailwind、Bulma——都没有。每一个像素都是手写的 CSS 自定义属性。
三栏的取舍
左侧栏是固定的 240 像素,包含作者卡片、导航菜单、一个每秒更新的数字时钟,还有一个 JavaScript 生成的当月日历。右侧栏是固定的 260 像素,包含自动生成的目录、站点统计、分类列表和标签云。中间的主内容区自适应填满剩余空间。
这个布局在桌面端表现不错,但在平板上我切成了两栏:隐藏右侧栏,左侧栏保持固定。手机上则完全放弃侧栏,左侧栏变成一个从左侧滑出的抽屉式菜单,由汉堡按钮控制开关。响应的断点选在 1200px 和 768px——前者是常见笔记本屏幕的宽度下限,后者是 iPad 竖屏和手机的分界线。
颜色的设计上,浅色模式以柔钢蓝为主色调(#5a8fb4),配以暖灰色背景(#e0e8f0)和白色卡片(#ffffff);暗色模式则切换到暖琥珀色(#e8915a),背景用深灰(#18181b),卡片用略浅的深灰(#222225)。四个辅助色分布在不同的 UI 元素上——分类标签用墨绿系,标签云用暖棕系,侧栏装饰用薰衣草系,代码块用土橘系。用户切换一次主题后,选择会被存进 localStorage,下次打开页面直接复用,不依赖系统偏好。
细节上,所有圆角都设成了 0。这是有意的选择——直角在中文排版里比圆角更接近传统印刷的质感,尤其是配合衬线字体做标题时,直角边框和规整的网格更能呼应纸媒的气质。
搜索和脚本
搜索功能用了 Fuse.js 做客户端模糊匹配。构建时先生成一个 search.json,结构是一个数组,每篇文章包含标题、URL、日期、摘要、标签、分类,以及截取过的正文前 1500 个字符。用户在搜索弹窗里输入关键词后,Fuse.js 在浏览器端做模糊匹配,权重分配是标题 50%、摘要 25%、正文 15%、标签和分类各 5%。阈值为 0.35,允许单字符匹配——这对中文搜索很重要,因为中文不像英文那样有天然的空格分词。
用户的搜索偏好历史记录存进来了,每次按 Ctrl+K 或点击搜索图标,弹窗显示的输入框自动获得焦点,搜索结果实时刷新。
JavaScript 文件全部手写,没有用 jQuery 或其他框架。主要功能包括:左侧栏移动端抽屉的开闭、返回顶部按钮的显隐(滚动超过 300px 时出现)、阅读进度条(根据 scrollY 与可滚动总高度的比值更新宽度)、标题锚点链接(为文章内容中每个标题生成一个可点击的链式图标,点击后复制完整 URL 到剪贴板)、目录生成(从 h2/h3 提取标题文本构建 TOC 列表,滚动时自动高亮当前章节)、主题切换、数字时钟(每秒更新)、当月日历(JavaScript 渲染,支持前后翻月)、以及搜索弹窗的全部交互逻辑。
Service Worker 在生产环境下注册,用来预缓存站点资源,实现离线访问能力。manifest.json 中定义了 PWA 的基本配置,包括启动画面、主题色和应用名称。
部署和 CI/CD
代码推送到 GitHub 后,GitHub Actions 自动执行部署流程。先把 _site 目录上传为 GitHub Pages 的 artifact,然后调用官方的 deploy-pages action 部署到 GitHub Pages。同时我配置了两个额外的推送目标:一个是推送到 GitLab 做镜像仓库,另一个是克隆 Codeberg Pages 仓库,把 _site 内容复制进去,写入自定义域名文件,再推送回 Codeberg。三个平台共用同一份构建产物,确保输出完全一致。
Build.ps1 是 Windows 下的构建脚本,逻辑很简单:删除旧的 _site 目录,然后执行 npx @11ty/eleventy。
模板和数据流
所有的文章使用同一个 frontmatter 结构。_posts.json 为 _posts/ 目录下的所有 Markdown 文件设置默认值:布局为 post.njk,永久链接格式为 /posts/building-a-three-column-blog-with-eleventy/。每篇文章的 frontmatter 至少包含标题、日期、分类和标签。摘要是可选的,如果不填,首页卡片上会显示正文的前几行。
评论系统同时支持 Giscus 和 Twikoo。comments.json 中配置了两个 provider 的参数,post 布局底部的 comments.njk 组件根据 provider 字段选择渲染 Giscus 的 client.js 脚本还是 Twikoo 的初始化脚本。两个评论系统可以同时启用,读者自由选择。
路由方面,除了文章详情页,系统还生成了首页分页列表、分类索引页、分类详情页、标签索引页、标签详情页、归档页、关于页、友链页、留言页和分享页。这些页面共用 base.njk 布局,各自独立渲染内容区。
结语
从 Jekyll 到 Eleventy,最大的感受不是某个具体功能的好坏,而是控制权的转移。在 Chirpy 主题下,你只能做主题允许你做的事;在 Eleventy 上,你做的每一个决定——CSS 变量命名、布局如何分层、搜索索引的权重分配——都是你自己的决定。当然代价是每件事都要自己动手。如果你喜欢开箱即用,Chirpy 仍然是更好的选择。但如果你想完全掌控博客的每一层,Eleventy 值得花时间。
项目代码完全开源,在 GitHub 上可以找到。你可以直接 fork 使用,也可以只参考某个模块的实现——比如 Fuse.js 搜索的集成方式、三栏布局的 CSS 实现、或者 GitHub Actions 多平台部署的 workflow 配置。