# SEO Audit Tool: Bulk Website Audit with Scores & Fixes (`hillcountrybots/seo-audit`) Actor

Run a website SEO audit on a whole site from its sitemap, or any list of URLs: 0-100 SEO score per page, a site summary with the top 5 fixes ranked by impact, duplicate titles, broken pages and a client-ready HTML report. $2 per 1,000 pages, failures free.

- **URL**: https://apify.com/hillcountrybots/seo-audit.md
- **Developed by:** [Hill Country Bots](https://apify.com/hillcountrybots) (community)
- **Categories:** SEO tools, Developer tools, Automation
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.00 / 1,000 audited pages

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

## SEO Audit Tool: Bulk Website Audit with Scores & Fixes

Run a **website SEO audit** on a **whole website from its sitemap**, or any list of URLs, and get back what an SEO client actually wants to see: a **0-100 score for every page**, a **site summary with the top 5 fixes ranked by impact**, **duplicate titles and descriptions**, **broken pages and broken links**, and a **clean HTML report you can send to a client**. Every issue is written in plain English with **why it matters** and **how to fix it**. Plain HTTP, no browser, no login. **$2 per 1,000 audited pages. Failed, blocked and broken pages are free.**

### What does this SEO audit tool do?

Give it `example.com` and it will:

1. Find the site's XML sitemap (robots.txt, then `/sitemap.xml`), or crawl internal links if there is none.
2. Audit the **most important pages first** (shallow URLs like `/pricing` before deep ones like `/blog/2019/03/post`), up to your page limit.
3. Check each page for 31 on-page and technical SEO issues (plus 7 site-wide checks): title and meta description (missing, too long, too short), H1 (missing, several), canonical (missing, pointing elsewhere), **noindex** (robots meta and X-Robots-Tag), hreflang, Open Graph and Twitter cards, schema.org types and **broken JSON-LD**, images without alt text, thin content (word count), mobile viewport, HTTPS and mixed content, redirect chains, slow responses, page size, and **broken links** (404/410/5xx and dead domains).
4. Score every page 0-100 and grade it A-F.
5. Build one **site summary** per website: site score, the **top 5 fixes ranked by impact** (how bad x how many pages), issue counts by severity, duplicate titles and descriptions, broken pages, noindex pages listed in the sitemap, and the 10 lowest-scoring pages.
6. Save a **shareable HTML report** (one link, no login needed) plus CSV-friendly rows.

### Who is it for?

- **SEO freelancers and agencies:** run a full audit before a client call, send the HTML report, and turn the top 5 fixes into a proposal.
- **Web designers and developers:** check a site before and after a launch or migration. Catch noindex left on, broken links, missing titles and redirect chains.
- **Lead generation / client pitches:** audit a list of prospect homepages and open with a score and a short list of fixes you would actually do.
- **In-house marketers:** a cheap monthly health check of every page in your sitemap.
- **AI agents and automations:** stable JSON fields, one flat price per page, and machine-readable issue IDs plus human-readable text.

### Quick start

1. Paste one or more websites, sitemap URLs or page URLs into **Websites, sitemaps or page URLs**. The form comes prefilled, so **Start** works right away.
2. Set **Max pages per website** (10 for a preview, 100 for a normal audit, 1,000+ for a deep one).
3. Click **Start**. Open the **HTML report** from the Output tab (or the `REPORT` record in the key-value store) and download the rows as CSV, Excel or JSON. The **Page audits** view shows one line per page.

#### Sample output

This table is from a real whole-site preview of crawlee.dev on **November 10, 2026, at 9:34 PM CT** (America/Chicago): 5 pages audited, site score **87 (B)**. You pay about **$2 per 1,000** audited pages. Failed, blocked and broken pages are free. 23 further sitemap URLs were skipped by the preview cap.

![Sample output from a real SEO Audit Tool run](https://api.apify.com/v2/key-value-stores/2kusIBSeqRffwM3Q9/records/seoaudit-sample-output.png?signature=i5ZMlCaQTPcpGSMlItJj)

| Page | Score | Grade | Warnings | Notices | Top issue |
|---|---:|---|---:|---:|---|
| crawlee.dev/ | 92 | A | 1 | 1 | Very little text on the page |
| crawlee.dev/js | 97 | A | 0 | 1 | No structured data (schema.org) |
| crawlee.dev/search | 88 | B | 2 | 1 | Very little text on the page |
| crawlee.dev/blog | 88 | B | 2 | 0 | No H1 heading |
| crawlee.dev/blog/archive | 90 | A | 1 | 2 | Title is too long (Google will cut it off) |

If a site or page cannot be audited, the run writes a row with `status: "error"`, `errorCode` and `errorMessage` instead of finishing as a silent success with empty data. Those rows are not charged.

#### Example inputs

Whole-site audit, the 100 most important pages:

```json
{ "startUrls": ["https://crawlee.dev"], "maxPagesPerSite": 100 }
```

Audit exactly the pages in a sitemap (pairs with our [Sitemap URL Extractor](https://apify.com/hillcountrybots/sitemap-url-extractor)):

```json
{ "startUrls": ["https://docs.apify.com/sitemap.xml"], "maxPagesPerSite": 1000 }
```

Prospect list: just the homepage of each site, spend capped at $1:

```json
{ "startUrls": ["prospect-one.com", "prospect-two.com", "prospect-three.com"], "auditWholeSite": false, "maxPages": 500 }
```

Only the blog, skipping tag pages, without the link check (fastest):

```json
{ "startUrls": ["https://example.com"], "includePatterns": ["*/blog/*"], "excludePatterns": ["*/tag/*"], "checkBrokenLinks": false }
```

Ready-to-run example tasks (pre-filled inputs on this Actor's Store page):

- [Audit a whole website and get a scored fix list](https://apify.com/hillcountrybots/seo-audit/examples/seoaudit-whole-site-crawlee)
- [Audit the pages listed in a sitemap](https://apify.com/hillcountrybots/seo-audit/examples/seoaudit-sitemap-apify-docs)

### Input

| Field | Default | What it does |
|---|---|---|
| `startUrls` (required) | (none) | Homepage or bare domain = whole-site audit. Sitemap URL = its pages. Any other URL = just that page. |
| `maxPagesPerSite` | 100 | Pages audited per website or sitemap, most important first. |
| `maxPages` | 1000 | Spend cap for the run: maxPages x $0.002. Failed pages don't count. |
| `auditWholeSite` | true | Off = only audit the homepage you give. |
| `includePatterns` / `excludePatterns` | \[] | Globs (`*/blog/*`) or `/regex/` for sitemap and crawl URLs. |
| `checkBrokenLinks` | true | Check links on each page (main-content links first, up to `maxLinksPerPage` new links per page, each link once per run). |
| `maxLinksPerPage` | 20 | New links checked per page (1-100). |
| `htmlReport` | true | Save the shareable HTML report. |
| `respectRobotsTxt` | true | Skip pages that robots.txt disallows (free). |
| `maxConcurrency` / `maxConcurrencyPerSite` | 10 / 4 | Parallel requests overall / per website. |
| `requestTimeoutSecs` | 20 | Per-page timeout (one retry on timeouts, 429 and 5xx). |

### Output example

Real row from a test run (shortened):

```json
{
  "rowType": "page",
  "site": "https://crawlee.dev",
  "url": "https://crawlee.dev/blog",
  "statusCode": 200,
  "score": 88,
  "grade": "B",
  "criticalCount": 0, "warningCount": 2, "noticeCount": 0,
  "topIssue": "No H1 heading",
  "title": "Crawlee Blog - learn how to build better scrapers | Crawlee for JavaScript · Build reliable crawlers. Fast.",
  "titleLength": 107,
  "metaDescriptionLength": 151,
  "h1Count": 0,
  "canonicalStatus": "self",
  "indexable": true,
  "schemaTypes": ["Blog", "BlogPosting", "Person", "ImageObject"],
  "imagesMissingAlt": 0,
  "internalLinks": 41, "externalLinks": 29,
  "linksChecked": 20, "brokenLinks": 0,
  "wordCount": 1108,
  "responseTimeMs": 113,
  "pageSizeKb": 65.2,
  "issues": [
    {
      "id": "MISSING_H1",
      "severity": "warning",
      "title": "No H1 heading",
      "whyItMatters": "The H1 is the main headline on the page. It tells visitors and search engines what the page is about.",
      "howToFix": "Add one <h1> near the top of the page that matches the page topic (it can be close to the title)."
    },
    {
      "id": "TITLE_TOO_LONG",
      "severity": "warning",
      "title": "Title is too long (Google will cut it off)",
      "detail": "107 characters; aim for 30-60",
      "whyItMatters": "Google shows roughly 60 characters. The rest is replaced with \"...\", which can hide your key message.",
      "howToFix": "Shorten the title to 60 characters or fewer and put the most important words first."
    }
  ],
  "status": "ok"
}
```

And the free site summary row for the same run (10 pages):

```json
{
  "rowType": "site-summary",
  "site": "https://crawlee.dev",
  "siteScore": 88, "grade": "B", "averagePageScore": 92,
  "pagesAudited": 10, "pagesFailed": 0, "brokenPages": 0,
  "issueCounts": { "critical": 0, "warning": 16, "notice": 8 },
  "auditSource": "sitemap",
  "topFixes": [
    { "rank": 1, "severity": "warning", "fix": "Title is too long (Google will cut it off)", "pagesAffected": 8, "percentOfPages": 80,
      "howToFix": "Shorten the title to 60 characters or fewer and put the most important words first." },
    { "rank": 2, "severity": "warning", "fix": "Several pages share the same meta description", "pagesAffected": 4, "percentOfPages": 40,
      "howToFix": "Write a unique description for each page, or at least for your most important ones." }
  ]
}
```

A page that cannot be audited comes back free, with a reason: `{"status": "error", "errorCode": "HTTP_404", "url": "...", "errorMessage": "Page answered HTTP 404 (broken page)"}`. Broken (404/410/5xx) pages also appear in the site summary as a critical site issue.

#### The HTML report

One self-contained page per run: a score circle per site, the top 5 fixes with why and how, duplicate titles and descriptions, every page sorted from the lowest score, and the pages that could not be audited. The run's `RUN_SUMMARY` record holds a signed `reportUrl` you can share directly.

#### How the score works

Every page starts at 100 and loses points per issue: critical 10-30 (noindex, missing title, no HTTPS, broken internal links), warning 4-8 (missing description or H1, no viewport, canonical elsewhere, broken JSON-LD, slow response, thin content, missing alt text...), notice 1-3 (polish items). Grades: A 90+, B 80+, C 70+, D 50+, F below 50. The site score is the average page score minus up to 15 points for site-wide problems (broken pages, duplicates, no sitemap), scaled by how many pages they affect. The rules are fixed and documented, so scores are comparable run to run.

### Use it from the API, MCP or an AI agent

- **Actor ID:** `hillcountrybots/seo-audit`
- One call returns the rows:

```bash
curl -X POST "https://api.apify.com/v2/acts/hillcountrybots~seo-audit/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"startUrls":["https://crawlee.dev"],"maxPagesPerSite":10}'
```

- With the Apify MCP server, an agent can call `hillcountrybots/seo-audit` with the same input. Filter rows on `rowType` (`page` / `site-summary`) and `status` (`ok` / `error`).

### Use with n8n, Make, Zapier or on a schedule

You can run this Actor from the tools you already use. No extra plugin is required. Apify's official integrations start a run with **your own Apify token** and pass the results to the next step.

- **On a schedule:** in [Apify Console](https://console.apify.com), open this Actor, go to **Schedules**, and run it weekly (for example every Monday) on the same URL list or website. See [Schedules](https://docs.apify.com/platform/schedules).
- **Make:** use the official [Apify Make integration](https://docs.apify.com/integrations/make) to start a run and send the dataset to Sheets, Slack or email.
- **Zapier:** use the official [Apify Zapier integration](https://docs.apify.com/integrations/zapier).
- **n8n:** use the official [Apify n8n integration](https://docs.apify.com/integrations/n8n) to add this Actor to a workflow.

A typical setup is: schedule a weekly run → export the dataset to Google Sheets or Slack → review the new rows. The input is the same as on this page.

### How much does it cost?

- **$2.00 per 1,000 audited pages** ($0.002 per page). One flat event, `page-audited`. No start fee, platform compute included, broken-link checking included.
- **Free:** site summary rows, the HTML report, and every page that fails (404, 403, 5xx, timeouts, robots.txt-blocked, bot challenges, non-HTML files, invalid or private URLs).
- Example: a 300-page site audit costs $0.60. A 5,000-page agency batch costs $10.
- For comparison, other SEO audit Actors in Apify Store list $4 to $40 per 1,000 pages on the Free plan (checked November 10, 2026).
- Set `maxPages` or the run's max total charge to cap spend. The Actor stops cleanly when the cap is reached.

### Limits

- **Plain HTTP, no browser.** Content that only appears after JavaScript runs (some single-page apps) is not seen, so word counts and headings can read low on those sites. Core Web Vitals are not measured here; use our [Bulk PageSpeed Checker](https://apify.com/hillcountrybots/bulk-pagespeed-checker) for that.
- **Some sites block bots.** Sites behind strong bot protection answer 403 or a challenge page. Those pages are free and listed in the report. We use an honest User-Agent, no proxies and no tricks.
- **robots.txt is respected** by default, and login, account, admin, cart and checkout pages are never fetched.
- Broken-link checking samples up to `maxLinksPerPage` new links per page; only clear answers count as broken (404, 410, 500, 502, 504, dead domain, redirect loop, each confirmed with a full GET). A link that answers 403, 429, 503 or times out is treated as unknown, not broken. Links to social networks (X/Twitter, LinkedIn, Facebook, Instagram, TikTok, Threads, Pinterest) are not checked, because they block bots with misleading answers.
- Public pages only; no cookies or credentials are ever sent, and no personal data is collected.

### FAQ

**Can I audit a client's whole site in one click?** Yes. Put the homepage in, set `maxPagesPerSite`, and run. The sitemap (or a link crawl) does the rest.

**Can I audit thousands of sites?** Yes. Use `auditWholeSite: false` for homepage-only audits of a prospect list, or keep it on with a small `maxPagesPerSite`.

**Why are failed pages free?** You should pay for audits, not for a site that blocked us.

**What's the difference between critical, warning and notice?** Critical hurts visibility right now (noindex, missing title, no HTTPS, broken links). Warnings cost clicks or rankings. Notices are polish.

**Does it follow nofollow or external links?** It only audits pages on the same site. External links are counted and, with the link check on, tested for dead targets.

### More tools from Hill Country Bots

- [Bulk PageSpeed Insights Test](https://apify.com/hillcountrybots/bulk-pagespeed-checker): Lighthouse scores and Core Web Vitals for many URLs. $3 per 1,000 audits, failures free.
- [Bulk Domain Availability Checker](https://apify.com/hillcountrybots/domain-availability-checker): Bulk available/taken checks from official registry RDAP, with registrar and expiry dates. $1 per 1,000 domains, invalid inputs and errors free.
- [Sitemap URL Extractor](https://apify.com/hillcountrybots/sitemap-url-extractor): Every URL from a site's XML sitemaps (robots.txt, indexes, gzip). $0.50 per 1,000 URLs, failures free.
- [Tech Stack Detector](https://apify.com/hillcountrybots/tech-stack-detector): CMS, ecommerce, analytics, CDN and frameworks for many websites. $0.50 per 1,000 sites, failures free.

### Support

Found a wrong result or want a new check? Open an issue on this Actor's **Issues** tab with the run ID and the URL. We reply within 2 business days (US Central time). Built and maintained by **Hill Country Bots**.

# Actor input Schema

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

Required. One per line. A homepage or bare domain (example.com) = whole-site audit. A sitemap URL (…/sitemap.xml) = audit the pages it lists. Any other URL = audit just that page. Private, local and internal addresses and login/account/admin/cart/checkout pages are refused for free.

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

Pages audited per website or sitemap (shallow, important pages first). 10 for a quick preview, 100 for a standard audit, 1,000+ for a deep one. Single page URLs you list yourself are not limited by this.

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

Total pages audited and charged in this run. Caps spend at maxPages x $0.002 (1,000 = $2). Failed pages do not count. The run's own max-charge setting is also respected.

## `auditWholeSite` (type: `boolean`):

On: a homepage or bare domain expands to the site's pages (sitemap, or link crawl if no sitemap). Off: only the homepage itself is audited.

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

Optional globs (e.g. */blog/*) or /regex/. Applied to pages from sitemaps and link crawls.

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

Optional globs or /regex/, e.g. */tag/* or */page/*.

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

Check the links on each page: links in the main content first, up to 'Links checked per page' new links per page (each unique link is checked once per run, HEAD first; links already checked are reported for free) and flag 404/410/5xx and dead domains. Included in the page price. Turn off for faster runs.

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

Only used when the broken-link check is on (1-100).

## `htmlReport` (type: `boolean`):

Save a clean, shareable HTML report (scores, top fixes, duplicates, every page) to the key-value store as REPORT. Free.

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

Skip (free) pages that the site's robots.txt disallows for our crawler or all crawlers. Turn off only for sites you own or are allowed to audit.

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

Pages fetched in parallel across all sites (1-50).

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

Pages fetched in parallel from one website (1-10). Keep low to be polite to small sites.

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

Seconds to wait for one page, including redirects (one retry on timeouts, network errors, 429 and 5xx).

## Actor input object example

```json
{
  "startUrls": [
    "https://crawlee.dev"
  ],
  "maxPagesPerSite": 10,
  "maxPages": 1000,
  "auditWholeSite": true,
  "includePatterns": [],
  "excludePatterns": [],
  "checkBrokenLinks": true,
  "maxLinksPerPage": 20,
  "htmlReport": true,
  "respectRobotsTxt": true,
  "maxConcurrency": 10,
  "maxConcurrencyPerSite": 4,
  "requestTimeoutSecs": 20
}
```

# Actor output Schema

## `report` (type: `string`):

No description

## `pages` (type: `string`):

No description

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

No description

## `summary` (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": 10
};

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

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

```

## MCP server setup

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