# Website SEO Audit: Broken Links & Fix List (`zerahdata/website-seo-audit`) Actor

Crawl a website, verify every internal and external link, and get a prioritized, actionable list of SEO issues with fix instructions.

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

## Pricing

$3.00 / 1,000 results

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

## Website SEO Audit: Broken Links & Fix List

Crawl any website and get a **prioritized fix list**: every broken link with the exact page that links to it, every missing or duplicate title and meta description, missing H1s, images without alt text, redirect chains, robots.txt and sitemap problems, canonical errors and slow pages - each with **why it matters and how to fix it**. You also get a **0-100 site score** and, in monitoring mode, a **change report** against your previous run.

Pure HTTP crawler - fast, cheap, no browser.

### Why this actor

- **A fix list, not a data dump.** Issues are grouped per site and ranked by severity and impact, with concrete fix instructions and example URLs - ready to hand to a developer or a client.
- **Broken links with their source page.** Every internal and external link found is verified, and each broken one is reported together with the pages that link to it, so you know exactly where to edit.
- **Fewer false alarms.** Links answered with 401/403/429 (typical bot blocking) are reported as "unverified", not as broken.
- **Sitemap-aware.** Reads robots.txt and sitemap.xml (including sitemap indexes and gzip), finds sitemap URLs that redirect or error, and flags orphan pages that are in the sitemap but never linked.
- **Change tracking built in.** Schedule it weekly and get NEW_ISSUE, RESOLVED_ISSUE, NEW_BROKEN_LINK, FIXED_BROKEN_LINK and SCORE_CHANGED rows automatically.
- **Low, simple price.** You pay only per audited page. Issues, summaries and change rows are free.
- **Self-check report.** Every run writes a `RUN_REPORT` with pages audited, links checked, fill rates and warnings (for example when the page limit stopped the crawl early).

### What it checks

| Area | Checks |
|---|---|
| Technical | HTTP errors (4xx/5xx), redirect loops, redirect chains over 3 hops, non-HTTPS pages, mixed content, missing/broken/cross-host canonical, noindex pages, missing `lang`, missing viewport, robots.txt missing or blocking everything, sitemap missing or invalid, sitemap URLs that don't return 200, orphan pages |
| On-page | Missing/empty/too long/too short/duplicate titles, missing/too long/too short/duplicate meta descriptions, missing or multiple H1, images missing alt, thin content, missing Open Graph, missing structured data |
| Links | Broken internal links, broken external links (with linking pages) |
| Performance | Slow pages (configurable threshold), large HTML documents |

### Use cases

- SEO agencies and freelancers: a client-ready audit with priorities in minutes.
- Site owners: find broken links and missing tags before Google does.
- Developers: run after every release and compare to the previous run.
- Monitoring: schedule weekly and get only what changed.

### How to use

1. Add one or more websites in **Start URLs** (or bare domains in **Domains**).
2. Set **Max pages per site** (default 50).
3. Optional: enable **Track changes between runs** and give the monitor a name.
4. Run, then open the **Fix list** view.

#### Example input

```json
{
  "startUrls": [{ "url": "https://books.toscrape.com" }],
  "domains": ["example.com"],
  "maxPagesPerSite": 100,
  "checkExternalLinks": true,
  "trackChanges": true,
  "monitorName": "weekly-client-audit"
}
```

### Output

Four row types in one dataset (each has its own view):

**`issue` - the fix list (free)**

```json
{
  "type": "issue",
  "siteDomain": "books.toscrape.com",
  "rank": 1,
  "issueCode": "mixed-content",
  "severity": "high",
  "priorityScore": 180,
  "title": "Mixed content: HTTP assets on an HTTPS page",
  "howToFix": "Change asset URLs to https:// ...",
  "affectedCount": 30,
  "affectedUrls": ["https://books.toscrape.com/", "..."],
  "examples": [{ "url": "https://books.toscrape.com/", "examples": ["http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"] }]
}
```

**`siteSummary` - one per site (free):** score 0-100, grade, sub-scores (technical, on-page, links, performance), issues by severity, top 5 fixes, average response time, slowest pages, broken/unverified link counts, redirect count, robots.txt and sitemap status.

