Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
cowork-bot: fix link resolution for ../ intermediates and absolute pa…
…ths; update article count
  • Loading branch information
DevForge Engineer
DevForge Engineer committed Jun 23, 2026
commit 42783fc2c5d1a18266ad1c1b8119a35b95b6ced8
28 changes: 18 additions & 10 deletions .hermes/linkcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,25 @@ def build_actual_pages(all_files: set[str]) -> set[str]:

def resolve_link(link: str, source_dir: str) -> str:
"""Resolve a relative href to an absolute site-relative path."""
if link.startswith("../"):
parts = source_dir.split("/") if source_dir else []
up_count = link.count("../")
resolved_parts = parts[:-up_count] if len(parts) >= up_count else []
remaining = link[3 * up_count :]
resolved = "/".join(resolved_parts + [remaining]) if resolved_parts else remaining
elif link.startswith("./"):
resolved = (source_dir + "/" + link[2:]) if source_dir else link[2:]
if link.startswith("http://") or link.startswith("https://") or link.startswith("#"):
return link
if link.startswith("/"):
return link.lstrip("/")

if source_dir:
path = source_dir + "/" + link
else:
resolved = (source_dir + "/" + link) if source_dir else link
return resolved.replace("//", "/")
path = link

normalized = os.path.normpath(path).replace(os.sep, "/")

# Cap at site root: strip any leading ../ that goes above root
while normalized.startswith("../"):
normalized = normalized[3:]
if normalized.startswith("/"):
normalized = normalized[1:]

return normalized


def check_links(root_dir: str, verbose: bool = False) -> int:
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![Deploy](https://img.shields.io/badge/deploy-GitHub%20Pages-brightgreen)](https://coding-dev-tools.github.io/devforge/)
[![Stars](https://img.shields.io/github/stars/Coding-Dev-Tools/devforge?style=social)](https://github.com/Coding-Dev-Tools/devforge/stargazers)

Landing page, documentation, pricing, alternatives, and blog for the **DevForge CLI tool suite** — 11 developer CLI tools for API contracts, SQL, infrastructure, config drift, and more. **60 articles and growing.**
Landing page, documentation, pricing, alternatives, and blog for the **DevForge CLI tool suite** — 11 developer CLI tools for API contracts, SQL, infrastructure, config drift, and more. **70 articles and growing.**

**Live site:** [coding-dev-tools.github.io/devforge/](https://coding-dev-tools.github.io/devforge/)

Expand All @@ -14,7 +14,7 @@ Landing page, documentation, pricing, alternatives, and blog for the **DevForge
| [Home](https://coding-dev-tools.github.io/devforge/) | Hero, stats, feature cards, CTA |
| [Pricing](https://coding-dev-tools.github.io/devforge/pricing.html) | Free / Pro / Team / Enterprise tiers |
| [Alternatives](https://coding-dev-tools.github.io/devforge/alternatives.html) | 11 comparison tables vs competitors |
| [Blog](https://coding-dev-tools.github.io/devforge/blog.html) | 60+ articles and tutorials |
| [Blog](https://coding-dev-tools.github.io/devforge/blog.html) | 70+ articles and tutorials |
| [Docs](https://coding-dev-tools.github.io/devforge/docs.html) | Tool documentation hub |
| [Quickstart](https://coding-dev-tools.github.io/devforge/quickstart.html) | Get started in 60 seconds |
| [FAQ](https://coding-dev-tools.github.io/devforge/#faq) | Common questions |
Expand Down
18 changes: 18 additions & 0 deletions tests/test_linkcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,24 @@ def test_resolve_link_double_slash():
assert result == "blog/page.html"


def test_resolve_link_absolute_path():
"""Absolute path /about.html strips leading slash."""
result = resolve_link("/about.html", "blog")
assert result == "about.html"


def test_resolve_link_up_with_intermediate_segments():
"""../foo/../bar.html correctly cancels the intermediate segment."""
result = resolve_link("../foo/../bar.html", "blog/2024")
assert result == "blog/bar.html"


def test_resolve_link_up_capped_at_root():
"""../../ from a shallow dir is capped at site root."""
result = resolve_link("../../page.html", "blog")
assert result == "page.html"


# ---- CLI argument parsing ----


Expand Down