# SEO Audit (`agarwoods/seo-audit`) Actor

Audit many web pages at once and get one row per page with a 0-100 score, titles, headings, links, indexability, structured data and a list of fixes ranked by importance. Plain HTTP only, $5 per 1,000 pages.

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

## Pricing

from $4.25 / 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

Audit many web pages in one run and get a ranked list of what to fix on each, at $5 per 1,000 pages. Give it a list of addresses, or let it follow internal links from them. Each page comes back as one row with a 0 to 100 score, the page's SEO details, and its issues in fix-first order.

You don't need a login, a browser, proxies or an API key. The Actor reads pages over plain HTTP and respects each site's robots.txt.

### What you get

- One row per page, with the same fields every time
- A score from 0 to 100 and a `topFix` field, the single most useful thing to do first
- Issues sorted most important first, each with a severity, a code, a message and a fix
- Title, meta description, canonical, robots rules, headings, word count, images and alt text
- Internal and external link counts, plus broken links when you turn that on
- Viewport, language, hreflang, structured data types, Open Graph and Twitter card
- Page size, response time, HTTPS, robots.txt result, sitemap listing and the full redirect chain

### Sample output

```json
{
  "recordType": "audit",
  "url": "http://shop.example/desks",
  "finalUrl": "https://shop.example/desks",
  "httpStatus": 200,
  "redirectChain": [{ "url": "http://shop.example/desks", "httpStatus": 301 }],
  "score": 76,
  "title": "Oak desks",
  "titleLength": 9,
  "metaDescription": null,
  "metaDescriptionLength": 0,
  "canonical": "https://shop.example/desks",
  "robotsMeta": null,
  "xRobotsTag": null,
  "isIndexable": true,
  "h1Count": 1,
  "firstH1": "Handmade oak desks",
  "headingOutline": { "h1": 1, "h2": 4, "h3": 2, "h4": 0, "h5": 0, "h6": 0 },
  "wordCount": 812,
  "imageCount": 14,
  "imagesMissingAlt": 2,
  "internalLinks": 38,
  "externalLinks": 3,
  "brokenLinks": null,
  "linksChecked": null,
  "linksNotChecked": null,
  "hasViewport": true,
  "lang": "en",
  "hreflangCount": 0,
  "structuredDataTypes": ["Organization", "Product"],
  "openGraph": true,
  "twitterCard": true,
  "pageSizeBytes": 84211,
  "responseTimeMs": 412,
  "usesHttps": true,
  "robotsTxtAllowed": true,
  "inSitemap": true,
  "issues": [
    {
      "severity": "high",
      "code": "missing_meta_description",
      "message": "The page has no meta description.",
      "fix": "Add a meta description of 70 to 160 characters that sums up the page and gives a reason to click."
    },
    {
      "severity": "medium",
      "code": "title_too_short",
      "message": "The title has 9 characters, under the suggested 30.",
      "fix": "Expand the title to 30 to 60 characters with the page's main topic."
    },
    {
      "severity": "medium",
      "code": "images_missing_alt",
      "message": "2 of 14 images have no alt attribute.",
      "fix": "Add alt text that describes each image, or alt=\"\" for images that are only decoration."
    },
    {
      "severity": "low",
      "code": "single_redirect",
      "message": "The address redirects once before the page loads.",
      "fix": "Link directly to the final address where you can."
    }
  ],
  "topFix": "Add a meta description of 70 to 160 characters that sums up the page and gives a reason to click.",
  "status": null,
  "reason": null,
  "fetchedAt": "2026-11-02T10:00:00.000Z"
}
```

This example shows the format, and the page is made up.

### How to use it

1. Add your page addresses, one per line. A bare domain such as example.com is read as https://example.com.
2. Set Maximum pages, which also caps the cost. Turn on Follow internal links or Check links if you want them.
3. Press Start, then sort the results by `score` or filter on `recordType` equal to `audit`.

The example input audits two pages. A run with no input at all uses those same two pages, so it costs about $0.01. An explicitly empty address list audits nothing, costs nothing and returns a status row.

### Input options

| Option | Default | What it does |
| --- | --- | --- |
| urls | Two example pages | The pages to audit |
| maxPages | 100 | Stops after requesting this many pages, up to 10,000. Failed pages count, and pages robots.txt blocks don't |
| followInternalLinks | Off | Also audits pages linked from each audited page on the same site. Skips links marked nofollow, pages with a nofollow robots tag, and links to files such as PDFs and images |
| maxPagesPerSite | 50 | When following links, stops adding pages for a site at this many |
| checkLinks | Off | Requests the links on each page and lists the broken ones |
| maxLinksPerPage | 25 | When checking links, how many different links to check per page, up to 100 |

A link is listed as broken only when it answers a clear error such as 404 or 410, or when its host doesn't exist or refuses connections. Answers such as 403, 429 or a server error often mean a site is turning bots away, so those links are counted in `linksNotChecked` and aren't called broken. Link checking costs nothing extra, but it makes runs slower.

### What it checks

| Severity | Issue codes |
| --- | --- |
| Critical | noindex_meta, noindex_header, blocked_for_googlebot, missing_title |
| High | broken_links, not_https, missing_h1, missing_meta_description, missing_viewport |
| Medium | canonical_points_elsewhere, multiple_canonicals, missing_canonical, redirect_chain, thin_content, multiple_h1, title_too_short, title_too_long, images_missing_alt, missing_lang, invalid_structured_data, not_in_sitemap, slow_response |
| Low | description_too_short, description_too_long, single_redirect, large_page, skipped_heading_level, no_internal_links, missing_structured_data, missing_open_graph, missing_twitter_card |

