很早的时候就想过引入字体,但是碍于字体文件太大,所以只好为网页加载速度让步。直到最近看到了 Maple Mono 这款字体,效果预览确实有吸引到我,又开始让我的想法蠢蠢欲动——不如给网站一个新的视觉感,最终在 ChatGPT 的帮助下成功实现。

字体选择

Maple Mono 是一款开源等宽字体,更适合用在代码和控制台的体验上,手写风格的斜体给人一种很丝滑的感觉。因为只打算用在代码块的表现上,我选择了不带中文和 Nerd Fonts 的版本。

霞鹜文楷是基于日文字体 Klee One 衍生的一款开源中文字体。我一直觉得这款字体的日文特别美观,没有手写体的杂乱观感,又包含手写体特有的圆润。因为我更喜欢旧字形,所以选择了这个字体系列的繁中版,即霞鶩文楷 TC

字体引入

两种字体都提供了 CDN 引入:

考虑到文件大小和 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/ 目录下所有生成页面中的文字,极个别由外链引入的未使用文字会成为漏网之鱼,造成显示的突兀感,比如我的评论系统及表情列表中的(昵称,柏田,滑稽,小鲨鱼)。好了,现在也变成已使用了。


Dusk

Image Source : JLT4n