跳到正文

我所理解的 Hexo 最佳实践(2):让本地预览和公开链接共用一份资源

我所理解的 Hexo 最佳实践

  1. 用文件名管理文章,用稳定字段生成网址
  2. 让本地预览和公开链接共用一份资源(当前)
  3. 用系列目录组织连载正文
  4. 在首页分页前聚合连载
  5. 让列表排序与全站翻篇沿用同一顺序
  6. 把源码工作区嵌进 Hexo 文章
  7. 用 Vue 重写主题并保留页面交互
  8. 让 Hexo 支持 Markdown 扩展语法

写完那篇视频编码长文时,我先在 Hexo 页面上看了一遍,五张 SVG 都正常。切回编辑器的 Markdown 预览,文字、表格和代码块都在,图片却全是空白。两边读的是同一份 Markdown,区别在于网页里的 /files/… 从网站根目录找文件,编辑器预览则需要从本地找到那张图。

我希望写稿时直接预览本地文件,读者仍能打开以前收藏的图片和下载链接。这里以视频编码文章的流水线图为例,目录与文件名沿用本站的 Hexo 约定。

流水线图原来写的是 /files/video-codec-history-from-macbook-av1/images/…。Markdown 预览没有 Hexo 的路由表。即便填完整的线上 URL,新稿也得等图片发布后才能预览,编辑旧稿时则只能看到远端那一版。

站点根路径在网页和本地 Markdown 预览中的解析差异

Markdown 里写能找到真实文件的路径

以这张视频编码流水线图为例。它属于视频编码系列的总览,资源放在 source/files/ 下与总览文件同名的目录里。这篇正文在 source/_posts/ 下的系列目录里,从文件所在目录要先退两级再进入 files/。平铺在 source/_posts/ 下的文章则从 ../files/ 起步。目录名里有空格时,用 Markdown 的尖括号包住整个链接目标。

![视频编码流水线](<../../files/2026-09-21 - 从 MacBook 的 AV1 开始:一部视频编码编年史/images/04-encoder-pipeline.fonts.6186cd6a0f42.svg>)

编辑器可以沿着这条路径读到本地 SVG。生成网页时如果原样输出这个相对地址,浏览器会以文章 URL 为基准解析它,访问的就成了日期目录下的 files/。Hexo 需要同时转换 HTML 里的引用和资源文件的发布位置。

Markdown 引用与资源文件分别经过转换,最终汇合到同一个公开地址

渲染时换引用,生成后换资源路由

本站的 hexo-runtime/site/publish-files-by-url-title.ts 通过 marked:renderer 包装图片与链接的渲染方法。Marked 已经把 Markdown 解析成 token,此时改 token 的 href,代码块里的示例仍会保留原样。过滤器可能随多篇文章反复执行,脚本用一个 Symbol 标记已处理的 renderer,让同一个对象只包装一次。 hexo-renderer-marked 的扩展说明

