字体方案以及 Fontmin 优化
很早的时候就想过引入字体,但是碍于字体文件太大,所以只好为网页加载速度让步。直到最近看到了 Maple Mono 这款字体,效果预览确实有吸引到我,又开始让我的想法蠢蠢欲动——不如给网站一个新的视觉感,最终在 ChatGPT 的帮助下成功实现。
字体选择
Maple Mono 是一款开源等宽字体,更适合用在代码和控制台的体验上,手写风格的斜体给人一种很丝滑的感觉。因为只打算用在代码块的表现上,我选择了不带中文和 Nerd Fonts 的版本。
霞鹜文楷是基于日文字体 Klee One 衍生的一款开源中文字体。我一直觉得这款字体的日文特别美观,没有手写体的杂乱观感,又包含手写体特有的圆润。因为我更喜欢旧字形,所以选择了这个字体系列的繁中版,即霞鶩文楷 TC。
字体引入
两种字体都提供了 CDN 引入:
Maple Mono 参考 README#CDN
霞鶩文楷 TC 入驻了 Google Fonts
考虑到文件大小和 CDN 在国内的表现,我选择下载字体文件到本地并用 Fontmin 缩减体积后引入的办法。
在 source
目录下创建一个 css
和一个 fonts
文件夹,用于存放自定义的 css 和字体文件,新建一个 fonts.css
如下:
@font-face {
font-family: "LXGW WenKai TC"; // 对应 butterfly 配置中的 font_family 或 code_font_family
src: url("/fonts-min/LXGWWenKaiTC-Regular.woff2") format("woff2"), // 字体引用的路径和格式
url("/fonts-min/LXGWWenKaiTC-Regular.ttf") format("truetype"); // 备用字体路径和格式
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Maple Mono";
src: url("/fonts-min/MapleMono-Regular.woff2") format("woff2"), // 也可以不添加备用字体
url("/fonts-min/MapleMono-Regular.ttf") format("truetype");
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Maple Mono";
src: url("/fonts-min/MapleMono-Italic.woff2") format("woff2"), // 同种字体的斜体
url("/fonts-min/MapleMono-Italic.ttf") format("truetype");
font-style: italic;
font-display: swap;
}
下载的字体格式为 .ttf
,可以看到我使用的是 fonts-min
文件夹作为路径且默认格式为 .woff2
,因为接下来还需要进行压缩和转换,使网页字体加载更加高效。
修改 _config.butterfly.yml
更改字体并引入 css:
font:
global_font_size: 16px
code_font_size: 14.5px
font_family: LXGW WenKai TC
code_font_family: Maple Mono
inject:
head:
- <link rel="stylesheet" href="/css/fonts.min.css" media="defer" onload="this.media='all'" />
字体压缩
因为字体的缩减是提取使用到的字符作为子集,而每次新文章都可能会出现新字符,所以有必要为 GitHub Actions 也添加一组自动化的流程,在每次部署前都重新对字体进行压缩。
首先执行 npm install fontmin
安装 fontmin,然后在根目录新建一个 fontmin.js
如下:
import Fontmin from "fontmin";
import fs from "fs";
import path from "path";
/**
* 递归读取 `./public/` 目录下的所有 HTML 文件,提取文本内容
*/
function extractTextFromHTML(directory) {
let text = "";
const files = fs.readdirSync(directory);
files.forEach((file) => {
const fullPath = path.join(directory, file);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
// 递归处理子目录
text += extractTextFromHTML(fullPath);
} else if (file.endsWith(".html")) {
text += fs.readFileSync(fullPath, "utf-8");
}
});
return text;
}
/**
* 处理 `./public/fonts/` 目录下的所有 `.ttf` 字体,进行子集化和格式转换
*/
function minifyFonts() {
const sourceDir = "public/fonts"; // 原字体目录
const outputDir = "public/fonts-min"; // 压缩后字体存放目录
if (!fs.existsSync(sourceDir)) {
console.error("❌ Font directory not found, skipping font compression");
return;
}
// 提取 HTML 文件中的文本内容
const text = extractTextFromHTML("public");
console.log(
"✅ Text content extraction completed, character count:",
text.length
);
// 读取 `fonts` 目录下所有 `.ttf` 文件
const fontFiles = fs
.readdirSync(sourceDir)
.filter((file) => file.endsWith(".ttf"));
if (fontFiles.length === 0) {
console.error("❌ No TTF font files found, skipping process");
return;
}
// 遍历所有字体进行压缩
fontFiles.forEach((fontFile) => {
const fontPath = path.join(sourceDir, fontFile);
console.log(`⚡ Processing font: ${fontFile}`);
const fontmin = new Fontmin()
.src(fontPath)
.use(Fontmin.glyph({ text })) // 仅保留 HTML 中的字符
.use(Fontmin.ttf2woff2()) // 额外转换为 woff2 格式
.dest(outputDir);
fontmin.run((err, files) => {
if (err) {
console.error(`❌ Font ${fontFile} processing failed:`, err);
} else {
console.log(
`✅ Font ${fontFile} processing completed, generated files: ${files
.map((f) => f.path)
.join(", ")}`
);
}
});
});
}
// 执行字体压缩
minifyFonts();
在执行 hexo generate
后使用 node fontmin.js
来运行此脚本,输出的字体文件将在 public/fonts-min/
下。
由于只会处理 ./public/
目录下所有生成页面中的文字,极个别由外链引入的未使用文字会成为漏网之鱼,造成显示的突兀感,比如我的评论系统及表情列表中的(昵称,柏田,滑稽,小鲨鱼)。好了,现在也变成已使用了。
Image Source : JLT4n