一次 Hexo 文章目录结构探索:为什么最后放弃了 index.md

这次本来想把博客的文章目录结构彻底整理一下。

起因很简单:有一篇文章需要附带两个可公开访问的脚本。如果脚本继续放在单独的 source/files 里,文章和资源会分开;如果脚本放在 _posts 的同名目录里,目录又会慢慢变多。既然迟早要给博客定一个长期规范,不如趁这个机会把文章 URL、文章源码和相关资源一起想清楚。

最理想的样子大概是这样:

source/_posts/webstorm-eap-plugin-compat/
  index.md
  scripts/
    force-webstorm-plugin-compat.sh
    force-webstorm-plugin-compat.ps1
  images/
    screenshot.png

这类结构很像很多文档站的「文章包」:根目录只放正文,资源按类型放进子目录。打开目录时,index.md 就是入口;资源一多,也不会跟正文混在一起。

想法很顺,但落到 Hexo 上,就没那么自然了。

第一个目标:URL 不带日期

一开始还想顺手把文章 URL 改得更干净。

旧 URL 是这样:

https://shengsheng.fun/2026/05/29/webstorm-eap-plugin-compat/

如果改成:

permalink: :title/

就可以变成:

https://shengsheng.fun/webstorm-eap-plugin-compat/

这当然更短,也更像一个长期文档页。但它会带来一个现实问题:已有外链都要迁移。

个人博客可以大胆试,但 URL 一旦发出去,就不只是本地文件名了。它可能已经贴在聊天、文档、Issue 或收藏夹里。哪怕只有几处,也要么逐个改,要么补重定向。

URL 可以改,只是它和「文章目录结构」其实是两个问题。

URL 是否带日期,解决的是公开链接形态。
文章和资源怎么放,解决的是源码维护形态。

这两个问题不一定要绑在一起改。

第二个目标:文章和资源放一起

Hexo 有一个原生配置叫 post_asset_folder

它支持这种结构:

source/_posts/foo.md
source/_posts/foo/image.png
source/_posts/foo/tool.sh

文章是 foo.md,资源目录是 foo/。资源会作为这篇文章的 PostAsset 跟着生成,最终可以挂在文章 URL 下面。

这个方案的好处是:它是 Hexo 原生规则,不需要自己写额外脚本。

但它也有一个不舒服的地方:source/_posts 下面会同时出现文章文件和资源目录。

文章一多,目录会变成这样:

source/_posts/
  foo.md
  foo/
  bar.md
  bar/
  baz.md
  baz/

如果每篇文章都有资源,_posts 的根目录就会变得很吵。它不再只是文章列表,而是文章和资源包混在一起。

所以又看向了 index.md

第三个目标:用 index.md 做文章入口

如果能写成这样,目录会更像一个独立文章包:

source/_posts/foo/
  index.md
  scripts/tool.sh
  images/a.png

这里有两个问题要验证。

第一个问题是 slug。

默认情况下,Hexo 的 new_post_name 是:

new_post_name: :title.md

如果直接把文章写成 source/_posts/foo/index.md,Hexo 可能会把它理解成 foo/index,生成出来的 URL 也会多一段 /index/

这个可以通过配置绕过去:

new_post_name: :title/index.md

这样 foo/index.md 仍然可以解析出 foo 这个 title,URL 也能维持成 /foo//2026/05/29/foo/

卡住的是第二个问题:资源目录。

Hexo 的 PostAsset 目录不是按「文章所在目录」算的,而是按「文章源文件去掉扩展名」算的。

也就是说:

source/_posts/foo.md

对应的资源目录是:

source/_posts/foo/

而:

source/_posts/foo/index.md

对应的资源目录会变成:

source/_posts/foo/index/

所以这个结构并不是 Hexo 原生认可的文章资产目录:

source/_posts/foo/
  index.md
  scripts/tool.sh
  images/a.png

原生能走通的反而是这样:

source/_posts/foo/
  index.md
  index/
    scripts/tool.sh
    images/a.png

