# SEO Site Audit - Crawler, Broken Links, Sitemap & Score (`scrapewise/seo-site-audit`) Actor

Crawl any website and audit on-page SEO: titles, meta descriptions, H1, canonical, noindex, hreflang, Open Graph, alt text, broken links, thin content, JSON-LD, sitemap, robots.txt, HTTPS and orphan pages, with a 0-100 score. US$ 10 per 1,000 pages.

- **URL**: https://apify.com/scrapewise/seo-site-audit.md
- **Developed by:** [Scrapewise Data](https://apify.com/scrapewise) (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

from $8.50 / 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 Site Audit

SEO Site Audit **crawls a whole website and audits every page for on-page SEO**, like a desktop SEO spider, but in the cloud and billed per page. Give it one website or a list of websites and you get:

- **one row per page** with status code, redirect chain, title and meta description (length in characters and in Google pixels, duplicates), H1 and all headings, canonical, noindex and nofollow (meta and X-Robots-Tag), hreflang (with return-link check), Open Graph and Twitter card, images without alt, internal and external links, **broken links**, word count (thin content), JSON-LD and microdata types, HTML size, response time, click depth, a **0-100 score** and the list of issues on that page;
- **one summary row per website** with a site score and grade, the **issues prioritized** by severity and number of pages affected, robots.txt and XML sitemap checks (do they exist, do they agree with each other, with noindex and with canonicals), **orphan pages** (in the sitemap but not linked), HTTPS and TLS certificate expiry, http to https and www redirects, llms.txt and AI crawlers blocked;
- **an HTML report per website** in the key-value store, ready to send to a client.

**US$ 10 per 1,000 audited pages plus US$ 0.025 per website. No monthly fee, no start fee. Error pages, redirects and non-HTML files are free.**

### At a glance

- **Price:** US$ 10 per 1,000 audited pages + US$ 0.025 per website summary
- **A 200-page website:** about US$ 2.03, in about 1 minute
- **What you get for it:** page rows with 60+ fields, a website summary with prioritized issues, broken links checked, an HTML report
- **Free:** error pages, redirects, non-HTML files, pages skipped by robots.txt

### One real row

A page of `crawlee.dev`, audited on 2026-09-30 in the Apify cloud (lists shortened):

```json
{
  "type": "page",
  "site": "crawlee.dev",
  "url": "https://crawlee.dev/python",
  "statusCode": 200,
  "score": 92,
  "issueCount": 2,
  "issues": [
    { "code": "META_DESCRIPTION_DUPLICATE", "severity": "medium", "message": "Same meta description on 4 pages" },
    { "code": "NO_STRUCTURED_DATA", "severity": "low", "message": "No JSON-LD or microdata" }
  ],
  "depth": 1,
  "responseTimeMs": 35,
  "htmlSizeBytes": 69909,
  "inSitemap": true,
  "inlinks": 186,
  "title": "Crawlee for Python · Fast, reliable Python web crawlers.",
  "titleLength": 56,
  "titlePixels": 502,
  "metaDescriptionLength": 144,
  "metaDescriptionDuplicateCount": 4,
  "h1": ["Build reliable web scrapers. Fast."],
  "h1Count": 1,
  "canonical": "https://crawlee.dev/python/",
  "canonicalStatus": "self",
  "noindex": false,
  "hreflang": [{ "lang": "en", "url": "https://crawlee.dev/python/" }, { "lang": "x-default", "url": "https://crawlee.dev/python/" }],
  "openGraph": { "title": "Crawlee for Python · Fast, reliable Python web crawlers.", "image": "https://crawlee.dev/python/img/crawlee-python-og.png" },
  "wordCount": 668,
  "structuredDataTypes": [],
  "imagesTotal": 30,
  "imagesMissingAltCount": 0,
  "internalLinksCount": 16,
  "externalLinksCount": 11,
  "brokenLinksCount": 0
}
```

The website summary of the same run: 200 pages audited in 69 seconds, score 91 (A), 1 broken internal link, 92 pages sharing a meta description, certificate valid until 2026-11-17, llms.txt present. In other runs the same day, `htmx.org` (279 pages, whole site) came back with 13 broken links, 5 orphan pages and 3 pages returning 404, and a coffee shop on Shopify (150 pages) with a sitemap URL blocked by its own robots.txt and noindex pages listed in the sitemap.

### What you can do with it

- **Technical SEO audits** for your own website or for clients, on a schedule, without installing a desktop crawler.
- **Broken link monitoring:** every link found on the audited pages is checked, internal and external.
- **Pre-launch and migration checks:** redirects, canonicals, noindex left behind, sitemap and robots.txt coherence.
- **Agency prospecting:** audit a list of prospect websites and open the conversation with their score and top 3 issues.
- **Content cleanup:** find duplicate titles and descriptions, thin pages, missing H1 and images without alt.

### Input

| Field | What it does |
|---|---|
| `startUrls` | Websites or domains, one per line. The crawl stays on each website |
| `maxPagesPerSite` | Stop after this many pages per website (default 100, up to 10,000) |
| `includeSubdomains` | Also crawl `blog.example.com` when you start at `example.com` |
| `includePatterns`, `excludePatterns` | Only crawl, or skip, URLs containing a text or wildcard pattern (`/blog/`, `*/tag/*`) |
| `crawlSitemapUrls` | Fill the remaining budget with sitemap URLs (default on) |
| `checkExternalLinks`, `maxLinkChecksPerSite` | Check outgoing links for 4xx, 5xx and dead domains (default on, 500 per website) |
| `minWordCount` | Thin content threshold (default 200 words) |
| `respectRobotsTxt`, `maxConcurrencyPerSite`, `delayMs` | Politeness: robots.txt and Crawl-delay honored, 4 requests at a time per website by default |
| `proxyConfiguration` | Optional; most websites need none |

```json
{ "startUrls": ["https://crawlee.dev", "example.com"], "maxPagesPerSite": 200, "excludePatterns": ["/tag/"] }
```

### Output

Rows have `type`: `page`, `site` or `error`. The dataset has two views, **Pages** and **Website summaries**.

**Page fields:** `url`, `finalUrl`, `statusCode`, `redirectChain`, `redirectCount`, `responseTimeMs`, `htmlSizeBytes`, `depth`, `inlinks`, `inSitemap`, `score`, `issues` (`code`, `severity`, `message`), `title`, `titleLength`, `titlePixels`, `titleDuplicateCount`, `metaDescription` (+ length, pixels, duplicates), `h1`, `h1Count`, `headings`, `headingCounts`, `headingSkips`, `canonical`, `canonicalStatus` (`self`, `other`, `missing`), `metaRobots`, `xRobotsTag`, `noindex`, `nofollow`, `hreflang`, `hreflangMissingReturn`, `openGraph`, `twitterCard`, `lang`, `viewport`, `favicon`, `wordCount`, `thinContent`, `structuredDataTypes`, `microdataTypes`, `jsonLdInvalid`, `imagesTotal`, `imagesMissingAlt`, `imagesEmptyAlt`, `internalLinksCount`, `externalLinksCount`, `brokenInternalLinks`, `brokenExternalLinks`, `linksToRedirects`, `mixedContent`.

**Website fields:** `score`, `grade`, `averagePageScore`, `pagesAudited`, `crawlComplete`, `topIssues` (prioritized, with pages affected and example URLs), `https`, `httpRedirectsToHttps`, `wwwConsolidated`, `certificate` (issuer, valid to, days left), `robotsTxt` (sitemaps declared, crawl delay, AI crawlers blocked), `llmsTxt`, `sitemap` (URLs, broken, redirecting, noindex, non-canonical, blocked by robots.txt), `orphanPages`, `duplicateTitles`, `duplicateMetaDescriptions`, `brokenLinksFound`, `averageResponseTimeMs`, `structuredDataTypes`.

#### Issue codes

High: `HTTP_ERROR`, `TITLE_MISSING`, `BROKEN_INTERNAL_LINKS`, `CANONICAL_TO_ERROR`, `NO_HTTPS`, `CERTIFICATE_INVALID`, `ROBOTS_BLOCKS_SITE`, `SITEMAP_URLS_BROKEN`, `SITEMAP_URLS_BLOCKED_BY_ROBOTS`. Medium: `META_DESCRIPTION_MISSING`, `TITLE_DUPLICATE`, `META_DESCRIPTION_DUPLICATE`, `H1_MISSING`, `NOINDEX`, `IMAGES_MISSING_ALT`, `BROKEN_EXTERNAL_LINKS`, `THIN_CONTENT`, `REDIRECT_CHAIN`, `SLOW_RESPONSE`, `VIEWPORT_MISSING`, `MIXED_CONTENT`, `HREFLANG_NO_RETURN`, `JSON_LD_INVALID`, `SITEMAP_MISSING`, `ORPHAN_PAGES`, `WWW_NOT_CONSOLIDATED`, and more. Low: length, heading order, Open Graph, Twitter card, URL hygiene and similar.

### Pricing

| Event | Price |
|---|---|
| Page audited (HTML page with a 2xx answer) | US$ 10 per 1,000 |
| Website summary | US$ 0.025 per website |

A 50-page blog costs about US$ 0.53, a 1,000-page store about US$ 10.03. The best-known SEO audit Actor in the Store charges US$ 40 per 1,000 pages.

### FAQ

**Does it render JavaScript?** No. It reads the HTML the server sends, which is what search engines index first. Websites that build all content in the browser will show little text; that is itself an SEO finding.

**Will it hurt my server?** It sends at most 4 requests at a time per website by default, honors robots.txt and Crawl-delay, and you can lower the concurrency or add a delay.

**Why is `orphanPages.count` empty?** Orphan pages can only be known when the whole website was crawled. Raise `maxPagesPerSite` until `crawlComplete` is `true`.

**Why is a link reported as blocked and not broken?** Sites like LinkedIn answer 403 or 999 to every automated check. Those links are counted in `externalLinksBlockedCheck` and never flagged as broken.

**How is the score computed?** Each page starts at 100 and loses 15 points per high issue, 6 per medium and 2 per low. The website score is the average page score minus half the weight of the website issues.

### Limitations

- JavaScript is not executed and Core Web Vitals are not measured.
- Pixel widths use Arial at Google's sizes (20 px titles, 14 px descriptions); Google's own cut can differ by a few characters.
- Link checks stop at `maxLinkChecksPerSite`; the summary tells how many were not checked.

### Changelog

- **2026-09-30, 0.1:** first release.

# Actor input Schema

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

Websites to audit, one per line, as links or plain domains (for example https://crawlee.dev or example.com). The crawl starts at each link and stays on that website. Each website returns one row per audited page (status, title, meta description, headings, canonical, word count, broken links, issues and a page score) and one summary row with the site score and a prioritized issue list. Leave empty to audit an example website. Also accepts 'urls', 'websites' or 'domains'.

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

The crawl stops after this many pages on each website (1 to 10,000, default 100). Each audited page is charged, so this is the main cost control: 100 pages is enough for most small sites, raise it for full audits of large ones. Links found on the audited pages are still checked for broken links.

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

Also crawls subdomains such as blog.example.com and shop.example.com when you start at example.com, and counts them in the same summary. Off (default): only the start host is crawled, with www and non-www treated as the same site.

## `includePatterns` (type: `array`):

Optional. Only crawl URLs that contain one of these texts or match these wildcard patterns, one per line (for example /blog/ or */products/*). Useful to audit one section of a large site. The start URL is always audited. Leave empty to crawl every page.

## `excludePatterns` (type: `array`):

Optional. Skip URLs that contain one of these texts or match these wildcard patterns, one per line (for example /tag/, ?page= or */cart*). Skipped URLs are not crawled and not charged. Leave empty to skip nothing.

## `crawlSitemapUrls` (type: `boolean`):

On by default: after following links, fills the remaining page budget with URLs from the website's XML sitemap, which finds pages with no internal links pointing to them (orphan pages). Turn off to audit only pages reachable by links.

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

On by default: checks every outgoing link for 4xx and 5xx answers and dead domains, and reports them as broken links on the page that has them. Internal links are always checked. Turn off for a faster run focused on on-page issues. No extra charge.

## `maxLinkChecksPerSite` (type: `integer`):

Links that were not crawled (external links and internal links beyond the page limit) are checked with a light HEAD request, up to this number per website (0 to 20,000, default 500). 0 skips those checks. Link checks are not charged.

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

Pages with fewer visible words than this (default 200) get the THIN_CONTENT issue. Set a lower value for sites with short product or landing pages, or 0 to turn the check off.

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

On by default: skips URLs that robots.txt disallows for all crawlers and honors its Crawl-delay (up to 5 seconds). Turn off to audit your own site including pages hidden from crawlers. Skipped pages are free.

## `maxConcurrencyPerSite` (type: `integer`):

How many pages of the same website are requested at the same time (1 to 10, default 4). Keep it low to be gentle with small servers; raise it for large sites on strong hosting to finish faster.

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

Optional pause in milliseconds between page requests on the same website (0 to 30,000, default 0; for example 1000 for one second). Use it for servers that rate-limit crawlers.

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

Optional and off by default: most websites do not need a proxy. Turn on Apify Proxy only if a website blocks the crawler and its pages come back with 403 or 429 errors.

## Actor input object example

```json
{
  "startUrls": [
    "https://crawlee.dev"
  ],
  "maxPagesPerSite": 50,
  "includeSubdomains": false,
  "crawlSitemapUrls": true,
  "checkExternalLinks": true,
  "maxLinkChecksPerSite": 500,
  "minWordCount": 200,
  "respectRobotsTxt": true,
  "maxConcurrencyPerSite": 4,
  "delayMs": 0
}
```

# Actor output Schema

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

No description

## `resultsCsv` (type: `string`):

No description

## `reports` (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 = {
    "startUrls": [
        "https://crawlee.dev"
    ],
    "maxPagesPerSite": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapewise/seo-site-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": ["https://crawlee.dev"],
    "maxPagesPerSite": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("scrapewise/seo-site-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": [
    "https://crawlee.dev"
  ],
  "maxPagesPerSite": 50
}' |
apify call scrapewise/seo-site-audit --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapewise/seo-site-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/SHh4d2AMjmEYFUUxh/builds/e3EFEJMSGXwsEc0A0/openapi.json