**`page` - one per audited page (billed):** URL, final URL, status code, redirect chain, response time, title, meta description, H1, canonical, robots meta, word count, internal/external link counts, images without alt, issue codes, crawl depth.

**`change` - monitoring mode only (free):** NEW_ISSUE, RESOLVED_ISSUE, SCORE_CHANGED (old/new), NEW_BROKEN_LINK, FIXED_BROKEN_LINK, PAGES_CHANGED.

#### Score formula

Each page starts at 100 in four categories. Every issue deducts points from its category (critical 25, high 15, medium 8, low 3, floor 0). Category score = average across pages (site-wide robots/sitemap issues are deducted from technical). Overall score = mean of the four categories.

### Pricing

**$3.00 per 1,000 audited pages** ($0.003 per page). Issue rows, site summaries and change rows are free, and **error rows are never charged**. The page charge applies in every output mode. The run stops cleanly when your spending limit is reached.

### FAQ

**Does it render JavaScript?** No. It audits the HTML the server returns, which is what most search engine checks start from. Sites that build all content in the browser will show as thin content.

**Will it overload my site?** Concurrency is configurable (default 10) and robots.txt is respected by default.

**Why is a link "unverified"?** The target answered 401, 403 or 429, which usually means it blocks automated requests, not that it is dead.

**What happens if my page limit is smaller than the site?** The audit covers the pages crawled, the summary shows `crawlComplete: false`, and the orphan-page check is skipped to avoid false positives.

### Limits

- Up to 5,000 pages per site per run.
- External link checks are capped by `maxExternalLinksChecked` (default 300) to keep runs fast.
- Sites behind strong bot protection may return errors; try enabling a proxy.

# Actor input Schema

## `startUrls` (type: `array`):

Websites to audit. The crawler starts here, follows internal links, and also reads each site's sitemap.xml (via robots.txt) to find pages that aren't linked from the homepage. Accepts objects ({"url": "https://..."}) or plain URL strings. Note: when calling the Apify API directly (not the Console UI), this field's editor rejects bare domains like 'example.com' - use the 'Domains' field below instead if you only have bare domains.

## `domains` (type: `array`):

Optional additional sites, merged with 'Start URLs' above. Unlike 'Start URLs', this field accepts bare domains (e.g. example.com) with no 'https://' prefix required - useful when calling the Apify API directly, since the 'Start URLs' editor rejects bare domains there. Each entry is audited as https://<domain>/.

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

Maximum number of pages crawled and audited per site (the site a start URL lands on). This is also the number of billable 'page' rows a site can produce. Start URLs on the same site share this budget.

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

Maximum number of link hops from a start URL. 0 audits only the start URLs. Sitemap-discovered pages are not limited by depth. Set high (e.g. 10) for a thorough crawl, low (e.g. 1-2) to stay close to the homepage.

## `sameDomainOnly` (type: `boolean`):

If enabled (default), the crawler only follows and audits links on the same registrable domain as the start URL (see 'Include subdomains' below). Links leaving the domain are still checked for broken status if 'Check external links' is enabled, but are never crawled as pages.

## `includeSubdomains` (type: `boolean`):

When 'Stay on the same domain' is enabled, also crawl and audit pages on subdomains of the start URL's domain (e.g. blog.example.com and shop.example.com when starting from example.com).

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

Verify links that point to other domains (lightweight GET request with a short timeout; 401/403/429 answers are reported as unverified, not broken, because they usually mean the site blocks bots). Results are cached so the same external URL is never checked twice in a run. Disable to skip external checks entirely and reduce run time.

## `maxExternalLinksChecked` (type: `integer`):

Caps the total number of unique external URLs verified across the whole run, to bound cost and duration on sites with very many outbound links. Once the cap is reached, remaining external links are left unchecked (not reported as broken or OK).

## `respectRobotsTxt` (type: `boolean`):

Skip URLs disallowed for the configured user agent by the site's robots.txt. Recommended: keep enabled for ethical crawling. The actor still fetches and reports on robots.txt itself (missing / blocking everything) regardless of this setting.

