✨ Highlights

Three coordinated fixes that close the follow-ups to #175 reported on v0.8.15. No breaking changes, no new config keys, no required migrations. Existing projects build and serve unchanged.

🐛 Fix: Dev server respects the production URL

v0.8.15 introduced auto-derivation of base from url’s pathname, so a GitHub Pages project site at https://user.github.io/repo automatically gets base = /repo/ and emits subpath-aware asset URLs. The dev server, however, always serves the output dir at root (/) — so every asset on localhost 404’d whenever url carried a subpath.

The reported workaround was "base": "/", which made dev work but disabled the auto-derivation, breaking search on the production deploy. Both states were broken for one environment.

The fix: dev mode now forces base = / when the value was auto-derived. The override is restricted to:

  • isDev: true (build path is untouched)
  • _baseAutoDerived is set (an explicit base in the config is always honoured)
  • DOCMD_PROJECT_PREFIX env var is absent (workspace dev uses it)

Users no longer need a base entry in their config at all.

Keyword and semantic search now share a single output folder:

  • Before: search-index.json lived at the output root, semantic files under .docmd-search/.
  • After: both live under .docmd-search/. Default locale: .docmd-search/search-index.json. Non-default: .docmd-search/<locale>/search-index.json.

This keeps the output root clean and unifies the contract the client fetches against, so the next fix could land cleanly.

🐛 Fix: Removed the runtime manifest.json HEAD probe

The search client probed .docmd-search/manifest.json with a HEAD request on every page load to catch the narrow case where semantic deps were installed mid-build. On every keyword-only site that probe returned 404, which was the exact error users were reporting.

The fix: semantic mode is decided solely by the build-time data-semantic flag on the search modal. The flag is computed in onConfigResolved from semanticUsable, so the build and the client agree on which mode is active. The rare mid-build edge case resolves on the next rebuild (dev mode triggers one automatically).

🐛 Fix: Keyword index always generates as fallback

Once the runtime probe was removed, a second race surfaced: when semantic indexing succeeded, the keyword search-index.json was skipped entirely. If the modal’s data-semantic flag was stale (set to false at render time because deps were not yet installed), the client fell back to keyword mode and 404’d because no keyword index existed.

The fix: the keyword index now always generates, even after a successful semantic build. The index is tiny (a few KB for most projects) and serves as a runtime safety net. TUI messages are suppressed when semantic already ran, so the log stays clean.

🐛 Fix: data-semantic flag stamped post-build

The data-semantic="true" attribute on the search modal is computed at render time from semanticUsable, which checks whether docmd-search is resolvable. On a fresh install, deps are installed in onPostBuild, after pages have already been rendered without the flag. The client then defaults to keyword mode even though the semantic index exists.

The fix: after the semantic index builds successfully, every HTML page is post-processed to ensure the modal carries data-semantic="true". This closes the race without reintroducing the runtime HEAD probe. The post-build walk skips .docmd-search/ and node_modules/ to stay fast on large sites.

🧰 Auto-install no longer pre-blocks workspace sub-projects

The pre-check that scanned for package.json in cwd before spawning the package manager has been removed. npm, pnpm, yarn, and bun all walk up the directory tree themselves to find the project root, so the pre-check was wrong in two cases it claimed to handle:

  1. Workspace sub-projects (e.g. docs/docmd-search/ when docs/package.json lives one level up).
  2. Any case where a plugin or template needs to install and the project root is several dirs deep.

The fix: the spawn is always attempted. The friendly “no package.json found” hint now fires only when the spawn genuinely fails and no package.json exists in any ancestor directory. Templates are covered by the same path as plugins.

Verified end-to-end with a fresh brutetest using @docmd/template-summer and plugins.search.semantic: true: both auto-install silently on the first build, summer CSS/JS land at site/assets/template/, and the semantic index writes to site/.docmd-search/.

🏗 Portable deploys via canonical <base href>

Non-offline builds with a subpath (siteRootAbs !== '/') now emit a single <base href="/<repo>/"> tag in the document head. Asset URLs in the head use simple-relative paths (assets/foo.css); the <base> tag shifts them to the correct location at runtime, no matter where the site is hosted.

Why this matters: the previous approach encoded the subpath into every asset URL. The rename-flow went like this — you rename the repo, the next build keeps emitting /old-name/assets/... until you update config.url and rebuild. With <base>, the path is computed once from the deploy location and applied uniformly. Moving a site between repos, hosts, or folder mounts no longer requires touching config.url if url is already correct for the current deploy.

Three-mode boundary to keep this clean:

Mode <base> Assets
build (subpath) emitted simple-relative 'assets/foo.css'
build (root deploy) none (document default) simple-relative 'assets/foo.css'
dev none depth-aware ./ or ../
build --offline none (file:// has no host) depth-aware ./ or ../

The canonicaliser (normaliseBaseTag in packages/parser/src/utils/url-utils.ts) strips any pre-existing <base> tag emitted by older templates or third-party plugins, then injects exactly one at the canonical position right after </title>. The deploy shape is decided in one place; templates don’t need to know about it.

📦 Upgrade

upgrade
npx @docmd/core@0.8.16 build

No config changes required. If you previously set "base": "/" as a workaround for v0.8.15, you can remove it — the dev server now adapts automatically.