最终 URL 可以还是:

/foo/scripts/tool.sh
/foo/images/a.png

但源码里多了一层 index/。这层目录不是为了人的理解存在的,而是为了适配 Hexo 的资产计算规则存在的。

这就有点别扭了。

为什么不写一个 Hexo Script

当然,也可以写一个 Hexo Script。

比如在生成时扫描:

source/_posts/foo/
  index.md
  scripts/
  images/

然后把同级 scripts/images/ 映射到最终文章 URL 下面。

但不太喜欢这个方案。

原因不是它写不出来,而是它会引入一层隐藏逻辑。

源码看起来像普通 Hexo 文章,但实际资源发布规则依赖一个项目自定义脚本。以后本地预览、主题升级、Hexo 升级、别的 Agent 接手、甚至临时换一套生成命令时,都要记得这个脚本存在。

如果只是为了让目录看起来更整齐,就让「源文件放哪里」和「最终 URL 怎么出来」之间多一层自定义映射,并不划算。

项目约定最好是能被文件结构直接说明的。
看路径,就知道它会怎么生成。
不需要先知道某个隐藏脚本。

最后为什么回滚

试下来以后,其实选择只剩下几条。

第一条,继续用 foo.md + foo/

这是 Hexo 原生文章资产方式,最直接。但 _posts 会同时堆文章和资源目录。

第二条,用 foo/index.md + foo/index/

这也是原生能跑通的方式,但源码结构很反直觉。index.md 旁边的资源不在旁边,而在 index/ 下面。

第三条,用 foo/index.md + foo/scripts/

这是最想要的源码结构,但它需要自定义 Hexo Script 才能稳定发布。

第四条,把资源放回 source/files

它不优雅,但很直白。

source/_posts/webstorm-eap-plugin-compat.md
source/files/webstorm-eap-plugin-compat/force-webstorm-plugin-compat.sh
source/files/webstorm-eap-plugin-compat/force-webstorm-plugin-compat.ps1

文章继续是普通文章。资源继续是普通静态文件。两边用同一个 slug 关联。

公开 URL 也很明确:

/2026/05/29/webstorm-eap-plugin-compat/
/files/webstorm-eap-plugin-compat/force-webstorm-plugin-compat.sh
/files/webstorm-eap-plugin-compat/force-webstorm-plugin-compat.ps1

最后选择第四条。

这不是最漂亮的目录结构,但它最少魔法。对一个个人博客来说,这比目录洁癖更重要。

这次留下的规则

这次探索之后,给这个博客留下了几条规则。

正式文章继续放在:

source/_posts/<slug>.md

公开资源放在:

source/files/<slug>/

如果资源很多,再按需要拆子目录:

source/files/<slug>/images/
source/files/<slug>/scripts/
source/files/<slug>/data/

文章 URL 继续保留日期:

permalink: :year/:month/:day/:title/

这能减少旧链接迁移,也符合这个博客过去的 URL 形态。

暂时不启用 post_asset_folder 作为默认资源组织方式,也不把 index.md 作为默认文章形态。

如果未来真的要重新设计博客结构,也应该先明确接受哪种代价:是接受 _posts 里文章和资源目录混排,还是接受源码里多一层 index/,还是接受自定义 Hexo Script。

在没有这个必要之前,先回到最朴素、最容易解释的方案。

绕一圈也有用

这次看起来像是绕了一圈又回去了。

但这圈不是白绕。

一开始的直觉是:「把文章和资源放进一个目录,应该更容易维护。」

验证之后才发现,对 Hexo 这个具体系统来说,这个直觉只成立一半。目录看起来更像一个文章包,不代表生成模型也会天然配合。

如果为了追求源码目录的漂亮,引入自定义发布逻辑,后面维护者反而要记更多规则。

这次留下来的判断是:

不要只问目录看起来整不整齐。
还要问它是不是顺着工具本身的模型走。

顺着模型,哪怕目录朴素一点,也更容易长期维护。