网站主题 Lime 抽取并开源

起初只有一个做内容分享的网站时,不存在主题的维护问题,但随着自己想法的增多再加上较好的执行力,各类型网站的数量「爆炸式」增长,为求快而施展「CV 大法」,导致了较为严重的维护和未来复用的问题……

是时候将使用多年的网站主题提取出来了,考虑到会有其他人喜欢且想用,打算把整理后的代码作为独立 Git 仓库开源;除了能在 Jekyll 中用之外,再增加对 Hexo 的支持,并可通过纯样式与交互的方式使用。

需求分析

要完成这个项目的需求,大概会经过如下几步:

  1. 将已存在各网站中「版本」最新的样式、交互与模板整理出来并存放在新开的独立 Git 仓库中;
  2. 合理划分可复用资源的文件目录结构并归一化相关配置以便于自动化操作及用户自定义;
  3. 用 Hexo 的机制实现一遍相关功能;
  4. 独立于静态网站生成器的纯样式与交互的使用方式。

除了功能方面,还需要编写较为详细的使用手册并搭建可演示的在线文档站。

以上工作完成后,先结合 KnoSys 把持有的网站中原本分布式维护的文件全部替换为集中式自动化及自定义扩展的方式;再对外适当宣传以吸引用户,为日后用户驱动的迭代方式做铺垫。

设计

目录结构

受不同静态网站生成器本身机制的限制,实际的目录结构会有些许差异;即便如此,亦可在一定程度上将结构固化下来,在此将可固化结构的「根节点」用 [CURABLE_ROOT] 的形式表示——

静态资源

先按照资源类型去划分「根节点」:

资源类型 根节点
字体 [FONT_ROOT]
图片 [IMAGE_ROOT]
样式 [STYLE_ROOT]
脚本 [SCRIPT_ROOT]

再根据功能类别去拆分文件夹:

功能类别 文件夹 所属资源类型 说明
组件 components 样式、脚本 可被组合,按需引用
页面 pages 样式、脚本 可被组合,按需引用
供应商 vendors 字体、样式、脚本 -
头图 banners 图片 -
初始化 initializers 脚本 -
补丁 polyfills 字体、样式 只在 Hexo 中使用

动态模板

片段:

[PARTIAL_ROOT]
├── components
│ └── ...
├── partials
│ └── ...
├── slots
│ └── ...
└── widgets
└── ...

布局:

[LAYOUT_ROOT]
└── ...

页面:

[PAGE_ROOT]
└── ...

模板片段

即「partial」,是对 HTML 进行拆分后的一部分结构,有些是可复用的。

常规

每个页面顶多会用到一次的非功能性片段,存放于 [PARTIAL_ROOT]/partials 文件夹:

名称/路径 说明
meta/seo SEO 相关标签
meta/render 页面渲染及兼容性相关标签
meta/feed Feed 订阅相关标签
meta/brand 网站品牌相关标签
head <head> 内非样式与脚本内容
header 页面头部
footer 页面底部

组件

多次使用的一般性可复用片段,通常会传入参数,存放于 [PARTIAL_ROOT]/components 文件夹:

名称 说明
link 用于站内或站外链接的 <a> 标签
brand-link 网站品牌链接
nav-list 导航菜单条目列表
copyright 版权声明文本及链接
doc-toc 用于使用指南、API 文档等的文档目录树

部件

为完成特定业务功能的片段,没有传入的参数,存放于 [PARTIAL_ROOT]/widgets 文件夹:

名称 说明
disqus Disqus 评论框
comment 综合评论框
share 分享到 SNS
toc 页内目录树

插槽

对下文中所描述的「内容插槽」的使用及默认处理,存放于 [PARTIAL_ROOT]/slots 文件夹:

名称 说明
banner 头图,对应插槽 banner
header 头部,对应插槽 headertitlemeta
footer 底部,对应插槽 footer
aside 侧边栏,对应插槽 aside

布局模板

名称 说明
default 作为其他布局的基础结构,包含了 <head> 及其内容,继承该布局的其他布局的内容会被渲染在 <body>
page 衍生自 default,用于常规页面
post 衍生自 page,用于文章
doc 衍生自 default,用于使用指南、API 文档等
blank 没有任何 HTML 标签,完全空白
hack 只在 Hexo 中使用,用于支持通过页面配置默认值指定的动态布局

内容插槽

为提高用户自定义页面的扩展自由度,布局中的部分区域除了可通过全局或页面的配置对预置功能进行控制,还可添加内容区域自定义的内容块:

名称 说明
banner 头图
header 头部
title 标题
meta 作者、标签等信息
content 正文
footer 底部,大尺寸设备中视觉上是正文右侧
aside 侧边栏