The score starts at 100 and drops by 25 for each critical issue, 12 for each high, 5 for each medium and 2 for each low, and it stops at 0. It's our own measure for ranking your pages against each other, not a score from Google or any other search engine. Length limits such as 30 to 60 characters for titles are common rules of thumb, not search engine rules.

### Pricing

You pay only for pages that are audited, and Apify platform usage is included.

| Event | Price |
| --- | --- |
| Each page audited | $0.005 ($5 per 1,000) |
| Each run started | $0.00005 |

Status rows are free, so you aren't charged for pages that are blocked, missing, not HTML or unreachable. Checking links and following links add no separate charge, though each extra page you follow is billed as one audit.

A run stops at the maximum cost you set, and a status row tells you when that happens. On the free Apify plan, each run covers up to 100 pages, and a status row tells you when you reach that limit. Any paid Apify plan removes it.

### Status rows

When a page can't be audited, you get a free row with `recordType` set to `status`. It has the same fields as an audit row, with `status` and `reason` filled in.

| Status | Meaning |
| --- | --- |
| no_urls | No addresses were given |
| invalid_url | The text isn't a valid web address |
| blocked_url | The address isn't allowed: private, internal or loopback, not http or https, or it contains a password |
| robots_blocked | The site's robots.txt doesn't allow this Actor to read the page, so it wasn't fetched |
| robots_unavailable | The site's robots.txt answered with a server error, so the page wasn't fetched |
| not_html | The address returned something other than an HTML page |
| too_large | The page is over 5 MB |
| http_error | The site answered with an error such as 404 or 403, shown in `httpStatus` |
| redirect_error | Too many redirects, a redirect loop, or a redirect to an invalid address |
| fetch_failed | The site couldn't be reached, and `reason` says why |
| audit_failed | The page was read, but something went wrong while auditing it |
| site_limit | Maximum pages per site was reached for a site |
| max_pages | Maximum pages was reached |
| free_limit | The run reached the free plan limit |
| spending_limit | The run reached your maximum cost |

A run in which every page gets a status row still finishes as succeeded, so check the status rows when you see no audits.

### What it can't do

- **JavaScript isn't run.** The Actor reads the HTML the server sends. Text, links and tags that JavaScript adds later aren't seen, so a JavaScript-rendered page can show false issues such as a missing H1 or thin content. The thin_content message says when a page looks like this.
- It doesn't measure Core Web Vitals, rendering speed or mobile layout. `responseTimeMs` is the time to download the HTML from our server, so it won't match what your visitors see.
- It doesn't use proxies, solve CAPTCHAs or log in. Sites that turn away automated requests return a free `http_error` row.
- It doesn't track rankings, keywords or backlinks.
- It treats www and non-www as one site. Other subdomains count as separate sites and external links.
- Text fields past 1,000 characters are cut. The length fields still give the full length.
- `inSitemap` reads up to 10 sitemap files and 10 MB per site. If it can't read them all and the page isn't in those it read, the field is null rather than false.

### How it treats sites

The Actor identifies itself as `seo-audit-actor` and checks robots.txt before every page, redirect, sitemap and link check. It makes at most 2 requests at once per host, waits 500 ms between request starts, and honours Crawl-delay up to 10 seconds. It refuses private and internal addresses, including when a public page redirects to one. It stops reading any response at 5 MB.

### Using it with AI agents

The main input is a list, every field is optional, and no credentials are needed. An agent can audit many pages in one call, act on `topFix`, and read status rows to see what went wrong.

### Data and use

This Actor reads only public pages and stores only the fields shown above. You are responsible for auditing pages in line with the sites' terms. This Actor is not affiliated with or endorsed by Google, any other search engine, or any site you audit.

### Support

Open an issue on the Issues tab with your input and what you expected.

# Actor input Schema

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

The pages to audit, one per line. A bare domain such as example.com is read as https://example.com. An empty list returns a free status row and audits nothing.

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

Stop after requesting this many pages, which also caps the cost. Pages that fail count, and pages robots.txt blocks do not.

## `followInternalLinks` (type: `boolean`):

Also audit pages linked from each audited page on the same site. A site's www and non-www addresses count as one site, and other subdomains do not. Each extra page is billed as one audit.

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

When following internal links, stop adding pages for a site at this many, counting the addresses you listed. Maximum pages still applies to the whole run.

## `checkLinks` (type: `boolean`):

Request the links on each audited page and list the broken ones. This makes runs slower and costs nothing extra.

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

When checking links, check this many different links per page, in page order. linksNotChecked in the output counts the rest.

## Actor input object example

```json
{
  "urls": [
    "https://www.apify.com",
    "https://example.com"
  ],
  "maxPages": 100,
  "followInternalLinks": false,
  "maxPagesPerSite": 50,
  "checkLinks": false,
  "maxLinksPerPage": 25
}
```

# Actor output Schema

## `audits` (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://www.apify.com",
        "https://example.com"
    ],
    "maxPages": 100,
    "maxPagesPerSite": 50,
    "maxLinksPerPage": 25
};

// Run the Actor and wait for it to finish
const run = await client.actor("agarwoods/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://www.apify.com",
        "https://example.com",
    ],
    "maxPages": 100,
    "maxPagesPerSite": 50,
    "maxLinksPerPage": 25,
}

# Run the Actor and wait for it to finish
run = client.actor("agarwoods/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://www.apify.com",
    "https://example.com"
  ],
  "maxPages": 100,
  "maxPagesPerSite": 50,
  "maxLinksPerPage": 25
}' |
apify call agarwoods/seo-audit --silent --output-dataset

```

## MCP server setup

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