agentsclimarketplace

Link checker

Skill ncklrs/claude-chrome-user-testing/skills/link-checker

Claude Code plugin for persona-based user testing. Simulates Boomers, Millennials, Gen Z, and Gen Alpha users to find UX issues before real users do.From the repository description

Install
npx -y skills add ncklrs/claude-chrome-user-testing --skill link-checker

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 19 stars19 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

5.8 KB, as published. Nobody here has run it

Link Checker Skill

Find and validate links on web pages. Reports broken links, redirects, and other issues.

When to Use

  • Before releases to catch broken links
  • After content updates
  • During user testing (with --check-links flag)
  • Regular site maintenance

Link Extraction

Using Browser Snapshot

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)
];

Link Types to Identify

TypePatternAction
InternalSame domainCheck via HEAD
ExternalDifferent domainCheck via HEAD (unless excluded)
Anchor#sectionVerify element exists
mailto:mailto:*Validate format
tel:tel:*Validate format
javascript:javascript:*Flag as warning
data:data:*Skip

Extracting via browser_evaluate

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;
}

Link Validation

HEAD Request Method

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 Interpretation

Status CodeCategoryAction
200SuccessMark as working
201-299SuccessMark as working
301Permanent RedirectWarn - update link
302Temporary RedirectNote redirect
303, 307, 308RedirectsNote redirect
400Bad RequestError
401, 403Auth RequiredWarning
404Not FoundError - broken link
405Method Not AllowedRetry with GET
429Rate LimitedWait and retry
500-599Server ErrorError
TimeoutNo ResponseError

Handling Redirects

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 };
};

Depth Crawling

Algorithm

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

Performance Considerations

DepthTypical LinksTime
120-10010-30s
2100-5001-5 min
3500-2000+5-20 min

Tips:

  • Default to depth 1
  • Use --internal-only to reduce scope
  • Parallelize requests where possible

Report Generation

Data Structure

const 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 },
  }
};

Report Sections

  1. Summary: Quick overview of link health
  2. Broken Links: 404s and errors (highest priority)
  3. Redirects: Links that should be updated
  4. Warnings: javascript: links, slow responses, etc.
  5. By Page: Where issues were found

Integration with User Testing

When --check-links is added to /user-test:

During Test

  1. As persona navigates, collect all unique links seen
  2. Don't interrupt persona flow for link checking
  3. Store links for post-test validation

After Test

  1. Validate all collected links
  2. Add "Link Health" section to report
  3. Note any broken links encountered during tasks

Report Section

## 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.

Best Practices

  1. Check internal links first: External sites may block or rate limit
  2. Respect robots.txt: For external sites
  3. Handle timeouts gracefully: Some servers are slow
  4. Track over time: Links break gradually
  5. Fix redirects: Replace with final destination
  6. Don't hammer servers: Add delays between requests

Keep looking

Skills are one crate of 627,801. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.