实现方案有以下几种:

  1. 在布局中通过模板片段的方式引用页面配置中以 ksio_slot_[SLOT_NAME] 形式定义的文件路径;
  2. 通过静态网站生成器的机制将页面中定义的内容块设置为构建流程中的某个变量,再在布局中判断并输出。

其中,第一个方案实现成本低很多,不需要深入了解静态网站生成器的相关机制。

内置页面

名称 说明
index 首页
404 未找到资源
posts 文章列表

全局配置

将用于控制主题模板中预置渲染逻辑的配置项通过 TypeScript 的类型定义 ThemeConfig 进行表示:

interface Link {
readonly text: string;
readonly url?: string;
readonly children?: Link[];
}

interface BrandConfig {
readonly color?: string; // 品牌主题色
readonly icon?: string; // 品牌图标,默认为主题自带的 `favicon.ico`
readonly parent?: Link; // 当前网站的父品牌,指定后会在当前网站品牌的左边显示
readonly text?: string; // 当前网站品牌所要显示的文本,不指定则显示网站标题,默认为网站标题
}

interface CopyrightPeriod {
readonly start?: Date | string | number; // 起始时间,一般只有年份,默认使用网站构建时间
}

interface CopyrightConfig {
readonly fragments?: string; // 显示在版权声明右侧的额外信息,值是可以包含 HTML 的字符串
readonly owner?: Link; // 版权所有人,默认使用网站标题与地址
readonly period?: CopyrightPeriod; // 有效期间
readonly provider?: boolean; // 是否显示主题与提供者声明,默认为 `true`
}

interface FooterConfig {
readonly links?: Link[]; // 页面底部链接
readonly partial?: string; // 预置布局模板中页脚部分,可指定自定义的替换主题预置的
}

interface HeaderNavBar {
readonly placement?: 'left' | 'right'; // 导航菜单显示位置,默认为 `'left'`
}

interface HeaderConfig {
readonly navbar?: HeaderNavBar; // 页头导航栏
readonly navs?: Link[]; // 页面头部导航
readonly partial?: string; // 预置布局模板中页头部分,可指定自定义的替换主题预置的
}

interface MasterConfig {
readonly name?: string; // 站长名称
}

interface MetaConfig {
readonly url: string; // 网站发布后的访问链接
}

interface SeoConfig {
readonly suffix?: boolean; // 是否在 `<title>` 中将站名作为后缀拼接,默认为 `true`
}

interface SocialComment {
// Disqus
readonly disqus?: {
shortname: string; // Disqus 上注册的网站名
// 以下几个配置项是在无法加载 Disqus 的情况下的代理,
// 默认为 DisqusJS(详见 https://disqusjs.skk.moe/)
proxy?: {
endpoint: string; // 用于 DisqusJS 的 `api` 配置项
key: string; // 用于 DisqusJS 的 `apikey` 配置项
};
username?: string; // 用于 DisqusJS 的 `admin` 配置项
label?: string; // 用于 DisqusJS 的 `adminLabel` 配置项
};
// 多说
readonly duoshuo?: {
shortname?: string;
};
}

interface SocialFeed {
readonly rss?: boolean; // 是否生成 RSS feed,默认为 `false`,会用到 `meta.url`
readonly atom?: boolean; // 是否生成 Atom feed,默认为 `false`
}

interface SocialConfig {
readonly comment?: SocialComment; // 评论功能
readonly feed?: SocialFeed; // Feed 订阅功能
readonly share?: boolean; // 是否启用将页面分享到其他 SNS 的按钮,默认为 `true`
}

interface AnalyticsConfig {
// TODO
}

interface ThemeConfig {
readonly brand?: BrandConfig; // 品牌相关配置,主要控制页头左上角的链接与文本
readonly copyright?: CopyrightConfig; // 网站版权声明
readonly footer?: FooterConfig; // 页面底部
readonly header?: HeaderConfig; // 页面头部
readonly master?: MasterConfig; // 站长信息
readonly meta?: MetaConfig; // 网站信息
readonly seo?: SeoConfig; // SEO 相关配置
readonly social?: SocialConfig; // 社交相关功能的开关与配置
readonly analytics?: AnalyticsConfig; // 数据统计相关配置
}

页面配置

即定义在 Front Matter 中的变量,除了会用到静态网站生成器预定义与约定俗成的变量之外,主题内也自定义了一些——

SEO

影响生成 SEO 相关信息的变量:

变量名 值类型/可选值 说明
ksio_seo_title string 显示在 <title> 中的页面标题,不指定则使用 title
ksio_seo_role `’writer’ ‘developer’`

资源注入

除了整站共用的个别静态资源会写死在 </head></body> 前方之外,有些只跟特定布局和页面绑定的静态资源得按需注入,可与设置页面配置默认值的能力相结合:

