docs: link the canonical home-page install anchor, not the deindexed duplicate

This commit is contained in:
worktrunk-bot
2026-09-03 06:53:26 +00:00
parent ce57d2d997
commit 36f201b36e
5 changed files with 70 additions and 22 deletions
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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.
+66 -18
View File
@@ -123,14 +123,46 @@ static RUST_RAW_STRING_PATTERN: LazyLock<Regex> = 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<Regex> =
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: &regex::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<Regex> =
@@ -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: &regex::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, St
/// Apply final transforms shared between command and non-command skill files:
/// Site-root links → full URLs, remove "See also", and collapse blank lines.
fn finalize_skill_content(content: &str) -> String {
let content = SITE_LINK_PATTERN
.replace_all(content, |caps: &regex::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");