// sheng-blog/hexo-runtime/site/publish-files-by-url-title.ts,节选
function rewriteLocalFilesHref(hexo, href) {
  if (typeof href !== 'string') return href;

  const normalizedHref = href.replace(/\\/g, '/');
  const suffixIndex = normalizedHref.search(/[?#]/);
  const pathname = suffixIndex === -1 ? normalizedHref : normalizedHref.slice(0, suffixIndex);
  const suffix = suffixIndex === -1 ? '' : normalizedHref.slice(suffixIndex);
  const match = LOCAL_FILES_HREF_RE.exec(pathname);
  if (!match) return href;

  const sourceName = decodePathSegment(match[1]);
  const post = hexo.locals
    .get('posts')
    .toArray()
    .find((item) => getPostSourceName(item) === sourceName);
  if (!post || !post.url_title) return href;

  return `/${FILES_PREFIX}${post.url_title}/${match[2]}${suffix}`;
}

匹配规则只接收一层或多层 ../ 后紧跟 files/ 的链接。函数用目录名找资源所属的文章,因此一篇文章也可以引用另一篇文章的图。它只对目录段尝试解码,保留文件子路径、查询参数和片段。站外链接、根路径和找不到所属文章的链接继续走原来的渲染逻辑。

这一步只决定浏览器要请求哪里。Hexo 默认仍会为 source/files/ 下的文件生成包含中文目录名的 route。脚本在 after_generate 里把这些 route 改到 files/<url_title>/…。它先把源 route 读成 Buffer,保留原有的修改标记,再设置目标 route、移除源 route。图片、字体和压缩包都按字节搬运。本地预览服务与静态生成读取的也是同一套 route。 Hexo Filter API · Hexo Router API

从路由表收齐待移动的资源后,写入和移除发生在这个循环里。这样遍历源路由时,列表里的条目仍然可读。

// sheng-blog/hexo-runtime/site/publish-files-by-url-title.ts,after_generate 中的写入循环
for (const move of moves) {
  this.route.set(move.targetPath, {
    data: move.data,
    modified: move.modified,
  });
  this.route.remove(move.sourcePath);
}
检查位置 这张图对应的值
Markdown 链接目标 ../../files/2026-09-21 - 从 MacBook 的 AV1 开始:一部视频编码编年史/images/04-encoder-pipeline.fonts.6186cd6a0f42.svg
Markdown 渲染后引用的公开资源 /files/video-codec-history-from-macbook-av1/images/04-encoder-pipeline.fonts.6186cd6a0f42.svg
默认静态输出目录里的文件 public/files/video-codec-history-from-macbook-av1/images/04-encoder-pipeline.fonts.6186cd6a0f42.svg

如果只改引用,浏览器会请求表里的公开地址,但文件还挂在中文目录下。如果只搬资源,浏览器则仍从文章 URL 去解析本地相对路径。排查图片 404 时,我会分别看生成 HTML 的 src 或 href,以及输出目录里是否有相同路径的文件。它们对上以后,再实际打开 Markdown 预览和网页。

Sheng 主题还会给部分 SVG 生成适配浅色、深色背景的展示版本。检查这类图时,src 可能指向主题生成的版本,data-original-src 则记录资源映射后的原图地址。两条地址都应该能请求到各自的文件。

公开地址稳定到什么程度

本地文件和同名资源目录一起改名时,只要所属文章的 url_title 和资源子路径保持原值,公开下载地址就不变。合并文章时,如果资源换了主人,公开前缀也会跟着变。需要继续提供旧下载地址时,本站可以在新所属文章声明 asset_aliases,生成时给旧前缀写入相同字节。两条公开路径都从一份维护源生成。

这套映射用文章文件基本名找目录,用 url_title 找公开前缀,所以同名目录和重复的 url_title 都可能让两个资源争用一个 route。地址稳定也和 HTTP 缓存是两件事。同一 URL 下替换了文件内容,客户端是否取到新内容仍取决于缓存策略。 静态站点的缓存配置

验证引用与资源一起生效

先用 Markdown 预览确认相对路径能找到实际文件。再用 pnpm exec hexo generate 检查渲染后的地址、public/files/<url_title>/ 下的输出文件,以及含中文目录名的公开路由是否已移除。连载篇的历史补丁快照也用普通 Markdown 链接走同一条映射,可以和图片一起验证。

本站源码包中的资源映射脚本可以在源码工作区弹窗查看。它目前以 hexo-renderer-marked 和本站资源目录为前提。若独立分发成插件,还需要在默认主题中验证子路径部署、不同 Markdown 路径深度和增量生成。 Hexo 插件文档

总结

正文用文件系统能读懂的相对路径,浏览器用公开 URL。构建时把页面引用和实际文件映射到同一条公开路由,才能让本地预览、网页配图与下载都指向同一份资源。

资源路由和相对图片转换

打开原文件 FILES.json

资源路由和相对图片转换正在加载代码工作区...