# Broken Link Checker & On-Page SEO Audit - 404s, Titles, Meta (`psx/broken-link-seo-audit`) Actor

Crawl sites, find broken internal/external links and audit titles, meta, H1, canonical, alt text, noindex. Per-page score and site summary.

- **URL**: https://apify.com/psx/broken-link-seo-audit.md
- **Developed by:** [psx](https://apify.com/psx) (community)
- **Categories:** SEO tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Broken Link Checker + On-Page SEO Audit - find 404s, duplicate titles, missing meta tags

Crawl a website (politely, HTTP only, no browser) and get, for every page: **broken internal and external links** (404/410/5xx/DNS/timeouts) and a **basic on-page SEO check**, plus a **site summary** with duplicate titles/descriptions. Fast and cheap, built for agencies, SEO freelancers, site owners and AI agents.

### What is checked

- **Links**: every `<a href>` on each crawled page (HEAD, GET fallback, redirects followed). 401/403/429 are reported as "blocked", not broken. Links disallowed by robots.txt are skipped, not reported.
- **On-page SEO**: title (missing/too long/short), meta description, H1 (missing/multiple), canonical, `lang`, viewport, `noindex` (meta and X-Robots-Tag), images without `alt`, thin content, Open Graph tags, mixed content on https pages, HTTP errors and redirects, structured data and hreflang presence.
- **Site level**: average score, pages with broken links, duplicate titles and meta descriptions, issue counts.

### Input

| Field | Default | Description |
|---|---|---|
| `urls` | required | Up to 100 start URLs or domains. |
| `maxPagesPerSite` | `20` | Pages audited per site (max 200). |
| `maxDepth` | `2` | Link depth from the start page. |
| `checkExternalLinks` | `true` | Also test links to other sites. |
| `maxLinksPerPage` | `100` | Distinct links tested per page. |
| `delayMs` | `500` | Spacing between requests to one host (max 2 in flight). |
| `concurrency` | `3` | Sites crawled in parallel. |
| `respectRobots` | `true` | Obey robots.txt for crawling and link testing. |

```json
{ "urls": ["https://example.com"], "maxPagesPerSite": 50, "maxDepth": 2 }
```

### Output

Items with `"type": "page"` (one per crawled page) and one `"type": "site-summary"` per site.

```json
{ "type": "page", "url": "https://www.w3.org/WAI/demos/bad/before/reports/home.html", "status": 200, "score": 58,
  "issues": [{ "severity": "high", "id": "broken-links", "message": "1 broken link(s)" }, { "severity": "low", "id": "og-missing", "message": "..." }],
  "meta": { "title": "...", "titleLength": 41, "wordCount": 320, "imagesWithoutAlt": 0 },
  "brokenLinks": [{ "url": "https://www.w3.org/WAI/WCAG20/quickref/G153", "status": 404, "anchorText": "Technique G153", "internal": true }] }
```

```json
{ "type": "site-summary", "siteOrigin": "https://www.w3.org", "pagesAudited": 5, "averageScore": 77,
  "brokenLinkOccurrences": 1, "duplicateTitles": [], "issueCounts": { "broken-links": 1, "viewport-missing": 3 } }
```

The page score starts at 100 and subtracts 20/8/3 per high/medium/low issue (a ranking heuristic).

### Pricing (pay per event)

| Event | When charged | Price |
|---|---|---|
| `apify-actor-start` | once per run | $0.005 |
| `page-audited` | each page crawled and analysed (its links included) | $0.004 (= $4 per 1,000 pages) |

**Why this price:** simple link checkers in the Store cost $0.25-$5 per 1,000 results, and SEO crawlers more. Here one billed page includes up to 100 tested links plus a full on-page SEO check and a site summary. Example: a 200-page site = $0.80; 50 client sites x 50 pages = $10.

### Politeness and safety

- Identifies as `LinkSeoAudit`; obeys robots.txt; max 2 requests in flight per host with `delayMs` spacing; pages of one site are fetched sequentially; hard caps on pages and links.
- Only http/https; localhost, internal hostnames and private/reserved IPs are refused (connect time and every redirect hop).
- Does not execute JavaScript: client-rendered content is not seen (use a rendering crawler for SPAs). Public pages only, no logins.

### FAQ

**Does it crawl JavaScript-rendered sites?**\
No. It is HTTP-only for speed and cost. Client-rendered content is not seen.

**Are 403 and 429 responses counted as broken?**\
No. 401/403/429 are reported as blocked, not broken, to avoid false alarms.

**Can it check external links?**\
Yes, `checkExternalLinks` (default true) tests outbound links too.

**How many pages per site?**\
Up to 200 pages per site and 100 start URLs per run.

**Will it overload my server?**\
No. It obeys robots.txt, keeps at most 2 requests in flight per host and spaces them with `delayMs`.

### Part of the Site Audit Suite

Run the same list of domains through every audit:

- [Website Tech Stack Detector](https://apify.com/psx/tech-stack-detector)
- [Website Accessibility Audit (WCAG 2.2 + EAA)](https://apify.com/psx/accessibility-eaa-audit)
- [Security Headers + TLS Audit](https://apify.com/psx/security-headers-tls-audit)
- llms.txt Generator and Sitemap + robots.txt Analyzer (coming soon)

Tip: pipe one domain list into several actors with an Apify Task or the API and join the results by `url`.

### Develop

```bash
npm install && npm test
mkdir -p storage/key_value_stores/default
echo '{"urls":["https://example.com"],"maxPagesPerSite":5}' > storage/key_value_stores/default/INPUT.json
npm start          # results in storage/datasets/default/
docker build -t broken-link-seo-audit .
```

Deploy (needs Apify account): `npx apify-cli login && npx apify-cli push`.

### Verified (2026-10-05, local)

w3.org BAD demo (5 pages, ~190 links, found a real 404), example.com, httpbin.org. Docker: `docker run --user 996 -v <dir>:/usr/src/app/storage broken-link-seo-audit`.

# Actor input Schema

## `urls` (type: `array`):

Sites or pages to crawl (up to 100). Bare domains are accepted. Only pages on the same site as each start URL are crawled.

## `maxPagesPerSite` (type: `integer`):

Pages audited per start URL (each page is billed).

## `maxDepth` (type: `integer`):

Link depth from the start URL (0 = only the start page).

## `checkExternalLinks` (type: `boolean`):

Also test links to other sites (HEAD request, GET fallback). Off = internal links only.

## `maxLinksPerPage` (type: `integer`):

Cap on distinct links tested on each page.

## `delayMs` (type: `integer`):

Politeness spacing; at most 2 requests in flight per host.

## `concurrency` (type: `integer`):

Number of start URLs crawled at the same time.

## `respectRobots` (type: `boolean`):

Do not crawl or test URLs disallowed for this bot (such links are reported as skipped, not broken).

## Actor input object example

```json
{
  "urls": [
    "https://example.com"
  ],
  "maxPagesPerSite": 20,
  "maxDepth": 2,
  "checkExternalLinks": true,
  "maxLinksPerPage": 100,
  "delayMs": 500,
  "concurrency": 3,
  "respectRobots": true
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "urls": [
        "https://example.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("psx/broken-link-seo-audit").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "urls": ["https://example.com"] }

# Run the Actor and wait for it to finish
run = client.actor("psx/broken-link-seo-audit").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "urls": [
    "https://example.com"
  ]
}' |
apify call psx/broken-link-seo-audit --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,psx/broken-link-seo-audit"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/2sdCR0TMhEOZQXmwS/builds/cZ22XGjOMFWzK1l7W/openapi.json
