Help us improve
Share bugs, ideas, or general feedback.
From user-testing-agent
Finds and validates links on web pages via browser evaluation and HEAD requests. Reports broken links, redirects, auth issues, timeouts. Use before releases, after updates, site maintenance.
npx claudepluginhub ncklrs/claude-chrome-user-testing --plugin user-testing-agentHow this skill is triggered — by the user, by Claude, or both
Slash command
/user-testing-agent:link-checkerThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Find and validate links on web pages. Reports broken links, redirects, and other issues.
Scans websites or codebases for broken internal/external links, 404s, redirect chains, and mixed content. Outputs a grouped report with fix suggestions.
Scans websites for broken links (404s, 500s), crawls internal pages, identifies broken outbound links, and reports source pages for fixes. Useful for site health audits.
Playwright browser automation: navigates URLs, captures screenshots and accessibility snapshots, interacts with UI elements (click, type, fill form), and reports findings with visual evidence.
Share bugs, ideas, or general feedback.
Find and validate links on web pages. Reports broken links, redirects, and other issues.
--check-links flag)Use browser_snapshot to get page accessibility tree, then extract links:
// Links to extract
const linkSelectors = [
'a[href]', // Standard links
'area[href]', // Image map links
'link[href]', // Stylesheet/resource links (optional)
];
| Type | Pattern | Action |
|---|---|---|
| Internal | Same domain | Check via HEAD |
| External | Different domain | Check via HEAD (unless excluded) |
| Anchor | #section | Verify element exists |
| mailto: | mailto:* | Validate format |
| tel: | tel:* | Validate format |
| javascript: | javascript:* | Flag as warning |
| data: | data:* | Skip |
async (page) => {
const links = await page.evaluate(() => {
const anchors = document.querySelectorAll('a[href]');
return Array.from(anchors).map(a => ({
href: a.href,
text: a.textContent.trim().slice(0, 50),
location: a.closest('[id]')?.id || 'page'
}));
});
return links;
}
For each link, make a HEAD request to check status:
async (page) => {
const response = await page.request.head(url, {
timeout: 10000,
ignoreHTTPSErrors: false
});
return {
status: response.status(),
headers: response.headers()
};
}
| Status Code | Category | Action |
|---|---|---|
| 200 | Success | Mark as working |
| 201-299 | Success | Mark as working |
| 301 | Permanent Redirect | Warn - update link |
| 302 | Temporary Redirect | Note redirect |
| 303, 307, 308 | Redirects | Note redirect |
| 400 | Bad Request | Error |
| 401, 403 | Auth Required | Warning |
| 404 | Not Found | Error - broken link |
| 405 | Method Not Allowed | Retry with GET |
| 429 | Rate Limited | Wait and retry |
| 500-599 | Server Error | Error |
| Timeout | No Response | Error |
Track redirect chains:
const checkLink = async (url) => {
const redirects = [];
let currentUrl = url;
let maxRedirects = 5;
while (maxRedirects > 0) {
const response = await fetch(currentUrl, {
method: 'HEAD',
redirect: 'manual'
});
if (response.status >= 300 && response.status < 400) {
const location = response.headers.get('location');
redirects.push({ from: currentUrl, to: location, status: response.status });
currentUrl = new URL(location, currentUrl).href;
maxRedirects--;
} else {
return { finalStatus: response.status, redirects };
}
}
return { error: 'Too many redirects', redirects };
};
function crawl(startUrl, maxDepth):
visited = Set()
toVisit = [(startUrl, 0)] // (url, depth)
results = []
while toVisit not empty:
(url, depth) = toVisit.pop()
if url in visited or depth > maxDepth:
continue
visited.add(url)
pageLinks = extractLinks(url)
results.extend(validateLinks(pageLinks))
if depth < maxDepth:
for link in pageLinks:
if isInternal(link) and link not in visited:
toVisit.append((link, depth + 1))
return results
| Depth | Typical Links | Time |
|---|---|---|
| 1 | 20-100 | 10-30s |
| 2 | 100-500 | 1-5 min |
| 3 | 500-2000+ | 5-20 min |
Tips:
--internal-only to reduce scopeconst linkReport = {
url: 'https://example.com',
checkedAt: '2025-01-06T14:30:00Z',
summary: {
total: 47,
working: 44,
broken: 2,
redirects: 3,
skipped: 1
},
broken: [
{ url: '/old-page', foundOn: '/', status: 404 },
],
redirects: [
{ url: '/blog', redirectsTo: '/news', status: 301 },
],
warnings: [
{ type: 'javascript-link', url: 'javascript:void(0)', foundOn: '/nav' },
],
byPage: {
'/': { checked: 15, broken: 1 },
'/about': { checked: 8, broken: 1 },
}
};
When --check-links is added to /user-test:
## Link Health
### Summary
- **Links Encountered**: 32
- **Working**: 30 (94%)
- **Broken**: 2 (6%)
### Broken Links Found During Testing
| Link | Encountered During | Status |
|------|-------------------|--------|
| /products/sale | Browsing products | 404 |
| /help/faq | Looking for help | 404 |
### Impact on User Experience
The persona encountered 2 broken links during their journey,
which could cause confusion and frustration.