This Blog Vendors Its Own Static Site Generator Now

womack.io runs on Hexo and a theme called apollo, both from around 2019. package-lock.json was gitignored this whole time, so every fresh install re-resolved every hexo-* plugin’s ^ range against whatever npm currently had. That’s how this exact site went down before: some plugin shipped a change apollo never agreed to support, and there was nothing pinning the tree to stop it.

The theme is written against Hexo 4.x’s generator and renderer APIs specifically. Upstream Hexo has moved past that, so floating a ^4.2.1 dependency was never going to be safe long-term, there was no version of “just upgrade” that didn’t mean rewriting the theme. So I vendored it instead: Hexo 4.2.1 lives at packages/hexo now, published locally as @jameswomack/hexo, installed via "hexo": "file:packages/hexo" so it lands in node_modules/hexo right where hexo-cli expects it. Every hexo-* plugin version is pinned exact, no carets, and package-lock.json is finally tracked. The tree can’t drift out from under the theme again because nothing in it is allowed to move on its own anymore.

Before trusting it I ran hexo generate under Node 20, 22, and 24 and diffed the output. Byte-identical, except sitemap.xml‘s entry order, which turned out to be non-deterministic tie-breaking on same-timestamp posts, not a real difference. .nvmrc moved to 24.7.0 for Vercel the same day.

womack.io, running on the now-vendored Hexo fork

Fifteen minutes after that landed, npm audit flagged send under 0.19.0 for a template injection XSS, GHSA-m6fv-jmcg-4jfg, reachable through hexo-server and hexo-browsersync, both of which are dev-server-only and both of which haven’t been touched upstream in years. No fixed version existed inside their declared ranges. Forced send to 0.19.2 and serve-static to 1.16.3 via package.json overrides. Tried jumping serve-static straight to 2.x first, that broke hexo-browsersync‘s plugin loading outright, its wrapper assumes the 1.x API, so back to the patched 1.x line instead.

Fitting, in a way, to be doing this kind of plumbing work on the same blog that’s currently telling you about it. Still finding these, four commits later it was a footer “Next »” link rendering as literal escaped text because of the same paginator helper’s default escaping. Old software has old corners.