✨ 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 set url: "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.html in offline mode, but the <base href="/"> then told the browser to re-root those relative paths against the filesystem root over file://. The links “worked” in the HTML source but 404’d in the browser.
  • #177 (offline CSS/assets): opening site/index.html from 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:

  1. Try createRequire(consumerCwd).resolve(name) (fast, honours exports field).
  2. If that fails, walk up from consumerCwd looking for node_modules/<name>/package.json directly (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.mjs now ignores _-prefixed variables/arguments/caught errors (standard convention)
  • tools/prep.js test runner streams test output live instead of capturing silently
  • tools/simulate-consumer.mjs empty catch block has an explanatory comment
  • packages/plugins/openapi/ removed a dead const r = ... binding and a stale eslint-disable directive
  • Removed dead summariseTests() / extractFailures() helpers from prep.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