|
| 1 | +--- |
| 2 | +name: 'Fix Broken Links' |
| 3 | +description: 'Checks changed web files for broken hyperlinks and SEO anchor issues after each Copilot tool use.' |
| 4 | +tags: ['links', 'seo', 'html', 'markdown', 'post-tool-use'] |
| 5 | +--- |
| 6 | + |
| 7 | +# Fix Broken Links Hook |
| 8 | + |
| 9 | +Scans recently-changed web files for broken hyperlinks after each GitHub Copilot |
| 10 | +tool use. For each broken URL the hook tries common spelling variations, then hands |
| 11 | +the link to the Copilot CLI agent for suggested replacements, and presents an |
| 12 | +interactive fix menu. Generic anchor text (`click here`, `read more`, etc.) is |
| 13 | +flagged as an SEO issue. |
| 14 | + |
| 15 | +## Overview |
| 16 | + |
| 17 | +Broken links accumulate silently in web projects. Running on the `postToolUse` |
| 18 | +event, this hook checks the web files the agent just edited — and only those — |
| 19 | +right after each change, so you can fix, replace, or remove each broken link in |
| 20 | +the same terminal session. |
| 21 | + |
| 22 | +The hook has two modes: |
| 23 | + |
| 24 | +- **With file paths** (the edited files injected from the hook payload, or paths |
| 25 | + passed on the command line): it checks each link, looks up replacement |
| 26 | + candidates, and presents the interactive fix menu. |
| 27 | +- **With no file arguments**: it simply lists the broken links it finds — no |
| 28 | + replacement lookups and no prompts. |
| 29 | + |
| 30 | +## Features |
| 31 | + |
| 32 | +- **Self-contained core**: bash and PowerShell ports — no runtime to install (the optional agent |
| 33 | + hand-off reuses the Copilot CLI you already have) |
| 34 | +- **Edited-files scope**: as a `postToolUse` hook it only checks the files the agent just changed — |
| 35 | + never a full repo scan |
| 36 | +- **Format-agnostic link scan**: extracts every `http(s)` URL with `grep`, covering HTML, Markdown, |
| 37 | + JS/TS, JSON, CSS, SQL, and templates at once |
| 38 | +- **Automatic URL healing**: tries www, https, and trailing-slash variations |
| 39 | +- **Agent-assisted suggestions**: hands the broken link to the Copilot CLI agent (a lightweight, |
| 40 | + low-token `gpt-5-mini` prompt with no tools) for replacement candidates; if the CLI is missing or |
| 41 | + errors, it simply offers none |
| 42 | +- **SEO audit**: flags anchor text that is too generic to benefit search ranking |
| 43 | +- **Large-file guard**: prompts before checking files with more than 50 links |
| 44 | +- **Interactive fix menu**: replace with suggestion, enter custom URL, strip tag keeping text, or |
| 45 | + skip |
| 46 | +- **Standard tools only**: `curl`, `grep`, `sed` — present on any POSIX system |
| 47 | + |
| 48 | +## Installation |
| 49 | + |
| 50 | +1. Copy the hook folder to your repository: |
| 51 | + |
| 52 | + ```bash |
| 53 | + cp -r hooks/fix-broken-links .github/hooks/ |
| 54 | + ``` |
| 55 | + |
| 56 | +2. Make the script executable: |
| 57 | + |
| 58 | + ```bash |
| 59 | + chmod +x .github/hooks/fix-broken-links/link-fix.sh |
| 60 | + ``` |
| 61 | + |
| 62 | +3. Commit the hook configuration to your repository's default branch. |
| 63 | + |
| 64 | +## Configuration |
| 65 | + |
| 66 | +The hook is configured in `hooks.json` to run on the `postToolUse` event: |
| 67 | + |
| 68 | +```json |
| 69 | +{ |
| 70 | + "version": 1, |
| 71 | + "hooks": { |
| 72 | + "postToolUse": [ |
| 73 | + { |
| 74 | + "type": "command", |
| 75 | + "bash": ".github/hooks/fix-broken-links/link-fix.sh", |
| 76 | + "powershell": ".github/hooks/fix-broken-links/link-fix.ps1", |
| 77 | + "cwd": ".", |
| 78 | + "timeoutSec": 120 |
| 79 | + } |
| 80 | + ] |
| 81 | + } |
| 82 | +} |
| 83 | +``` |
| 84 | + |
| 85 | +## Supported Source Types |
| 86 | + |
| 87 | +Links are found by scanning each file for `http(s)://` URLs, so the same logic |
| 88 | +covers every format that embeds absolute URLs: |
| 89 | + |
| 90 | +| Source | Examples matched | |
| 91 | +| --- | --- | |
| 92 | +| HTML | `<a href>`, `<img src>`, `<script src>`, `<link href>`, `<iframe src>` | |
| 93 | +| Markdown | `[text](url)`, `[text][ref]`, bare `<url>` | |
| 94 | +| JS / TS / Vue / Svelte | `fetch()`, `XMLHttpRequest.open()`, jQuery, axios, `href:`/`url:` props | |
| 95 | +| JSON / JSONL | any string value that is an absolute URL | |
| 96 | +| CSS | `url(...)` | |
| 97 | +| SQL | URL literals in query strings | |
| 98 | +| Templates | Jinja2, ERB, EJS, Handlebars, Pug | |
| 99 | + |
| 100 | +The `d` (remove) action understands HTML `<a>` wrappers and Markdown `[text](url)` |
| 101 | +links specifically, keeping the visible text. Other source types support |
| 102 | +`r` (replace) and `c` (custom) via literal URL substitution. |
| 103 | + |
| 104 | +## Fix Options |
| 105 | + |
| 106 | +For each broken link: |
| 107 | + |
| 108 | +| Key | Action | |
| 109 | +| --- | --- | |
| 110 | +| `r` | Replace with the suggested URL (a working variation, or an agent-proposed alternative) | |
| 111 | +| `d` | Strip the link wrapper, keeping the visible text as plain text | |
| 112 | +| `c` | Enter a custom replacement URL | |
| 113 | +| `s` | Skip | |
| 114 | + |
| 115 | +## Example Output |
| 116 | + |
| 117 | +```text |
| 118 | + Checking 2 link(s) in docs/guide.md ... |
| 119 | + BROKEN (404) https://example.com/old-page |
| 120 | +
|
| 121 | +------------------------------------------------------------ |
| 122 | + SEO anchor issues (consider descriptive link text) |
| 123 | + docs/guide.md: <a href="https://example.com/old-page">click here</a> |
| 124 | +
|
| 125 | +============================================================ |
| 126 | + fix-broken-links report |
| 127 | +============================================================ |
| 128 | +
|
| 129 | + [1] docs/guide.md |
| 130 | + URL : https://example.com/old-page |
| 131 | + HTTP: 404 |
| 132 | +
|
| 133 | + r Replace -> https://example.com/docs/install |
| 134 | + 1 Replace -> https://example.com/docs/getting-started |
| 135 | + d Remove link, keep text |
| 136 | + c Custom replacement URL |
| 137 | + s Skip |
| 138 | + > r |
| 139 | + replaced |
| 140 | +
|
| 141 | + 1 file(s) updated: |
| 142 | + docs/guide.md |
| 143 | +``` |
| 144 | + |
| 145 | +With no file arguments (or when the edited file carries no checkable links) the |
| 146 | +hook stops after the broken-link list — the menu above is skipped. |
| 147 | + |
| 148 | +## Requirements |
| 149 | + |
| 150 | +- `curl` — HTTP status checks (the hook exits quietly if absent) |
| 151 | +- `grep`, `sed` — link extraction (standard on any POSIX system) |
| 152 | +- `jq` — required by the bash hook to parse the postToolUse JSON payload and discover edited files |
| 153 | +- Bash 4+ (for `link-fix.sh`); on Windows use Git Bash or WSL, or run the PowerShell 7+ port |
| 154 | + `link-fix.ps1` |
| 155 | +- `copilot` (GitHub Copilot CLI) — optional; powers the agent-suggested replacements. Without it, |
| 156 | + only verified spelling variations are offered |
| 157 | +- `git` is used for changed-file discovery; the hook falls back to a full repo scan without it |
| 158 | + |
| 159 | +## File Structure |
| 160 | + |
| 161 | +``` |
| 162 | +.github/hooks/fix-broken-links/ |
| 163 | +├── hooks.json GitHub Copilot hook configuration |
| 164 | +├── link-fix.sh Bash hook implementation |
| 165 | +├── link-fix.ps1 PowerShell 7+ port |
| 166 | +└── README.md This file |
| 167 | +``` |
| 168 | + |
| 169 | +## Limitations |
| 170 | + |
| 171 | +- Only checks absolute `http://` and `https://` URLs; relative paths require a running server |
| 172 | +- Dynamic links generated at runtime from database queries are not detectable from source alone |
| 173 | +- When `copilot` suggestions are enabled, broken URLs are sent to the Copilot service as prompt input |
| 174 | +- Agent-suggested replacements are model proposals and are not verified live; confirm each before |
| 175 | + accepting |
| 176 | +- The `d` (remove) action targets HTML and Markdown link syntax; bare URLs in code are best handled |
| 177 | + with `r` or `c` |
0 commit comments