# SEO Audit Crawler: On-Page Issues, Broken Links & Score (`blond_terrarium/seo-page-audit-crawler`) Actor

Crawl any website and audit every page for SEO: title, meta description, H1, canonical, noindex, hreflang, Open Graph, schema, word count, missing alt, broken links, plus issues and a 0-100 score. Fast HTTP, no browser. Pay only per page audited.

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

## Pricing

from $5.00 / 1,000 page auditeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## SEO Audit Crawler: On-Page Issues, Broken Links & Score per Page

Crawl any website and get a **full on-page SEO audit for every page**: title and meta description (with lengths), H1/H2 headings, canonical, noindex (meta robots and `X-Robots-Tag`), hreflang, Open Graph and Twitter cards, JSON-LD schema types, word count, images missing alt text, internal/external links, **broken links**, redirect chains, response time and page size. Each page gets a ready-to-use **issues list with severity** and a simple **0-100 score**. You also get a free **site summary row** with average score, issue totals, **duplicate titles and descriptions**, and every broken link with the page it was found on.

It uses fast HTTP requests instead of a headless browser, so it's quick and cheap. In our tests, 60 pages from 3 sites were audited in 8 seconds without link checks, or about 4 minutes when it also checked 1,550 unique links.

**You only pay for pages that were actually audited.** 404s, timeouts, anti-bot pages, non-HTML files, robots.txt-blocked URLs and the summary rows are free.

### What you can use it for

- **SEO audits** for clients or your own sites, exported to CSV/Excel/Google Sheets
- **Broken link** and redirect clean-up
- Finding **missing or duplicate titles and meta descriptions**, missing H1s, thin pages, images without alt text
- Checking **noindex / canonical / hreflang** setup after a migration or redesign
- Scheduled monitoring: run weekly and compare `score` and `issues` over time
- Feeding SEO data to AI agents (works via the Apify MCP server)

### What it checks (issue codes)

| Severity | Code | When |
|---|---|---|
| high | `MISSING_TITLE` | No `<title>` |
| high | `NOINDEX` | `noindex` in meta robots or `X-Robots-Tag` |
| high | `BROKEN_INTERNAL_LINKS` | Links to your own site return 404/410/5xx, have a redirect loop, or the domain doesn't exist |
| medium | `TITLE_TOO_LONG` | Title > 60 characters (configurable) |
| medium | `MISSING_META_DESCRIPTION` | No meta description |
| medium | `MISSING_H1` / `EMPTY_H1` | No H1, or it's empty |
| medium | `THIN_CONTENT` | Main content < 300 words (configurable) |
| medium | `MULTIPLE_CANONICALS` | More than one different canonical URL |
| medium | `MISSING_VIEWPORT` | No meta viewport (mobile) |
| medium | `BROKEN_EXTERNAL_LINKS` | Links to other sites are dead |
| medium | `NOT_HTTPS` | Page served over plain HTTP |
| medium/low | `IMAGES_MISSING_ALT` | `<img>` without an `alt` attribute (medium at 5+) |
| low | `TITLE_TOO_SHORT`, `META_DESCRIPTION_TOO_LONG` (> 160), `META_DESCRIPTION_TOO_SHORT` | Length checks (configurable) |
| low | `MULTIPLE_H1`, `MULTIPLE_TITLES`, `MULTIPLE_META_DESCRIPTIONS` | Duplicated tags |
| low | `MISSING_CANONICAL`, `MISSING_LANG`, `MISSING_OPEN_GRAPH`, `INVALID_JSON_LD` | Missing/invalid tags |
| low | `REDIRECTED`, `SLOW_RESPONSE` (TTFB > 2 s), `LARGE_HTML` (> 2 MB), `NOFOLLOW_PAGE`, `NO_INTERNAL_LINKS` | Technical |
| info | `CANONICALIZED`, `NO_STRUCTURED_DATA` | For your information, no score penalty |

**Score** = 100 − 20 per high, 10 per medium, 4 per low issue (minimum 0).

**Broken links:** every link on an audited page is checked once per run (HEAD, then a GET to confirm before anything is reported). Responses like 401/403/429/999 and social networks blocking bots (Facebook, LinkedIn, Instagram, X…) are counted as `linksRestricted`, **not** broken, so you don't get false alarms. Turn link checking off (`checkBrokenLinks: false`) for the fastest crawl, or limit it to internal links only.

### Input example

```json
{
  "startUrls": ["https://www.example.com/"],
  "maxCrawlDepth": 3,
  "maxPages": 500,
  "crawlScope": "sameHostname",
  "useSitemaps": true,
  "checkBrokenLinks": true,
  "checkExternalLinks": true
}
```