## `userAgent` (type: `string`):

Custom User-Agent string sent on every request, and the agent name used to evaluate robots.txt rules when 'Respect robots.txt' is enabled. Leave at the default unless a site specifically blocks it.

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

Maximum number of parallel HTTP requests (page fetches and link checks) per site. Lower it on slow or rate-limiting sites.

## `slowPageThresholdMs` (type: `integer`):

Pages whose total response time exceeds this threshold are flagged with the 'slow-page' issue and counted in the site summary's slowest pages.

## `excludeUrlPatterns` (type: `array`):

Substrings or regex patterns (wrap in slashes, e.g. "/\\/page\\/\d+/") of URLs to skip crawling. Useful for CMS paths like /tag/, /author/, /page/. Matching URLs are never fetched as pages, but may still appear as link-check targets.

## `proxyConfiguration` (type: `object`):

Optional proxy configuration. Using Apify Proxy is recommended when auditing many sites or sites that rate-limit datacenter IPs.

## `trackChanges` (type: `boolean`):

If enabled, the actor saves each site's issues, broken links and score in a named key-value store, and compares them to the previous run with the same 'Monitor name'. Differences are output as extra 'change' rows (new/resolved issues, score changes, new/fixed broken links, pages added/removed). The first run with a given monitor name only saves a baseline and reports no changes.

## `monitorName` (type: `string`):

Name identifying this monitoring setup, used to pick which saved snapshot to compare against (and update) on each run. Only relevant when 'Track changes between runs' is enabled. Leave empty to auto-derive one from the start URLs/domains - but set a fixed value if you plan to change the list of sites between runs and still want continuous tracking.

## `outputMode` (type: `string`):

'All' outputs page rows plus issue, summary and change rows. 'Issues only' suppresses page rows and keeps the fix list, summary and change rows - useful when you only care about what to fix. 'Summary only' keeps just the per-site summary (and change rows). Pricing is per audited page in every mode.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://books.toscrape.com"
    }
  ],
  "domains": [],
  "maxPagesPerSite": 50,
  "maxDepth": 10,
  "sameDomainOnly": true,
  "includeSubdomains": false,
  "checkExternalLinks": true,
  "maxExternalLinksChecked": 300,
  "respectRobotsTxt": true,
  "userAgent": "Mozilla/5.0 (compatible; WebsiteSEOAuditBot/1.0; +https://apify.com)",
  "concurrency": 10,
  "slowPageThresholdMs": 2000,
  "excludeUrlPatterns": [],
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "trackChanges": false,
  "monitorName": "",
  "outputMode": "all"
}
```

# Actor output Schema

## `overview` (type: `string`):

All dataset rows: audited pages (type 'page', billed), grouped issues with fix instructions (type 'issue'), per-site summaries (type 'siteSummary') and change rows (type 'change').

## `issues` (type: `string`):

One row per issue code per site, with severity, priority score, why it matters, how to fix it, and the affected URLs - a ready-made, prioritized fix list. Never charged.

## `siteSummaries` (type: `string`):

One row per audited site: overall 0-100 score with sub-scores, issue counts by severity, top fixes, and performance stats. Never charged.

## `runReport` (type: `string`):

A RUN_REPORT record written to the default key-value store at the end of every run: inputs processed, row counts by type, errors by code, warnings, and a validation pass confirming required fields were present.

# 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 = {
    "startUrls": [
        {
            "url": "https://books.toscrape.com"
        }
    ],
    "domains": [],
    "excludeUrlPatterns": [],
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("zerahdata/website-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 = {
    "startUrls": [{ "url": "https://books.toscrape.com" }],
    "domains": [],
    "excludeUrlPatterns": [],
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("zerahdata/website-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 '{
  "startUrls": [
    {
      "url": "https://books.toscrape.com"
    }
  ],
  "domains": [],
  "excludeUrlPatterns": [],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call zerahdata/website-seo-audit --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,zerahdata/website-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/zj4Aw96Gf9Vu6fHd0/builds/SaQvSsNxOeFNP2XKc/openapi.json
