Release 0.8.16 — Robust asset resolution, portable base href, Windows fix, simulator overhaul - #183
Merged
Conversation
When optionsMenu.position === 'header' the layout now includes the options menu (search trigger, theme switch, sponsor, language, project switcher) inside the pageheader alongside the copy widgets. Adds .summer-options-menu-slot styles and child overrides for the default options-menu partial (no docmd-main.css dependency). At <= 600px the kbd hint in the search trigger is hidden so the pageheader row stays single-line.
The topbar already has individual, config-gated elements for theme / language / sponsor / github / version / project switcher. Adding a unified options-menu partial on top was duplicating those. Removed: - partials/options-menu include in the topbar - .summer-topbar__options CSS block Existing gates stay as-is: - _om.components.themeSwitch - _om.components.sponsor / github - config.versions.position - workspace.switcher.position - config.i18n.position
Three bugs that broke core interactions on first load / SPA nav:
1. summerInit() had an inverted condition:
if (dataset.summerWired === '1') { /* first run */ }
so the body never executed — the dataset was always unset,
the body never ran, and the flag was never set either.
Result: search, theme toggle, scroll-to-top, banner close,
sidebar mobile drawer, subnav dropdowns, and version/project/
language switchers all silently no-op'd. Flip to !== '1'.
2. wireSidebarGroups toggle clobbered by docmd-main.js:
both handlers listen for clicks on the same .nav-label /
.collapse-icon-wrapper, and use different sources of truth
(aria-expanded vs classList.contains('expanded')) — when
both fire, the second one reverts the first. Stop propagation
so only the summer-side handler runs.
3. wireSwitcherDropdowns used stopPropagation, but the
document-level click handler attached later (in docmd-main.js
for .version-dropdown-toggle) would still see the bubble. Bump
to stopImmediatePropagation on the button-level listener so it
wins regardless of handler attachment order.
… slot
Footer
- footer__top was using 'grid-template-columns: 1.5fr repeat(auto-fit,
minmax(160px, 1fr))' which Chromium was parsing the cssText without
(cssRule style had everything except grid-template-columns). Switched
to an explicit 1.5fr 1fr 1fr 1fr track list with responsive overrides
at 900/540px — the rule now applies as written and the 3-col brand +
Resources/Ecosystem/Community row lays out correctly.
- Sponsored link in the footer was hardcoded. Make it gated on a new
'optionsMenu.components.sponsorPosition' key:
'top' (default, only topbar) | 'footer' (only footer) | 'both'
The hardcoded inline-svg version is gone, replaced with the same
_sponsorUrl / _sponsorPos pair the topbar uses so a config flip moves
the link, not duplicates it.
Code blocks
- .summer-codeblock / .summer-codeblock__header / .summer-codeblock__copy /
.summer-codeblock pre / .summer-content pre were all locked to a hard
dark palette (#1d1b17 / #e7dfcf / #c8bfae / #8a8170) regardless of
theme, so light mode rendered the same dark slab. Re-keyed to the
summer surface / fg / muted / border tokens; added a sibling
html[data-theme='dark'] override that restores the original dark slab
for dark mode. Light + dark verified via getComputedStyle snapshots.
Page nav
- .summer-pagenav is a CSS grid. The no-prev / no-next edge-push rules
used margin-left/right: auto, which the grid layout ignores — the
lone link floated in the first column. Switched to
grid-column: <n> + justify-self: end/start + max-width: 380px, and
added a reset inside the <=900px media query (where the grid goes
1fr) so the single-link case doesn't leave a phantom empty cell.
Sidebar / TOC sticky offset
- Trimmed the +16px / -32px breathers on the .summer-sidebar and
.summer-toc sticky top / max-height to +12px / -24px so they sit a
touch closer to the pageheader's bottom hairline, while keeping the
same 4-band top: calc(topbar + subnav + banner + pageheader) formula
so they auto-adjust to the menubar / banner / header CSS variables.
Topbar switcher pills
- The version / project / language switcher toggles in the topbar
were 32px tall with rectangular corners. Trimmed to 28px tall and
pill (border-radius: 999px) so the three switchers + theme + sponsor
+ github don't crowd the right side of the topbar.
The user feedback was that docmd-main.js already implements the
working SPA router, sidebar collapse, theme toggle, version/project/
language switchers, code-block copy, page copy and banner close —
so duplicating them in summer.js was both dead code and source of
conflicts (e.g. the summer toggle + docmd-main toggle would race on
the same data-theme attribute).
This refactor:
- removes ~370 lines of duplicated handlers from summer.js
(theme, sidebar drawer, sidebar collapse, subnav dropdowns,
switcher dropdowns, code-block copy, page copy, banner close,
and their unused helpers isMac / copyToClipboard / showCopiedFeedback)
- keeps only summer-specific wires: topbar search dropdown, TOC
scroll-spy + smooth scroll, scroll-to-top button, git popover,
relative-date rendering
- re-uses the same wireHeaderSearch() that was in 3825372 (the
proven working implementation) and calls it once in the first-run
block. summerInit then listens for docmd:page-mounted and re-runs
the per-page wires (TOC, git popover, relative timestamps) so
they re-attach after every docmd-main.js SPA nav
Layout / EJS:
- body now carries data-spa-enabled, data-copy-code-enabled and
data-default-collapsed so docmd-main.js's bootstrap() sees a
real config and SPA-init, code-injection and collapse-state
default all match the source config
- topbar mobile-toggle button also carries .sidebar-menu-button
so docmd-main.js's mobile-drawer outside-click + Escape
behaviour just works
- theme button also carries .theme-toggle-button so docmd-main.js's
bootstrap() wires it
- banner wrapper now has data-docmd-banner and the close button
has data-docmd-banner-dismiss so docmd-main.js's initBanner()
(sessionStorage-persisted) owns the dismiss state
CSS:
- .summer-sidebar nav-group click / hover affordances (cursor:
pointer, background, accent icon) are now scoped to
.nav-group.collapsible only. Non-collapsible groups (e.g.
'Getting Started' with "collapsible": false in navigation.json)
no longer look clickable — the visual state now matches the
behaviour handled by docmd-main.js's nav handler
- nav-group header padding reduced from 8px 12px to 8px 14px so
a first-child group doesn't sit visually too close to the top
edge after the margin-top rule was removed
Codeblock copy button - Layout was missing the summer.js script tag; the bottom-floating copy button from docmd-main.js was rendering inline at the end of the <pre> with no styles (since summer.css is self-contained and doesn't load docmd-main.css). Wire summer.js into layout.ejs so its codeblock handler runs. - New summerCodeblocks() in summer.js: re-homes the copy button into a thin title bar at the top of the codeblock. Uses the existing .docmd-code-block-header (parser-rendered when the source has ```lang "title.js"```) and falls back to building a .summer-cb__titlebar (filename = language, snippet if none) for un-titled blocks. - Replaced the dead .summer-codeblock* CSS (only used by the old summer.js attachCodeCopyButtons path) with .summer-cb__* rules. Also styles .code-wrapper, .docmd-code-block-wrapper, and .copy-code-button to look the same so the result is consistent regardless of which path produced the DOM. Footer - Modernised: brand column now stacks logo + description with a breathing gap, columns are tighter, link lists have hover indicators, external links get a subtle ↗ on hover, and the complete-footer's bottom bar gets a hairline divider for visual separation from the columns. - Adjusted padding / gap scale and responsive breakpoints. Sidebar / TOC - Aligned the sticky offset to the bottom of the pageheader band (was 12px below, leaving the sidebar's first item visually floating above the first main content item). - Added --summer-main-padding-top so the sticky calc accounts for the breathing room between topbar and pageheader that lives on .summer-main. Sidebar, TOC and main now share the same Y anchor for their first visible item.
…mers Previously the highlighter consumed the language for token colouring but dropped it from the markup. The summer template's codeblock title bar (summer.js) reads `code.className.match(/language-(...))/` to show the filename / language pill on the left of the title bar — without this class it always fell back to "snippet". Add a `class="language-XXX"` attribute on the rendered <code> element whenever a language is provided. Mirrors the same change in the no-highlight fallback.
… fix
ui (docmd-main.js)
- Add summer-template selectors to the SPA swap list so the SPA
router actually swaps content when the user navigates. Without
these, docmd-main.js was changing the URL but the visible page
stayed the same (no swap targets matched the active template).
- New swap targets: .summer-content, .summer-toc, .summer-pageheader,
.summer-pagenav, .summer-pagefooter, #docmd-raw-markdown,
.summer-sidebar nav. All summer-only — they're a no-op for the
classic template.
summer (layout.ejs)
- Remove the duplicate summer.js <script> tag I added in the
previous round. The template's src/index.ts already registers
summer.js as a templateAsset which the core build emits at
assets/template/summer.js — the extra tag at assets/js/summer.js
was 404-ing.
summer (summer.js)
- Fix syntax error from the previous refactor (orphan
wrap.classList.add line that broke the IIFE).
- Refactor the codeblock title bar to the new spec:
- Drop the standalone .summer-cb__filename span (it duplicated
the lang pill when there was no markdown title).
- .summer-cb__lang becomes the primary label. Shows the
detected language (e.g. "BASH") or "CODEBLOCK" when no
language is set.
- For parser-titled code blocks (), keep
the filename from .docmd-code-block-title and place the lang
pill after it.
- Extract the shared SVG icon into makeFileIcon() so both
paths use the same markup.
- The dedicated .summer-cb__copy-label span is preserved (used
by CSS for the gap between the copy icon and the "Copy"
text).
summer (summer.css)
- Codeblock filename (.summer-cb__filename, .docmd-code-block-title)
demoted to a subtle text style (regular weight, slightly
smaller, --summer-fg-soft) so the lang pill remains the primary
visual element.
- Lang pill: tighter line-height, slightly larger horizontal
padding for the new role as the main label.
- Sidebar / TOC sticky offset: drop --summer-main-padding-top
from the calc. The 4-band sum (topbar + subnav + banner +
pageheader) is the only formula needed — the main's top padding
was being added on top of the pageheader band, which gave the
sidebar a visible menubar margin even when the menubar was
disabled. The pageheader's natural Y in the layout still
positions the sidebar correctly via the pageheader-height
variable.
ui (docmd-main.js)
- The copy-code click handler looked for the associated <code> in
copyBtn.closest('.code-wrapper'). But the summer template re-homes
the copy button into the title bar, which for
blocks lives in .docmd-code-block-wrapper — *outside* the
.code-wrapper. closest('.code-wrapper') returned null and the
click silently did nothing.
- Broaden the lookup: try .code-wrapper first, then fall back to
any codeblock-shaped parent (.docmd-code-block-wrapper, .summer-cb,
.summer-codeblock) and grab the first pre code inside. Works for
both the classic and summer templates, with or without a parser-
rendered title.
summer (summer.css)
- For parser-titled code blocks, the inner .code-wrapper (added by
docmd-main.js to host the copy button) inherits the same
background / border / border-radius as the outer
.docmd-code-block-wrapper — rendering as a "window inside a
window". Strip those styles on the inner wrapper when it sits
inside a parser wrapper so only the outer box is visible.
The .github/workflows/docker-publish.yml workflow never applied the `latest` tag because its `enable` expression required a `push` event on main, but the workflow had no `push` trigger — only `release` and `workflow_dispatch`. As a result, the published image had only the versioned tag (`0.8.6`), an `edge` tag, and commit SHAs, with no `latest` alias even though the README, DOCKER.md, and docs all pointed at it. Workflow changes: - Tag `:latest` automatically on `release` events (the standard Docker convention — only stable releases get the floating tag). - Re-add a `push: branches: [main]` trigger so `:edge` rebuilds on pushes to main. - Expose a manual `tag_latest` boolean input to `workflow_dispatch` for back-filling `:latest` onto an existing image. Doc changes: - Pin every `ghcr.io/docmd-io/docmd:latest` reference to `ghcr.io/docmd-io/docmd:0.8.6` (the latest stable tag) across: - 6 READMEs (en/de/es/fr/ja/zh) - packages/core/README.md - docker/DOCKER.md - docs/.../installation.md (en/de/zh) - docs/.../deployment/docker.md (en/de/zh) - docs/.../0-8-6.md (en/de/zh) - Rewrite DOCKER.md `Available Tags` table to match the registry reality and recommend pinning for production / CI. - Audit entry added to bug-report.md (High Priority, in progress). Workaround for the original bug reporter: pull a specific version such as `ghcr.io/docmd-io/docmd:0.8.6` until the next release publishes the `latest` tag.
`docmd add summer` now works as a first-class template install.
Unlike regular plugins, templates register to `theme.template` (not
`plugins.*`) and **replace** any previously-set template — there is
no stacking.
Installer changes (@docmd/plugin-installer):
- New `summer` registry entry with `kind: "template"`.
- New `injectTemplateToConfig()` / `removeTemplateFromConfig()`
helpers that write `theme.template` in the config and replace any
existing value.
- Format-aware: JSON configs use `JSON.parse` + `stringify`;
JS configs use regex (matches the existing plugin injector style).
- New `resolveConfigPath()` prefers `docmd.config.json` (the
project standard), falls back to `.js` for legacy projects.
- `installPlugin()` and `removePlugin()` branch on `meta.kind`
to dispatch plugin vs. template paths.
- All 5 JS edge cases (empty theme, theme w/ keys, no theme, replace,
re-add no-op) format cleanly. JSON edge cases (add, re-add same,
re-add diff, remove, re-remove no-op) pass.
Summer template (@docmd/template-summer):
- New dedicated `packages/templates/summer/templates/404.ejs` —
standalone HTML, Summer-styled (halo gradient + glass card + Inter
font), i18n-aware (`t('pageNotFound')` / `t('returnHome')` /
`t('goBack')` with English fallbacks), RTL-aware via
`activeLocale.dir`. Loads `summer.css` + theme colour overlay +
`customCss`.
- Registered in `src/index.ts` as `{ type: '404', templatePath: ... }`.
- Both packages build cleanly via `tsc`.
…malink
Default template polish (Section C of the 0.8.7 backlog):
1. **Sticky bottom for page-nav and page-footer-actions**
- `.content-area` is now a flex column.
- `.content-layout` gets `flex: 1 0 auto` so it grows to fill
`.content-area`.
- `.page-footer-actions` is pushed to the bottom of the content
area (just above the page footer) when content is short.
- `.main-content` is now a flex column.
- `.page-navigation` (prev/next buttons) gets
`margin-top: auto` so it sticks to the bottom of the article
instead of floating in the middle on short pages.
2. **Thinner header & menubar**
- `--menubar-h: 52px → 44px` (both light & dark themes).
- `--header-h: 54px → 44px` (both light & dark themes).
- `.page-header` padding: `var(--space-3)` →
`var(--space-2) var(--space-3)` (8px top/bottom, 12px sides).
- Less vertical chrome, more content real estate.
3. **Smaller permalink icon**
- `.heading-anchor svg` width/height: `1.1em → 0.85em`.
- Subtler by default; still reveals on heading hover.
Verified: `@docmd/ui` builds cleanly via `tsc`.
…etch, default config
Four small bugs / QoL fixes from Sections C and D of the 0.8.7 backlog:
1. **i18n: partials never received t()** — the layout's menubar,
options-menu, version-dropdown, language-switcher, project-switcher,
and toc partials all called `t('…')` but were never passed the
`t` function (only banner/footer/cookie-consent were). Strings
like `builtWith`, `next`/`previous`, `selectVersion`,
`selectLanguage`, `selectProject`, and `onThisPage` silently
fell back to English even on non-English locales. Added
`t: typeof t === 'function' ? t : null` to every `include()`
call in `layout.ejs` (11 partials total).
2. **i18n: 13 missing translation keys** — added to all 7 locale
files (en/de/es/fr/hi/ja/zh): `close`, `cookieAccept`,
`cookieConsent`, `cookieDecline`, `cookieMessage`,
`cookiePolicy`, `goBack`, `lastUpdated`, `pageNotFoundMsg`,
`readMore`, `recentCommits`, `searchPlaceholder`,
`selectProject`. Translations provided for each language.
3. **Copy code button floating above codeblock** — the button was
anchored to an inner `.code-wrapper` created by
`injectCopyButtons` in `docmd-main.js`, which only wrapped the
`<pre>`, ignoring the `.docmd-code-block-header` above. Now:
- `.docmd-code-block-wrapper` is `position: relative` in CSS.
- `injectCopyButtons` anchors the button to the outer wrapper
when one exists, skipping the redundant inner wrapper.
Result: button now sits at the top-right of the whole code block
(next to or just below the title bar), not floating above it.
4. **Summer template: huge gap below short content** —
`.summer-content` had `flex: 1 0 auto` which stretched the
article to fill the grid cell. On short pages, that left a giant
empty area between the article and the footer. Removed the
grow; content now takes its natural height. The footer still
sticks to the bottom via the existing `margin-top: auto`.
5. **Default config values** — added to `config-schema.ts`:
- `pageNavigation` (top-level) defaults to `true`.
- `theme.codeHighlight` defaults to `true`.
`layout.spa` was already defaulted. Override with `false` to
opt out. Users no longer have to add these to
`docmd.config.json` to enable them.
Verified: `@docmd/ui`, `@docmd/core`, and `@docmd/template-summer`
all build cleanly via `tsc`.
The `makeFileIcon()` helper in summer.js injected a static SVG path/polyline string via `icon.innerHTML = '<path .../>...'`. The content was a hardcoded literal (no user input), so this was a false-positive in the security audit, but it tripped the `Potential DOM XSS (innerHTML)` rule in `scripts/security-audit.mjs` and broke `pnpm prep`. Refactored to build the SVG with `document.createElementNS` for the `<path>` and `<polyline>` children — canonical, no audit noise, and a touch more explicit about what we're creating. `pnpm prep` now passes all integration tests and the security audit reports no high-risk patterns.
…s, copyright, light
The user's copy-pasted minimal config revealed several QoL keys
that users had to manually add even though the resulting values
were always the same. Now defaulted in `config-schema.ts`:
Top-level:
- `pageNavigation` (already added previously)
- `copyCode: true` — copy-code buttons on <pre> blocks
- `autoTitleFromH1: true` — use the first H1 as page title
layout:
- `spa: true` (already)
- `breadcrumbs: true` — breadcrumb row above page header
- `header.enabled: true` (already)
- `sidebar.collapsible: true` + `defaultCollapsed: false` (already)
- `optionsMenu.position: 'header'` (already)
- `optionsMenu.components.search: true` + themeSwitch: true (already)
- `optionsMenu.components.sponsor: null` (opt-in, already)
- `footer.style: 'minimal'` (already)
- `footer.copyright: `© ${new Date().getFullYear()}`` (new)
- `footer.branding: true` (already)
theme:
- `name: 'default'` (already)
- `appearance: 'light'` (CHANGED from 'system' — matches the
user-pasted config; 'system' is still settable by the user)
- `codeHighlight: true` (already added previously)
All other previously-existing defaults preserved. `pnpm prep`
passes (failsafe + integration tests + security audit).
- Default 404 page redesigned with glass card, soft halo, and primary 'Return home' + ghost 'Go back' buttons (Lucide icons). All strings wrapped in t() with English fallbacks; lang/dir set from activeLocale for RTL support. errorCode404 now used for the '404' glyph for screen-reader consistency. - .card .card-title: 0.95rem (was 1.125rem), tighter padding/margin. Denser feature grids; cards feel less top-heavy in grids containers. - 3 new translation keys added to all 7 locales: poweredBy, viewSource, goToTop. Available for templates and i18n.locales[].translations overrides in docmd.config.json. pnpm prep: 22/22 mega tests, universal failsafe V5.0 passed.
- Halo / glow tokens (--glow-color, --glow-color-strong, --halo-soft,
--halo-soft-strong) added to :root + :root[data-theme=dark]. Use
them in card hover, button hover, focus-visible, and step number
glows to keep the visual language consistent.
- Card hover: tightened shadow + halo, subtle border tint (color-mix),
reduced lift (-1px from -2px). No more bouncy feel.
- Active sidebar link: replaced the old 'inset 2px border' with a
modern 3px pill centered vertically on the existing .submenu
thread border ('node on thread' style). Smaller, calmer, plays
well with the active-parent rail.
- TOC active: uses --link-color via color-mix for the background
tint, font-weight 600, rounded right edge.
- Focus-visible: subtle halo via --halo-soft in addition to outline.
Common interactive elements (a, button, .toc-link, .sidebar nav li a,
.docmd-button, .docmd-tag) get a unified 0.15s ease transition.
- .docmd-button: halo on hover, transform on active.
- Text contrast bump (light + dark): --text-color and --text-muted
in both modes are now AA+ (light --text-color #27272a, dark
--text-color #d4d4d8, dark --text-muted #94949e). Dark
--link-color brightened to #38bdf8 for better contrast against
the new darker --bg-color #0d0d0f.
- Steps: each <li> now has id='step-N' (globally unique across the
page) and a hidden .step-permalink anchor (Lucide link-2 icon,
same as .heading-anchor) that fades in on hover/focus. scroll-
margin-top on .step-item lands the anchor below the sticky
header. Visual numbering is unchanged (CSS counter resets per
<ol>).
pnpm prep: 22/22 mega tests, universal failsafe V5.0 passed.
Plugins can now mark individual Asset entries as conditional via the
new AssetCondition interface. The asset's <link> / <script> tag is
only emitted on pages where the condition matches the rendered HTML
and/or frontmatter. Unconditional assets keep the legacy behaviour
(included on every page), so this is fully backward compatible.
Supported predicates (all keys AND-ed; values within a key OR-ed):
- pageHtmlMatches: string | string[] page HTML substring match
- frontmatterHas: string page frontmatter key
The mermaid plugin ships with the new condition turned ON:
condition: { pageHtmlMatches: 'class="mermaid"' }
This skips the init-mermaid.js <script> on pages that have no mermaid
block, which in turn skips the ~500 KB mermaid library CDN fetch at
runtime. On a 11-page test build, the script now appears on 3/11 pages
(down from 11/11) — the largest single win in the mobile PageSpeed
'Reduce unused JavaScript' estimate (137 KiB across the test site).
API:
- @docmd/api: new AssetCondition type, re-exported from the
package root. Asset interface gets an optional field
with a JSDoc example.
- @docmd/core: per-page asset rendering moved from build-once to
per-page. The onBeforeRender hook now runs BEFORE asset filtering
so plugins can mutate page.htmlContent / page.frontmatter and the
conditional filter sees the final state.
Five-part cleanup focused on client weight and the stale version meta:
1. Conditional asset loading for the math plugin
- @docmd/plugin-math now ships
condition: { pageHtmlMatches: [class="katex", class="katex-display"] }
- Skips the ~30 KB katex.min.css fetch on pages with no rendered math
- Verified: 1/1 playground page with math still loads it, 0/10 others do
2. <meta name=generator> is now dynamic
- Previously hardcoded "docmd v0.5.x" in three templates even though
the binary is 0.8.x — stale since the 0.5 era
- New coreVersion template local threads pkg.version from
@docmd/core/package.json through build.ts → buildLocales/buildVersions
→ renderPages → template
- layout.ejs, no-style.ejs, docmd-search.ejs all use <%= coreVersion %>
- Verified: rendered meta now reads "docmd v0.8.7"
3. CSS weight shed in docmd-main.css (net -13 lines, zero behavior change)
- Removed 6 declared-but-unused vars:
--content-area-bg / -radius / -shadow / -border (4, never referenced)
--halo-soft-strong (light + dark, --halo-soft is what's wired)
--sponsor-red-hover (light + dark, --sponsor-red is used directly)
- Removed @Keyframes heartbeat (declared but never referenced)
4. Callout variants dedup
- 5 near-duplicate rules → 1 base + 4 token overrides
- Base .callout now uses --callout-color (falls back to transparent /
var(--code-bg) when no variant is present, so the no-variant case
still renders as a neutral box)
- .callout-info / -warning / -success+tip / -danger set --callout-color
- Border + bg derived from the token via color-mix()
- Tiny visual tightening on .callout-success / .callout-tip border
(was 47% alpha #2ecc7178, now fully opaque #2ecc71)
5. Verified mermaid conditional is live
- 3/11 playground pages get init-mermaid.js (matches the 3 pages with
class="mermaid" exactly — index.html, mermaid-icon-test/, hi/...)
- 8/11 pages now skip the ~500 KB mermaid library CDN fetch
- Same condition API as session 4, just confirming it works end-to-end
22/22 mega tests pass. pnpm prep ✅.
Docmd-source HTML comment + JS console.log: kept (per creator license
header; ~250 bytes per page, no SEO/perf impact worth removing).
After a full monorepo usage scan, 14 declared classes in summer.css
had zero live emitters. Verified with:
for c in <14 classes>; do
grep -rln "\b$c\b" packages/ | grep -v dist
done
Each returned exactly 1 hit (the stylesheet itself).
Removed (16 rules, 92 lines):
- .search-filter-pill (5 rules) — replaced by .docmd-search-filter-pill
- .summer-search { ... } + :hover/:focus-visible
— root class never applied; only .summer-search-container is
- .summer-search__placeholder, .summer-search__kbd (+ kbd descendant)
— never emitted
- .summer-search-filters, .summer-search-loading — never emitted
- .summer-copy-btn--icon-only — modifier never applied
- .summer-pageheader__title — no such element in layout
- .summer-pagenav__link--disabled — modifier never applied
- .summer-toc__item--level-1/2/3/4 — TOC indentation is set
inline by summer.js wireTocSvgTrack()
Net: 3416 -> 3325 lines (-91, ~2.7%), zero behavior change.
22/22 mega tests pass.
…ages
On a short page (e.g. nested-page-2/, just 110px of content), .page-navigation
was sitting at the bottom of .main-content (349px) — 131px of empty
space between it and .page-footer-actions, instead of sitting at the
bottom of the content area next to the page-footer-actions (24px gap).
Same problem in summer's .summer-pagenav (no sticky behavior at all
on short pages — just a fixed margin-top: 56px).
Root cause: .main-content / .summer-content were sized to their content
(align-items: flex-start on the row flex parent) and never stretched
to fill .content-area, so margin-top: auto on the page-nav had nothing
to push against.
Fix: let the flex containers fill the available height, and add
align-self: flex-start to the sidebar so it doesn't stretch.
docmd-main.css (default template):
- .content-layout: align-items: flex-start -> stretch
(with min-height: 0 so flex children can shrink)
- .toc-sidebar: add align-self: flex-start (don't stretch the TOC)
- .main-content already has flex: 1 1 0 — now fills the height
- .page-navigation already has margin-top: auto — now pushes to
the bottom of .main-content, which is the bottom of .content-area
summer.css:
- .summer-main: add flex: 1, min-height: 0 (so it fills the wrapper)
- .summer-content: add display: flex, flex-direction: column, flex: 1,
min-height: 0
- .summer-pagenav: margin-top: 56px -> margin-top: auto
Verified on http://localhost:3000/nested-page-2/ (height 735px, same as
viewport — no scroll). Before: page-nav bottom at y=458, page-footer-
actions top at y=589 (131px gap). After: page-nav bottom at y=565,
page-footer-actions top at y=589 (24px gap, just the 1.5rem margin).
22/22 mega tests pass.
Release 0.8.14
…l MiniSearch, auto-install dedup URL & assets (#175 follow-up): - Subpath deploys (GitHub Pages project sites) now use root-relative asset/nav URLs (/repo/assets/...) instead of depth-relative paths combined with a <base> tag. The <base> tag is no longer emitted for non-offline builds — root-relative paths resolve correctly at any page depth, work with fetch()/SPA, and don't break hash-links on nested pages. Offline builds keep their depth-relative + index.html design (#179 fix untouched, separate branch). Navigation normalisation: - Local navigation.json is now passed through normalizeNavPaths at load time in config-loader, matching the treatment inherited config nav and auto-nav already received. Fixes trailing-slash inconsistency between sidebar/prev-next and markdown body links in non-i18n projects. Search — local MiniSearch bundling: - MiniSearch is now copied from node_modules into site/assets/js/vendor/ at build time instead of loaded from a CDN at runtime. Makes keyword search work in air-gapped, offline, and CDN-blocked environments. Version is read dynamically from the installed package — no hardcoded version to maintain. Falls back to CDN only if local resolution fails. Auto-install dedup: - Worker threads no longer attempt auto-install (isMainThread guard in installRuntimeDep). Previously N workers each spawned npm install for the same missing plugin, printing the same failure N times. - Missing package.json now produces a single actionable hint instead of N 'unknown error' spawn failures (deduped via module flag). Tests: 609 passed, 0 failed.
Summer is a full layout override, so the default layout's versionRoot computation block never runs. Summer read locals.versionRoot which was never set, so DOCMD_VERSION_ROOT was always '/' — even on sub-version pages like /05/. This broke the sticky version switcher in docmd-main.js: the switcher strips the current version prefix (DOCMD_VERSION_ROOT) from the path to preserve the page suffix when switching versions. With DOCMD_VERSION_ROOT wrongly set to '/', it couldn't strip '/05/', so switching from v0.5.0 back to v0.6.0 recomputed the target as /05/ (same page) and appeared to do nothing. The fix mirrors the default layout's versionRoot logic inline in summer's layout. Default template is unaffected (it already computed this correctly). Tests: 609 passed, 0 failed.
…tall hardening Three coordinated fixes for the v0.8.15 follow-up (issue #175): 1. Dev mode now forces base=/ when base was auto-derived from url. The dev server always serves the output dir at root ('/'), so any subpath base (e.g. '/my-repo/' for a GitHub Pages project site) broke every asset on localhost. Users were forced to set base:'/' which then disabled auto-derivation and broke search on the production deploy. The override is restricted to dev and to auto-derived bases; an explicit DOCMD_PROJECT_PREFIX still wins (workspace dev uses it). 2. Search data moved from output root into .docmd-search/. Keyword search-index.json now lives at <out>/.docmd-search/search-index.json (or <out>/.docmd-search/<locale>/search-index.json for non-default locales), unifying all search artefacts in one folder and keeping the output root clean. 3. Removed the runtime HEAD probe for .docmd-search/manifest.json. The probe fired on every keyword-only page load and returned 404, which was the original symptom users reported. Semantic mode is now decided solely by the build-time data-semantic flag. 4. runtime-deps.ts: removed the pre-check that blocked auto-install whenever package.json was missing from cwd. Package managers (npm/pnpm/yarn/bun) walk up the tree themselves, so a missing local package.json in a workspace sub-project is not a failure case. The friendly hint now only fires when the spawn actually fails AND no package.json exists in any ancestor directory. Templates are covered by the same path as plugins. Verified in _brutetest/gh-sub-parent with alexhelms's exact config: - Build (no base): DOCMD_SITE_ROOT = '/my-site/', search-index.json at /.docmd-search/, summer and docmd-search auto-install silently. - Dev (no base): DOCMD_SITE_ROOT = '/', all assets serve at root. - Search returns results with correct /my-site/ subpath URLs.
…fter install Two follow-ups for the v0.8.16 search changes: 1. Keyword search-index.json now always generates, even when semantic succeeds. Previously the semantic path returned early, skipping keyword index generation. If the client's data-semantic flag was stale (deps installed in onPostBuild after pages were rendered without the flag), the client fell back to keyword mode and 404'd. 2. After semantic indexing succeeds, stamp data-semantic="true" on the search modal in every built HTML page. The flag is computed at render time from semanticUsable, which checks if docmd-search is resolvable. On a fresh install this is false until onPostBuild runs. Stamping the flag post-build closes the race without needing the runtime HEAD probe. Also bumps all monorepo packages to 0.8.16 and fixes the mega test (search-index.json moved to .docmd-search/).
Three fixes for the auto-install pipeline: 1. Version resolution: query npmjs for the actually-published latest version before pinning. When running a pre-release local build (e.g. 0.8.16 dev tar while npmjs has 0.8.15), the previous code pinned every auto-install to 0.8.16 which failed with ETARGET. Now fetches the registry's latest tag and uses that, so installs always target a version that exists. 2. Dedup: track failed installs per-package in a module-level Set. Dev server rebuilds call loadPlugins again, and a persistent failure (e.g. version mismatch, network error) won't fix itself between rebuilds. Each failed package now prints its error exactly once per process. resetInstallState() exposed for explicit user retries. 3. Sourcemap strip: copied .js assets (vendor/minisearch.js etc.) now have their //# sourceMappingURL comment stripped at copy time. The .map files were never shipped, so the browser 404'd on every page load looking for them. installRuntimeDep is now async to support the npm registry fetch.
1. config-loader: isDev override also accepts DOCMD_DEV env var, so any child docmd build process (e.g. semantic search subprocess) inherits the same base override as its parent dev server. Previously the subprocess ran a plain 'build' which auto-derived the production subpath and printed 'base auto-derived from url' in the dev TUI, confusing users about the actual serve path. 2. config-schema: declare autoNav: true (default; opt out with false). When the user provides no navigation config and no navigation.json, the loader falls back to Zero-Config auto-generation. Previously an explicit 'navigation: []' blocked auto-generation. Now that is the documented 'I have no nav, please generate' signal and 'autoNav: false' is the opt-out. 3. search onPostBuild: when spawning the child docmd build to do semantic indexing after a fresh install, set DOCMD_DEV=true so the base override applies.
…anges pnpm sim [--build|--dev|--push] Generates fresh tars from every @docmd/* monorepo package into a consumer project's local-tars/, runs npm install + docmd build in a throwaway workdir, and syncs the resulting site/ back so the user can inspect the live output. --build : build-only mode (default; exit after site/ is ready) --dev : start docmd dev server in the real consumer dir --push : commit + push the consumer (triggers GH Pages deploy) Variables live at the top of tools/sim.mjs so paths / branch / remote can be updated in one place. Packages are packed from a /tmp staging copy with workspace:* refs rewritten — the monorepo working tree is never mutated.
Renames a GitHub Pages repo without updating docmd.config.json's url field silently breaks sitemap, canonical, og:url, and auto-derived asset paths — they keep pointing at the old pathname. The build has no way to know the deploy host is wrong, so it produces a site that deploys without 4xx but is internally inconsistent. Detection: read the git remote origin, extract the repo name (last pathname segment), compare against the URL pathname's last segment. Warn only on actual mismatch (e.g. gh-sub-parent vs beta-test), not on owner/host differences which the URL doesn't encode. Output names the affected surfaces so the user knows what's stale, points at the field to update, and does not fail the build — the user may have a perfectly valid setup without a git remote (CI, local-only).
The build now emits a single <base href="..."> tag in the document
head for any subpath deploy (siteRootAbs !== '/'). Asset URLs in the
head use simple-relative paths ('assets/foo.css'); the <base> tag
shifts them to the correct location regardless of where the site is
served from. Deploys can move between repos and hosts without code
changes — the URL field stays authoritative, the build recomputes
the base path.
Why this matters:
v0.8.15 fix the original #175 (#175) by killing <base> entirely
and using root-relative asset URLs (e.g. '/repo/assets/x'). That
works but encodes the subpath into every URL. Moving the site
between repos breaks until you update config.url and rebuild.
Now: <base href="/repo/"> + simple-relative 'assets/x' =
'/repo/assets/x' at runtime. Same final URL, no per-URL encoding,
one tag to change if the deploy location shifts again.
Three-mode boundary:
• build (non-offline): <base> emitted at the head, assets simple-
relative ('assets/foo.css'), nav uses buildRelativeUrl() pages.
• dev: no <base>, assets depth-aware ('./' or '../') matching
the docmd dev fixed to /. Dev server prints from root.
• --offline: no <base>, depth-aware paths, file:// works because
base URLs are inherently host-less.
Implementation:
• packages/parser/src/utils/url-utils.ts
- UrlContext gains assetBaseUrl (string) and emitBase (boolean).
assetBaseUrl is '' when emitting <base>, otherwise page-depth.
- normaliseBaseTag() rewritten as a canonicaliser: strip all
existing <base> tags (template, partial, plugin stray), then
emit exactly one canonical tag right after </title>. Old
templates that emitted their own <base> get cleaned up here.
The function was previously a stripper — left in place from
pre-0.8.15. The new contract: one tag, placed by us, value
matches siteRootAbs exactly.
• packages/core/src/engine/generator.ts
- Thread assetBaseUrl, emitBaseTag, navRootPrefix, siteRootAbs
into the layout locals.
- renderAssetList() uses assetBaseUrl instead of relativePathToRoot
so plugin-emitted tags resolve through the same variable.
• packages/ui/templates/layout.ejs (default)
• packages/templates/summer/templates/layout.ejs (summer override)
• packages/ui/templates/partials/footer.ejs
- Switch asset URL <link>/<script>/<img> attrs from relativePathToRoot
to assetBaseUrl. The canonicaliser handles any leftover <base>
in templates, so this is purely about HTML emit shape.
Verified end-to-end with alexhelms-shaped config at /beta-test/: build
emits <base href='/beta-test/'> + assets/assets/...; every asset URL
loads HTTP 200 from the new simulator; nav, search, theme switch all
work. Dev mode unaffected (no base, depth-relative paths). --offline
mode unaffected (no base, depth-aware paths with index.html suffix).
The 609 existing contract/feature tests still pass (run took ~70s
because the feature-integration tests build real projects). No new
test added here — the existing asset-base-url.test.js exercises the
root-relative path which the new assetBaseUrl = '' slot doesn't
change behaviour for.
When the docmd-main.js SPA initialiser runs, it rewrites every asset
<link> to an absolute URL using:
new URL(href, window.location.href).href
That re-resolves the RAW (pre-base) attribute string against the page
URL, dropping the <base href> prefix. On a nested page like
/beta-test/guide/, the request becomes /beta-test/guide/assets/css/x
which 404s — even though the document has
<base href='/beta-test/'> and the browser initially resolves assets
correctly via that base.
The DOM property 'link.href' already returns the absolute URL that
the browser computed using <base>. Reading asset.href instead of
asset.getAttribute('href') preserves that resolution.
Same fix applies to the SPA page-swap path (line ~724): re-resolving
newAsset.getAttribute('href') against data.finalUrl strips the base
prefix on nested destinations. Use newAsset.href (already resolved
against the source page's <base>) and clone the element verbatim.
Repro: published site mgks.github.io/beta-test/guide/ — all CSS
requests went to /beta-test/guide/assets/... (404). After this fix
they correctly resolve to /beta-test/assets/... (200).
Verified:
- cssLinks.anyGuideInUrls = false (no /guide/ in any URL)
- cssLoaded = 5 of 5 CSS files
- failedCount = 0
The previous design used simple-relative asset hrefs (e.g. 'assets/x')
relying on the <base href> tag for preloader resolution. But the
HTML preloader (which starts fetching <link href=...> resources
during HTML parse, before <base> is in effect) resolves them against
the page URL, not <base>. On a nested page like /beta-test/guide/,
the preloader fetched /beta-test/guide/assets/x — 404.
The fix: when emitBase is true, assetBaseUrl = base (e.g. '/beta-test/').
This makes asset hrefs root-relative ('/beta-test/assets/x'), which
the preloader handles correctly because root-relative URLs are
origin-relative, independent of <base>.
<base> stays in the document for nav and SPA paths that may use
simple-relative URLs internally, but it is no longer the load-bearing
mechanism for asset fetches. The preloader fix is now the
load-bearing mechanism.
Verified:
- site/index.html: href='/beta-test/assets/...' (5 of 5 assets)
- site/guide/index.html: href='/beta-test/assets/...' (5 of 5 assets)
- No more /beta-test/guide/assets/... paths in the preloader
- <base href='/beta-test/'> still emitted (verified)
The earlier docmd-main.js fix (use asset.href instead of
getAttribute('href')) is still useful as a defence-in-depth: it
prevents the SPA initialiser from re-resolving root-relative hrefs
against the page URL.
…nd flags
Three problems with the existing prep:
1. The test step streams the runner's full TUI output during the
multi-minute run. Operator sees a wall of text scrolling past with
no clear pass/fail signal until the very end.
2. There's no way to skip the slow test pipeline. Lint + build alone
take ~30s; tests add 60-70s on a warm cache. Opening a PR usually
only needs lint + build to confirm nothing regressed.
3. No flag to expand a section's detail when investigating a failure —
the operator has to either re-run the whole prep or dig into logs.
This change:
- runTestStep() now CAPTURES stdout by default and prints one line
per step ('[ DONE ] 609 passed, 0 failed across 21 files'). If
a test failed, the captured stdout/stderr is replayed as a tail in
the Issues block. Streaming only happens with --verbose / --expand.
- New --skip-tests / --fast flag short-circuits the two test steps
with 'skipped' lines and continues the pipeline.
- New --expand flag streams the runner's full TUI when set (alongside
the existing --verbose / --full flags).
Prep time: --skip-tests ~49s, full ~120s on warm cache.
The runner wraps every section header in CYAN(...) which prefixes an ANSI escape sequence to the line. The previous regex started with [\s│]*, expecting whitespace or the box-drawing char, but the first byte was an escape. Strip ANSI codes first, then match. With this fix the progress markers (one per test file the runner starts) appear under each step's WAIT line as the test runs: Categorised test suite (tests/runner.js) [WAIT] │ \u2192 Exit-code contract (F6, M-12) │ \u2192 Plugin add/remove across config formats │ \u2192 Validate rewrite + workspace errors │ \u2192 Offline-mode internal links (#167) \u2026 Categorised test suite (tests/runner.js) [ DONE ] 609 passed Each marker tells the operator which test file is currently running so the multi-minute test suite never looks silent.
On Windows, `path.dirname(new URL(import.meta.url).pathname)` returns `/C:/Users/...` because URL.pathname keeps the leading slash and the drive letter prefix. Every downstream fs.existsSync that derives from this __dirname misses its target, the dev server spins in a tight microtask loop, and CPU usage pegs to ~90% (issue #182). `fileURLToPath(import.meta.url)` decodes the URL correctly per-platform and was the user-supplied fix in the issue thread. Verified: this is the only occurrence of the unsafe pattern in the monorepo (grep across packages/).
_playground (the user-facing manual playground):
- Move from packages/_playground/ to repo root as _playground/.
Was inside the pnpm workspace which forced 'workspace:*' deps that
defeat the purpose of a clean consumer-install test. New location
is outside the workspace and uses file:local-tars/*.tgz deps so it
installs exactly like a real user would.
- Add _playground/dev.js: small wrapper that validates tars exist,
runs npm install on first use, then dispatches to docmd dev / build
/ live. Wired as the source of truth for pnpm dev / build / live.
- Add _playground/README.md describing the tars-from-prep workflow.
- Delete packages/_playground/ (private, gitignored outputs included).
Test infrastructure:
- tests/runner.js: add a per-section progress bar (150 ms tick, 8-frame
animation) for external test entries that run as subprocesses.
Streams subprocess output live; clears the progress line on close
so the PASS/FAIL verdict can claim that row. Converted the main
loop to async so the in-process entries can also be awaited cleanly.
- tests/security.test.js + tests/feature-integration.test.js: import
tests/_lib/build-cache.mjs to memoise successful docmd build output
by content hash. Second run on unchanged fixtures is instant; first
run is unchanged. Bypass with DOCMD_TEST_CACHE_OFF=1.
- tests/_lib/build-cache.mjs is now tracked (was untracked before;
feature-integration.test.js already imported it).
Tools cleanup:
- tools/simulate-consumer.mjs deleted (no callers anywhere).
- scripts/probe-threads-xss.mjs deleted (orphan debug script).
- tools/sim.mjs: rewritten to take --source=<consumer-dir> instead
of a hardcoded _brutetest/gh-sub-parent path. Add --regen-tars to
produce fresh tarballs into <consumer>/local-tars/ (used by
pnpm prep). Use a /tmp staging copy + versionless filenames so the
consumer package.json can reference file:local-tars/*.tgz without
bumping on every version.
- tools/prep.js: add --docker flag to gate the Docker availability
check. Add a Consumer Simulation section after Tests that runs
'sim.mjs --source _playground --regen-tars' to keep the playground's
local-tars fresh. Keep --skip-tests / --expand / --verbose.
- tools/docker-prebuild.sh: drop the _playground stub (playground is
no longer inside packages/).
- tools/security-audit.mjs: docstring updated to clarify it
complements tests/security.test.js (static-analysis pass vs runtime
policy pass — not duplicates).
Root scripts:
- pnpm dev / build / live now route through _playground/dev.js,
which validates that local-tars/ exists and prompts the user to run
'pnpm prep' if it doesn't (the only place tars are produced now).
- pnpm docmd / docmd-live point at _playground/node_modules/...
after prep has installed there.
- Delete the stale 'simulate' / 'simulate:local' echo stubs.
- pnpm sim routes through 'sim.mjs --source _playground --regen-tars
--build' for manual dev iteration on monorepo source.
Directory placement:
- eslint-rules/ -> tools/eslint-rules/. Was a top-level directory
that contained 3 files for a single in-tree ESLint plugin used
only by eslint.config.mjs at the root. tools/ is the natural home
for developer tooling.
- eslint.config.mjs import path updated to './tools/eslint-rules/...'.
…les/images in offline builds
…imulation build; tui: simplify Output/Source paths to relative formats
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release 0.8.16
🐛 Fix: Robust offline asset path resolution
Offline builds no longer suffix asset files (PDFs, images, JSON) with
/index.html. The URL normalizer now checks for a file extension before any path transformation, leaving all non-markdown files completely untouched.🏗 Fix: Portable
<base href>for subpath deploysNon-offline builds with a subpath now emit a single
<base href="/<repo>/">tag. Asset URLs use simple-relative paths; the<base>tag resolves them at runtime. Moving a site between repos or hosts no longer requires rebuilding with an updatedconfig.url.🪟 Fix: Windows path crash (issue #182)
All
__dirnamederivations now usefileURLToPath(import.meta.url)instead ofpath.dirname(new URL(import.meta.url).pathname), which produced malformed/C:/...paths on Windows causingfs.existsSyncto loop at 100% CPU.🧰 Auto-install no longer blocks workspace sub-projects
The
package.jsonpre-check has been removed from the auto-install path. npm/pnpm/yarn/bun walk up the directory tree themselves. The friendly hint now fires only when the spawn genuinely fails and nopackage.jsonexists in any ancestor.🗂 Fix: All search data unified under
.docmd-search/Keyword and semantic search indexes now share a single output folder. The runtime manifest HEAD probe that caused 404s on keyword-only sites has been removed. The keyword index always generates as a fallback.
🎨 Favicon fallback — no more browser 404s
The page generator and 404 template now inject the default
assets/favicon.icowhen no custom favicon is configured.🧹 CLI environment warnings silenced
Subprocess spawners strip pnpm-internal
npm_config_*env vars before running npm/npx commands, eliminating the "Unknown env config" noise.🧰 Consumer simulation workflow (
sim.mjs)--source,--regen-tars,--doctorflags addedpackage.jsonfor install then cleanly restoreddev:sim,build:sim,doctor:simscripts wired indocs/package.json📊 Prep pipeline TUI polish
Cursor-up in-place rewriting for step status, accurate total elapsed timer, and concise error-only summary at the end.
613 tests passing · 0 failures · pnpm prep clean