### Output example (one row per audited page)

```json
{
  "rowType": "page",
  "url": "https://react.dev/versions",
  "statusCode": 200,
  "finalUrl": "https://react.dev/versions",
  "redirectChain": [],
  "score": 90,
  "issueCount": 1,
  "issues": [
    { "severity": "medium", "code": "MISSING_META_DESCRIPTION", "message": "Page has no meta description." },
    { "severity": "info", "code": "NO_STRUCTURED_DATA", "message": "No JSON-LD or microdata structured data found." }
  ],
  "title": "React Versions – React",
  "titleLength": 22,
  "metaDescription": null,
  "metaDescriptionLength": 0,
  "h1": ["React Versions"],
  "h1Count": 1,
  "h2Count": 4,
  "canonicalUrl": "https://react.dev/versions",
  "isCanonical": true,
  "robotsMeta": null,
  "xRobotsTag": null,
  "indexable": true,
  "hreflang": [],
  "openGraph": { "title": "React Versions – React", "description": "The library for web and native user interfaces", "image": "https://react.dev/images/og/versions.png", "url": "https://react.dev/versions", "type": "website" },
  "twitterCard": { "card": "summary_large_image", "title": "React Versions – React", "description": "The library for web and native user interfaces", "image": "https://react.dev/images/og/versions.png" },
  "schemaTypes": [],
  "wordCount": 1394,
  "contentWordCount": 1286,
  "imageCount": 1,
  "imagesMissingAltCount": 0,
  "internalLinkCount": 38,
  "externalLinkCount": 105,
  "brokenLinkCount": 0,
  "brokenLinks": [],
  "linksChecked": 143,
  "linksRestricted": 1,
  "linksUnchecked": 0,
  "responseTimeMs": 151,
  "pageSizeBytes": 234309,
  "charged": true,
  "crawledAt": "2026-10-22T13:30:00.000Z"
}
```

A page with dead links lists them like this:

```json
"brokenLinks": [
  { "url": "https://httpbin.org/status/404", "statusCode": 404, "error": "HTTP 404", "internal": true },
  { "url": "https://books.toscrape.com/missing-page.html", "statusCode": 404, "error": "HTTP 404", "internal": false }
]
```

Free rows: `rowType: "error"` or `"skipped"` (404, 5xx, timeout, anti-bot page, non-HTML, robots.txt) carry `statusCode`, `errorCode` and `error`. One free `rowType: "summary"` row per site has `averageScore`, `issueCounts`, `duplicateTitles`, `duplicateDescriptions`, `brokenLinks` (with `foundOn`), `failedUrls` and `lowestScoringPages`. The `RUN_SUMMARY` record in the key-value store has the run totals.

### Pricing (pay per event)

You pay per **page audited**: an HTML page that returned HTTP 2xx and got the full audit, including the link check if it's on.

| Apify plan | Price per 1,000 pages |
|---|---|
| Free | $8.00 |
| Bronze (Starter) | $7.00 |
| Silver (Scale) | $6.00 |
| Gold (Business) | $5.00 |

Link checks, failed URLs and summary rows are never charged. Platform usage (compute, storage) is included; you don't pay it separately. Each run also has Apify's standard start fee of $0.00005 per GB of memory. Optional Apify datacenter proxy (or your own proxy URLs) is included in the price; residential proxy isn't supported. Set **Max total charge** on the run to cap your spend; the run stops cleanly when it's reached, without storing unpaid results.

### Features