变量名 值类型/可选值 说明
ksio_asset_js `string string[]`
ksio_asset_css `string string[]`

布局插槽

详见上文中对「内容插槽」的描述。

功能开关

变量名 值类型/可选值 说明
ksio_shareable boolean 是否显示分享按钮

生成适配

为了主题预置功能可以正常运作而针对不同的静态网站生成器进行适配,在实际使用时很可能会受具体环境限制而有所差异。

文件路径

无论是 Jekyll 还是 Hexo,主题所提供的文件都放在 ksio_ksio 文件夹(中的某个子孙文件夹)内;上文中定义的形式化的「根节点」具体分别为:

根节点 Jekyll Hexo
[FONT_ROOT] _assets/fonts/ksio source/fonts/ksio
[IMAGE_ROOT] _assets/images/ksio source/images/ksio
[STYLE_ROOT] _assets/stylesheets/ksio source/stylesheets/ksio
[SCRIPT_ROOT] _assets/javascripts/ksio source/javascripts/ksio
[PARTIAL_ROOT] _includes/ksio layout/_ksio
[LAYOUT_ROOT] _layouts/ksio layout/_ksio/layouts
[PAGE_ROOT] _pages layout/_ksio/pages

主题配置

在 Hexo 中,可通过指定方式为主题进行全局配置;而 Jekyll 并没有指定的配置方式,故主题的全局配置都挂在配置文件中的 ksio 下面:

ksio:
brand:
color: "#0871ab"
copyright:
owner:
text: 欧雷流
url: https://ourai.ws/

在 Jekyll 中可按匹配规则为页面配置设置默认值,而 Hexo 不具备此机制,需要利用过滤器实现下:

hexo.extend.filter.register('template_locals', function(locals) {
const defaults = {/* 将默认值赋给匹配到的文件 */};

locals.page = { ...locals.page, ...defaults };

return locals;
});

这个方式有局限——

改变 page.layout 并无法影响 Hexo 所要渲染的布局模板,即与文件页面配置中的 layout 不等效。先通过主题内置的布局模板动态调整下,过后了解 Hexo 的内部流程后再实现个完美的。

另外,不知什么原因,before_post_renderafter_post_render 这两个过滤器都不起作用,在了解 Hexo 机制时顺便排查下。

组件片段

在 Jekyll 和 Hexo 中并没有「组件」相关支持,但通过它们所提供的片段复用机制可以进行一定程度上的模拟——

在 Jekyll 中是 include 标签,片段中用到的变量在使用时可不必显式传入:

<!-- 定义片段 `ksio/components/link.html` -->
<a href="javascript:void(0);">{% if include.wrap %}<span>链接</span>{% else %}链接{% endif %}</a>

<!-- 使用片段 -->
<p>{% include ksio/components/link.html %}</p>

在 Hexo 中是 partial 函数,片段中用到的变量在使用时必须显式传入:

<!-- 定义片段 `_ksio/components/link.ejs` -->
<a href="javascript:void(0);"><% if (wrap) { %><span>链接</span><% } else { %>链接<% } %></a>

<!-- 使用片段 -->
<p><%- partial('_ksio/components/link', { wrap: false }) %></p>

为保持使用时的一致性,在 include 标签和 partial 函数中的片段路径是基于各自指定目录结构的相对路径,即相对于 _includesksio/components/link.html 或相对于 layout_ksio/components/link

代码高亮

假如是要在文章中展示,无论是 Jekyll 或是 Hexo,都需先自建一个 post.scss 文件,里面引入主题预置的样式片段:

// `stylesheets/pages/post.scss`
@import "../ksio/pages/post";

若是使用 Jekyll,需在配置文件中设置 highlighter: pygments,再在 post.scss 中添加高亮样式文件的引用:

// `stylesheets/pages/post.scss`
@import "../ksio/pages/post";
@import "../ksio/syntax-highlighting";

在 Hexo 中则通过页面配置 ksio_asset_css 添加。

社媒优化

发布版本

新建 release 分支专门用于存放发版文件,根据使用场景将相关文件打包成压缩包后在发版页面提供下载。

使用方式

独立使用

任务

处理中 (1)

  • 独立于静态网站生成器使用的样式与交互

未开始 (1)

  • 发版自动化脚本

已完成 (10)

  • 将个人网站与各 API 文档站的通用文件抽取出来
  • 集中管理主题预置文件
  • 使页头中的元素可配置并动态渲染
  • 使页脚中的元素可配置并动态渲染
  • 页头与页脚可使用用户自定义文件
  • 统一主题相关配置定义与使用方式
  • Hexo 模板适配
  • Hexo 中用于设置 Front Matter 默认值的插件
  • 字体文件引用适配
  • 代码高亮适配