✨ Highlights
This release closes the loop on three issues reported against 0.8.10–0.8.12: the #175 GH Pages asset path regression, the #177 offline-mode <base> break, and the #167 offline link rewrite. The root cause of all three was that the <base> tag decision was scattered across templates — 0.8.13 makes the generator the single source of truth.
As a bonus, the auto-install retry path no longer fails on Linux CI for @docmd/template-summer, docmd-search, and other on-demand deps, because the post-install load now uses a manual node_modules walk-up instead of relying on Node’s stale require.resolve cache.
No breaking changes. No new required config. Existing configs, plugins, and CI pipelines work unchanged.
🐛 Fix: <base> tag decision centralised in the generator (#175, #167, #177)
0.8.10 added an unconditional <base href="/"> to the layout template. That fixed a workspace asset issue but broke three other things at once:
#175(GH Pages subpath): when a user seturl: "https://user.github.io/repo", the<base>should have been"/repo/"not"/". 0.8.10 was fixed for subpath, but the root-deploy case still emitted the unnecessary<base href="/">.#167(offline link rewrite): the markdown post-processor correctly rewrote/destination/to./destination/index.htmlin offline mode, but the<base href="/">then told the browser to re-root those relative paths against the filesystem root overfile://. The links “worked” in the HTML source but 404’d in the browser.#177(offline CSS/assets): openingsite/index.htmlfrom disk produced unstyled pages for the same reason — the<base>re-rooted./assets/css/...to filesystem root, which 404s.
The fix (0.8.13): the generator now computes the canonical <base> decision based on (isOfflineMode, siteRootAbs) and applies it after template rendering. Templates no longer emit <base> themselves. The decision table:
| Scenario | <base> emitted |
Why |
|---|---|---|
build (online, root deploy) |
no | Relative URLs resolve against the document URL, which is what we want |
build (online, GH Pages subpath) |
<base href="/repo/"> |
Browser needs to know it’s at a subpath |
build --offline |
no | file:// resolution must use document-relative paths |
file:// (direct disk view) |
n/a | Search client shows the helpful offline message instead |
No template edits required for existing users — the build output is now correct by default. The change is invisible unless you were relying on a template emitting its own <base> (which no official template did).
🔧 Auto-install retry now survives Node’s stale resolve cache
The auto-install path (tryLoadAfterInstall) used a bare import(packageName) for the post-install load. On Linux CI, Node’s internal require.resolve cache returned a stale “not found” even after npm install had placed the package in node_modules. The result: build would install @docmd/template-summer and docmd-search successfully, then fail with Could not load @docmd/template-summer after auto-install (this was the CI failure that broke GH Pages deploys in 0.8.12).
The fix: a new manualResolvePackageEntry() walks the node_modules directory tree with fresh fs.existsSync() checks, completely bypassing Node’s resolve cache. The new flow is:
- Try
createRequire(consumerCwd).resolve(name)(fast, honoursexportsfield). - If that fails, walk up from
consumerCwdlooking fornode_modules/<name>/package.jsondirectly (reliable, cache-free).
As a bonus, a new DOCMD_INSTALL_VERSION environment variable overrides the version used in npm install <name>@<version>, so users can pin to a specific release or use latest regardless of what’s installed locally.
🐛 Fix: search client shows a helpful message on file://
The keyword search and semantic search both use fetch() to load index files. Browsers block fetch() from file:// URLs (CORS), so opening site/index.html from disk showed Failed to load search index. — a misleading error that suggested something was broken.
0.8.13 detects window.location.protocol === 'file:' before any fetch and shows a clear message instead:
Search requires a web server. Open this site via http://localhost instead of file:// to enable search.
This makes the limitation explicit instead of leaving the user to wonder why their search “doesn’t work” offline. A true offline index is a separate feature (track separately if you want it).
🧹 Test and lint cleanup
eslint.config.mjsnow ignores_-prefixed variables/arguments/caught errors (standard convention)tools/prep.jstest runner streams test output live instead of capturing silentlytools/simulate-consumer.mjsempty catch block has an explanatory commentpackages/plugins/openapi/removed a deadconst r = ...binding and a stale eslint-disable directive- Removed dead
summariseTests()/extractFailures()helpers fromprep.js(replaced by streaming)
Migration
Zero. Existing configs, plugins, and CI pipelines work unchanged. The <base> change is fully backwards-compatible for any site whose url config matches the actual deployment URL.
Full changelog: https://github.com/docmd-io/docmd/compare/0.8.12...0.8.13