- **Crawling**: depth limit, page limit, scope (same path / hostname / domain), include/exclude regex, optional **sitemap discovery** (robots.txt, sitemap indexes, .gz)
- Polite by default: respects robots.txt and Crawl-delay, per-host delay, retries with backoff and Retry-After
- Fair crawling across multiple start URLs (one big site can't starve the others)
- Full redirect chain per page, including broken https→http redirects
- Configurable thresholds for title, description, thin content and slow response
- Refuses private/internal addresses

### Limitations

- **No JavaScript rendering.** The audit sees the HTML the server sends, like a basic crawler. Tags that are only added by JavaScript (some SPAs, client-side SEO plugins) are reported as missing.
- `wordCount` counts all text in the HTML body, including menus, footers and hidden pop-ups. `contentWordCount` (used for `THIN_CONTENT`) uses `<main>`, or the body without nav/header/footer/dialogs.
- Link checking is polite (same per-host delay), so sites with hundreds of unique links per page take longer. Use `maxLinkChecks`, `checkExternalLinks: false` or `checkBrokenLinks: false` to speed up. Links that time out are reported as unchecked, not broken.
- No Core Web Vitals / Lighthouse scores, keyword rankings or backlink data.
- Sites with strong bot protection (Cloudflare challenge, Akamai) may block requests; those pages are free. Enabling Apify datacenter proxy sometimes helps.

### Responsible use

Only crawl sites you are allowed to audit. robots.txt is respected by default.

# Actor input Schema

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

Websites or pages to audit. 'example.com' is fine; https:// is added automatically.

## `maxCrawlDepth` (type: `integer`):

0 = audit only the start URLs. 1 = also pages linked from them, and so on.

## `maxPages` (type: `integer`):

Stop after this many audited (charged) pages in total (0 = no limit). Your max total charge also stops the run cleanly.

## `crawlScope` (type: `string`):

Which discovered links to follow.

## `useSitemaps` (type: `boolean`):

Reads robots.txt / sitemap.xml (incl. indexes and .gz) to find pages that aren't linked.

## `includeUrlPatterns` (type: `array`):

Only follow/convert URLs matching at least one pattern, e.g. '/blog/'.

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

Skip URLs matching any pattern, e.g. '/tag/' or '?page='.

## `checkBrokenLinks` (type: `boolean`):

HEAD-checks (GET fallback) every link found on audited pages. Each URL is checked once per run. Turn off for the fastest crawl.

## `checkInternalLinks` (type: `boolean`):

Links to the same site.

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

Links to other websites.

## `maxLinkChecks` (type: `integer`):

Unique URLs to check across the whole run (0 = no limit). Links beyond the cap are reported as unchecked.

## `linkCheckConcurrency` (type: `integer`):

Across all hosts; per-host delay still applies.

## `titleMaxLength` (type: `integer`):

Flags TITLE_TOO_LONG.

## `titleMinLength` (type: `integer`):

Flags TITLE_TOO_SHORT.

## `descriptionMaxLength` (type: `integer`):

Flags META_DESCRIPTION_TOO_LONG.

## `descriptionMinLength` (type: `integer`):

Flags META_DESCRIPTION_TOO_SHORT.

## `minWordCount` (type: `integer`):

Flags THIN_CONTENT.

## `slowResponseMs` (type: `integer`):

Time to first byte that flags SLOW_RESPONSE.

## `includeSiteSummary` (type: `boolean`):

One extra row per start site: average score, issue counts, duplicate titles/descriptions, all broken links. Not charged.

## `writeFailedRows` (type: `boolean`):

404s, timeouts, non-HTML, blocked and robots-disallowed URLs get a row with the reason. Not charged.

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

Skip disallowed URLs and honor Crawl-delay (max 10 s).

## `maxConcurrency` (type: `integer`):

Across all hosts. Requests to one host are always spaced by the delay below.

## `requestDelayMs` (type: `integer`):

Politeness per host.

## `requestTimeoutSecs` (type: `integer`):

Per request.

## `maxRetries` (type: `integer`):

For timeouts, resets, 429 and 5xx (with backoff and Retry-After).

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

Leave empty for the default browser-compatible UA.

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

Optional. Apify datacenter proxy or your own proxy URLs. Residential proxy isn't supported (it would raise the price for everyone).

## Actor input object example

```json
{
  "startUrls": [
    "https://books.toscrape.com/"
  ],
  "maxCrawlDepth": 2,
  "maxPages": 100,
  "crawlScope": "sameHostname",
  "useSitemaps": false,
  "includeUrlPatterns": [],
  "excludeUrlPatterns": [],
  "checkBrokenLinks": true,
  "checkInternalLinks": true,
  "checkExternalLinks": true,
  "maxLinkChecks": 2000,
  "linkCheckConcurrency": 10,
  "titleMaxLength": 60,
  "titleMinLength": 10,
  "descriptionMaxLength": 160,
  "descriptionMinLength": 50,
  "minWordCount": 300,
  "slowResponseMs": 2000,
  "includeSiteSummary": true,
  "writeFailedRows": true,
  "respectRobotsTxt": true,
  "maxConcurrency": 10,
  "requestDelayMs": 200,
  "requestTimeoutSecs": 30,
  "maxRetries": 2,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

Pages with score and key SEO fields.

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

Pages with their issues list.

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

Every row: pages, failed URLs and site summaries.

## `summary` (type: `string`):

Totals, per-site scores and failed URLs (not charged).

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

// Run the Actor and wait for it to finish
const run = await client.actor("blond_terrarium/seo-page-audit-crawler").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": ["https://books.toscrape.com/"],
    "proxyConfiguration": { "useApifyProxy": False },
}

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

```

## MCP server setup

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

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/wwDbboLZPlbCngMys/builds/mHzalXy4ZvdF0Ymu0/openapi.json
