From 1ad139fafe03a002e73e7ff1e5aa7828ca68437b Mon Sep 17 00:00:00 2001 From: Maximilian Roos Date: Sat, 12 Sep 2026 16:42:55 -0700 Subject: [PATCH] docs: scope heading anchors under each subcommand section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Command pages append every subcommand's help, so headings like "Examples" and "Command reference" repeat, and the site numbered their anchors (#examples-3) by position, shifting them whenever a section was added. The heading-id plugin now prefixes ids below a `wt …` H2 with that section's id (#wt-step-push--examples), so the CLI help keeps unqualified headings. The six `wt config` " examples" renames that worked around the numbering go back to "Examples". Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HUmx2Jd5mTK5TznqGDmLGp --- docs/CLAUDE.md | 5 ++-- docs/src/content/docs/config.md | 12 ++++---- docs/src/content/docs/faq.md | 2 +- .../plugins/pagefind-command-references.mjs | 11 ++------ docs/src/plugins/stable-heading-ids.mjs | 28 +++++++++++++++++-- docs/tests/built-site.test.mjs | 13 +++++---- docs/tests/plugins.test.mjs | 28 +++++++++++++++++++ .../skills/worktrunk/reference/config.md | 12 ++++---- .../skills/worktrunk/reference/faq.md | 2 +- skills/worktrunk/reference/config.md | 12 ++++---- skills/worktrunk/reference/faq.md | 2 +- src/cli/config.rs | 16 +++++------ ...on_tests__help__help_config_approvals.snap | 2 +- ...ration_tests__help__help_config_state.snap | 2 +- ..._tests__help__help_config_state_cache.snap | 2 +- ...n_tests__help__help_config_state_logs.snap | 2 +- 16 files changed, 100 insertions(+), 51 deletions(-) diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md index 618c179bd9..f1ab17aec1 100644 --- a/docs/CLAUDE.md +++ b/docs/CLAUDE.md @@ -95,8 +95,9 @@ Use root-relative links in canonical Markdown: The sync pipeline expands them to full `https://worktrunk.dev/...` URLs for README and agent-skill copies. Do not add framework-specific link syntax. -`stable-heading-ids.mjs` preserves the site's established anchor scheme, and -`test:site` verifies every built internal page link and fragment. +`stable-heading-ids.mjs` defines the heading-anchor scheme, including the ids +scoped under each subcommand section, and `test:site` verifies every built +internal page link and fragment. Images and demos use root-relative paths into `public/`: diff --git a/docs/src/content/docs/config.md b/docs/src/content/docs/config.md index d9cfc8a00c..f5bde00c82 100644 --- a/docs/src/content/docs/config.md +++ b/docs/src/content/docs/config.md @@ -851,7 +851,7 @@ Manage command approvals. Project hooks and project aliases prompt for approval on first run to prevent untrusted projects from running arbitrary commands. Approvals from both flows are stored together. -### Approval examples +### Examples List commands and their approval status for current project: ```console @@ -936,7 +936,7 @@ Inspect and preview aliases. Aliases are command templates configured in user (`~/.config/worktrunk/config.toml`) or project (`.config/wt.toml`) config and run as `wt `. See the [Extending Worktrunk guide](/extending/#aliases) for the configuration format. -### Alias examples +### Examples Show every configured alias's template: ```console @@ -984,7 +984,7 @@ State is stored in `.git/` (config entries and log files), separate from configu - **vars**: [Custom variables per branch](/config/#wt-config-state-vars) - **logs**: [Operation and debug logs](/config/#wt-config-state-logs) -### State examples +### Examples Get the default branch: ```console @@ -1060,7 +1060,7 @@ View or drop worktrunk's regenerable caches in one place. Everything here is reb Without a subcommand, runs `get`. -### Cache examples +### Examples Show cache contents: ```console @@ -1201,7 +1201,7 @@ All logs are stored in `.git/wt/logs/` (in the main worktree's git directory). A `wt config state logs --format=json` emits three arrays — `command_log`, `hook_output`, `diagnostic`. Each entry carries a `file` (relative), `path` (absolute), `size`, and `modified_at` (unix seconds). Hook-output entries additionally expose `branch`, `source` (`user` / `project` / `internal`), `hook_type` (the `post-*` kind, or `null` for internal ops), and `name`. Filter with `jq` to pick out a specific entry. -### Log examples +### Examples List all log files: ```console @@ -1343,7 +1343,7 @@ Custom variables per branch. Store custom variables per branch. Values are stored as-is — plain strings or JSON. -### Variable examples +### Examples Set and get values: ```console diff --git a/docs/src/content/docs/faq.md b/docs/src/content/docs/faq.md index e9e765d74e..fd9039f499 100644 --- a/docs/src/content/docs/faq.md +++ b/docs/src/content/docs/faq.md @@ -59,7 +59,7 @@ Git TUIs operate on a single repository. Worktrunk manages multiple worktrees, r Worktrees share one `.git`. Each adds a checkout of the tracked files, plus whatever gitignored build output you copy in. -On APFS, btrfs, and XFS (not ext4 or NTFS), [`wt step copy-ignored`](/step/#copy-on-write) reflinks that output, so a new worktree shares the primary worktree's disk blocks. A later build rewrites only what changed, and the rest stays shared. On one machine, 56 worktrees of a Rust repository with a 40GB `target/` came to 2.6TB by `du` and 0.7TB on disk. +On APFS, btrfs, and XFS (not ext4 or NTFS), [`wt step copy-ignored`](/step/#wt-step-copy-ignored--copy-on-write) reflinks that output, so a new worktree shares the primary worktree's disk blocks. A later build rewrites only what changed, and the rest stays shared. On one machine, 56 worktrees of a Rust repository with a 40GB `target/` came to 2.6TB by `du` and 0.7TB on disk. ## Does Worktrunk support stacked branches? diff --git a/docs/src/plugins/pagefind-command-references.mjs b/docs/src/plugins/pagefind-command-references.mjs index eccdc8f788..27b58603bd 100644 --- a/docs/src/plugins/pagefind-command-references.mjs +++ b/docs/src/plugins/pagefind-command-references.mjs @@ -1,13 +1,6 @@ -const fragmentTitleClass = 'wt-pagefind-fragment-title'; - -function nodeText(node) { - if (node.type === 'text') return node.value; - return node.children?.map(nodeText).join('') ?? ''; -} +import { isCommandHeading, nodeText } from './stable-heading-ids.mjs'; -function isCommandHeading(text) { - return /^wt\s+\S/u.test(text.trim()); -} +const fragmentTitleClass = 'wt-pagefind-fragment-title'; function escapeHtml(text) { return text.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>'); diff --git a/docs/src/plugins/stable-heading-ids.mjs b/docs/src/plugins/stable-heading-ids.mjs index 3560fcd2a2..22762d653d 100644 --- a/docs/src/plugins/stable-heading-ids.mjs +++ b/docs/src/plugins/stable-heading-ids.mjs @@ -1,8 +1,13 @@ -function nodeText(node) { +export function nodeText(node) { if (node.type === 'text') return node.value; return node.children?.map(nodeText).join('') ?? ''; } +/** Whether a heading opens a generated subcommand section, such as "wt step commit". */ +export function isCommandHeading(text) { + return /^wt\s+\S/u.test(text.trim()); +} + /** Preserve the heading-anchor scheme used by Worktrunk's original site. */ export function slugHeading(text) { return text @@ -12,17 +17,36 @@ export function slugHeading(text) { .replace(/^-|-$/g, ''); } +/** + * Gives every heading a public anchor id: its slug. + * + * Command pages append each subcommand's help under an H2 such as "wt step push", + * so headings like "Examples" and "Command reference" repeat down the page. Below + * a subcommand's H2, until the next H1 or H2, ids are scoped by that section: + * "Examples" under "wt step push" is `wt-step-push--examples`. The id then depends + * only on the heading and its subcommand, not on how many same-named headings + * precede it, and the help text keeps its unqualified headings for the terminal. + * A slug never contains `--`, so a scoped id cannot equal another heading's slug: + * "Cache" under "wt config state" and the "wt config state cache" section differ. + * + * A heading that still repeats within one scope takes `-1`, `-2`, … in page order. + */ export function rehypeStableHeadingIds() { return (tree) => { const slugs = new Map(); + let scope; function visit(node) { if (node.type === 'element' && /^h[1-6]$/.test(node.tagName)) { - const base = slugHeading(nodeText(node)); + const text = nodeText(node); + const level = Number(node.tagName[1]); + if (level <= 2) scope = undefined; + const base = scope ? `${scope}--${slugHeading(text)}` : slugHeading(text); const occurrence = slugs.get(base) ?? 0; slugs.set(base, occurrence + 1); node.properties ??= {}; node.properties.id = occurrence === 0 ? base : `${base}-${occurrence}`; + if (level === 2 && isCommandHeading(text)) scope = node.properties.id; } node.children?.forEach(visit); } diff --git a/docs/tests/built-site.test.mjs b/docs/tests/built-site.test.mjs index aec684c98e..c89d74ec68 100644 --- a/docs/tests/built-site.test.mjs +++ b/docs/tests/built-site.test.mjs @@ -239,7 +239,7 @@ test('build preserves the public route contract', async () => { ); } assert.doesNotMatch(configToc, /href="#claude-code"/); - assert.doesNotMatch(configToc, /href="#command-reference-1"/); + assert.doesNotMatch(configToc, /href="#wt-config-[^"]*--command-reference"/); const structuralSections = new Map([ ['/hook/', ['hook-types', 'security', 'configuration', 'running-hooks-manually', 'recipes']], @@ -276,9 +276,12 @@ test('nested command references expose unique search-only fragment titles', asyn assert.match(stepPage, /

Command reference<\/h2>/); const references = [...stepPage.matchAll( - /