From 36f201b36e7b905eb0158ee25b5045342e5832f3 Mon Sep 17 00:00:00 2001 From: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Date: Thu, 3 Sep 2026 06:53:26 +0000 Subject: [PATCH] docs: link the canonical home-page install anchor, not the deindexed duplicate --- docs/src/content/docs/faq.md | 2 +- plugins/worktrunk/hooks/wt.sh | 2 +- .../skills/worktrunk/reference/faq.md | 2 +- skills/worktrunk/reference/faq.md | 2 +- tests/integration_tests/readme_sync.rs | 84 +++++++++++++++---- 5 files changed, 70 insertions(+), 22 deletions(-) diff --git a/docs/src/content/docs/faq.md b/docs/src/content/docs/faq.md index 167017a3d..12dab52b4 100644 --- a/docs/src/content/docs/faq.md +++ b/docs/src/content/docs/faq.md @@ -256,7 +256,7 @@ Clear with `wt config state logs clear`. ## Does Worktrunk work on Windows? -Yes. Core commands, shell integration, and tab completion work in both Git Bash and PowerShell. See [installation](/worktrunk/#install) for setup details, including avoiding the Windows Terminal `wt` conflict. +Yes. Core commands, shell integration, and tab completion work in both Git Bash and PowerShell. See [installation](/#install) for setup details, including avoiding the Windows Terminal `wt` conflict. **Git for Windows required** — Hooks use bash syntax and execute via Git Bash, so [Git for Windows](https://gitforwindows.org/) must be installed even when PowerShell is the interactive shell. diff --git a/plugins/worktrunk/hooks/wt.sh b/plugins/worktrunk/hooks/wt.sh index a7a4a72e1..9b4b74142 100755 --- a/plugins/worktrunk/hooks/wt.sh +++ b/plugins/worktrunk/hooks/wt.sh @@ -20,7 +20,7 @@ elif [[ "$(uname -o 2>/dev/null)" =~ ^(Msys|Cygwin)$ ]]; then elif command -v wt >/dev/null 2>&1; then # reject wt if it's the Windows Terminal alias if [[ "$(command -v wt)" == *WindowsApps* ]]; then - echo "worktrunk: 'wt' resolves to Windows Terminal; install worktrunk as git-wt.exe or remove the Windows Terminal alias. See https://worktrunk.dev/worktrunk/#install" >&2 + echo "worktrunk: 'wt' resolves to Windows Terminal; install worktrunk as git-wt.exe or remove the Windows Terminal alias. See https://worktrunk.dev/#install" >&2 exit 1 fi diff --git a/plugins/worktrunk/skills/worktrunk/reference/faq.md b/plugins/worktrunk/skills/worktrunk/reference/faq.md index 6d7e27263..938086e24 100644 --- a/plugins/worktrunk/skills/worktrunk/reference/faq.md +++ b/plugins/worktrunk/skills/worktrunk/reference/faq.md @@ -252,7 +252,7 @@ Clear with `wt config state logs clear`. ## Does Worktrunk work on Windows? -Yes. Core commands, shell integration, and tab completion work in both Git Bash and PowerShell. See [installation](https://worktrunk.dev/worktrunk/#install) for setup details, including avoiding the Windows Terminal `wt` conflict. +Yes. Core commands, shell integration, and tab completion work in both Git Bash and PowerShell. See [installation](https://worktrunk.dev/#install) for setup details, including avoiding the Windows Terminal `wt` conflict. **Git for Windows required** — Hooks use bash syntax and execute via Git Bash, so [Git for Windows](https://gitforwindows.org/) must be installed even when PowerShell is the interactive shell. diff --git a/skills/worktrunk/reference/faq.md b/skills/worktrunk/reference/faq.md index 6d7e27263..938086e24 100644 --- a/skills/worktrunk/reference/faq.md +++ b/skills/worktrunk/reference/faq.md @@ -252,7 +252,7 @@ Clear with `wt config state logs clear`. ## Does Worktrunk work on Windows? -Yes. Core commands, shell integration, and tab completion work in both Git Bash and PowerShell. See [installation](https://worktrunk.dev/worktrunk/#install) for setup details, including avoiding the Windows Terminal `wt` conflict. +Yes. Core commands, shell integration, and tab completion work in both Git Bash and PowerShell. See [installation](https://worktrunk.dev/#install) for setup details, including avoiding the Windows Terminal `wt` conflict. **Git for Windows required** — Hooks use bash syntax and execute via Git Bash, so [Git for Windows](https://gitforwindows.org/) must be installed even when PowerShell is the interactive shell. diff --git a/tests/integration_tests/readme_sync.rs b/tests/integration_tests/readme_sync.rs index 82b0a9296..959811137 100644 --- a/tests/integration_tests/readme_sync.rs +++ b/tests/integration_tests/readme_sync.rs @@ -123,14 +123,46 @@ static RUST_RAW_STRING_PATTERN: LazyLock = LazyLock::new(|| { }); /// Regex to convert site-root documentation links to full URLs. -/// Matches: [text](/page/) or [text](/page/#anchor). +/// Matches: [text](/page/), [text](/page/#anchor), and the page-less forms +/// [text](/) and [text](/#anchor) that name the site root. +/// +/// The page segment is optional because the home page is served at `/`: +/// `docs/src/pages/index.astro` renders `worktrunk.md` there, and that page +/// declares `/` canonical while marking its own `/worktrunk/` route +/// `noindex`. Without this alternative the only spelling the rewrite accepted +/// was the deindexed duplicate. +/// +/// A page segment still requires its trailing slash, so `](/page)` matches +/// nothing here and trips [`assert_no_untransformed_site_links`] rather than +/// being silently rewritten. /// /// Link text tolerates `]` characters when they appear inside a backticked /// code span (e.g. `[[block]]`), alternating "a `...` code span" with "any /// non-`]`-non-backtick char". Bare backticks are forbidden so the regex /// can't bridge across two unrelated code spans on the same line. static SITE_LINK_PATTERN: LazyLock = - LazyLock::new(|| Regex::new(r"\[((?:`[^`]*`|[^\]`])+)\]\(/([^)/]+)/(#[^)]*)?\)").unwrap()); + LazyLock::new(|| Regex::new(r"\[((?:`[^`]*`|[^\]`])+)\]\(/(?:([^)/]+)/)?(#[^)]*)?\)").unwrap()); + +/// Rewrite every site-root link in `content` to its absolute `worktrunk.dev` +/// URL. +/// +/// Shared by the two surfaces that need it — the GitHub-rendered copies and +/// the installed skill files — so the page-optional shape above is honored in +/// one place rather than in two closures that can drift. +fn rewrite_site_links(content: &str) -> String { + SITE_LINK_PATTERN + .replace_all(content, |caps: ®ex::Captures| { + let text = caps.get(1).unwrap().as_str(); + // Absent for a link to the site root; the trailing slash rides + // with the segment so `/#anchor` doesn't become `//#anchor`. + let page = caps + .get(2) + .map_or(String::new(), |m| format!("{}/", m.as_str())); + let anchor = caps.get(3).map_or("", |m| m.as_str()); + format!("[{text}](https://worktrunk.dev/{page}{anchor})") + }) + .into_owned() +} /// Guardrail for root-relative or legacy Zola links on generated non-site surfaces. static UNTRANSFORMED_SITE_LINK_PATTERN: LazyLock = @@ -947,14 +979,7 @@ fn heading_to_anchor(heading: &str) -> String { /// - AUTO-GENERATED marker comments → removed, leaving their Markdown body fn transform_docs_to_github(content: &str) -> String { // Transform internal links - let content = SITE_LINK_PATTERN - .replace_all(content, |caps: ®ex::Captures| { - let text = caps.get(1).unwrap().as_str(); - let page = caps.get(2).unwrap().as_str(); - let anchor = caps.get(3).map_or("", |m| m.as_str()); - format!("[{text}](https://worktrunk.dev/{page}/{anchor})") - }) - .into_owned(); + let content = rewrite_site_links(content); let content = AUTO_GENERATED_MARKER_PATTERN .replace_all(&content, "") .into_owned(); @@ -1250,6 +1275,36 @@ fn test_legacy_zola_link_fails_the_guardrail() { assert_no_untransformed_site_links("See [hooks](@/hook.md)", "test content"); } +/// A link naming the site root carries no page segment. Both generated +/// surfaces have to rewrite it, or the only spelling the docs can use is +/// `/worktrunk/` — the route the home page itself marks `noindex` with a +/// canonical pointing back at `/`. +/// +/// `](/page)` (a page segment without its trailing slash) stays unmatched, so +/// the guardrail keeps catching it instead of the widened pattern quietly +/// absorbing it. +#[test] +fn test_site_root_links_rewrite_to_the_canonical_url() { + assert_eq!( + rewrite_site_links("See [installation](/#install) and [hooks](/hook/#types)."), + "See [installation](https://worktrunk.dev/#install) and \ + [hooks](https://worktrunk.dev/hook/#types)." + ); + assert_eq!( + rewrite_site_links("[Worktrunk](/) is a CLI."), + "[Worktrunk](https://worktrunk.dev/) is a CLI." + ); + // Both surfaces, since each applies the rewrite on its own path. + assert!( + transform_docs_to_github("[install](/#install)").contains("https://worktrunk.dev/#install") + ); + assert!( + finalize_skill_content("[install](/#install)").contains("https://worktrunk.dev/#install") + ); + // Unchanged: a slash-less page segment is still not a site link. + assert_eq!(rewrite_site_links("[hooks](/hook)"), "[hooks](/hook)"); +} + /// Extract a config section from src/cli/mod.rs by marker pattern. fn extract_config_section(cli_mod_content: &str, pattern: &Regex, label: &str) -> String { pattern @@ -2093,14 +2148,7 @@ fn generate_skill_from_help(cmd: &str, project_root: &Path) -> Result String { - let content = SITE_LINK_PATTERN - .replace_all(content, |caps: ®ex::Captures| { - let text = caps.get(1).unwrap().as_str(); - let page = caps.get(2).unwrap().as_str(); - let anchor = caps.get(3).map_or("", |m| m.as_str()); - format!("[{text}](https://worktrunk.dev/{page}/{anchor})") - }) - .into_owned(); + let content = rewrite_site_links(content); // Installed skills don't have the site's root URL as a resolution base. assert_no_untransformed_site_links(&content, "skill content");