# Website SEO Audit: On-Page SEO Checker & Site Crawler (`fecklesspleb/website-seo-audit`) Actor

Crawl a website and run an on-page SEO audit of every page: missing or long titles and meta descriptions, H1s, canonicals, noindex, image alt text, thin content, Open Graph, JSON-LD, slow responses and duplicate titles. Issues and a 0 to 100 score per page.

- **URL**: https://apify.com/fecklesspleb/website-seo-audit.md
- **Developed by:** [Scott Williams](https://apify.com/fecklesspleb) (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

Pay per event

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: on-page SEO checker and crawler, $3 per 1,000 pages

Website SEO Audit crawls a website from a start page and checks every page it reaches for on-page SEO problems: missing or overlong titles, missing meta descriptions, missing H1s, canonical tags, `noindex`, images without alt text, thin content, Open Graph tags, JSON-LD errors, slow responses, and more. Each page gets a list of issues with a severity and a 0 to 100 score, so you can sort a whole site and fix the worst pages first.

It reads the HTML your server sends. No browser, no proxy, no login. It obeys robots.txt and crawl delays.

**At a glance:** $0.003 per page audited ($3.00 per 1,000 pages); pages that return an error are saved free. Pay per result with no subscription, set a maximum cost per run, export to JSON, CSV, Excel or HTML, or call it from the Apify API, n8n, Make or Zapier.

### Who it's for

- **SEO agencies and freelancers** running quick client audits or monthly site health checks.
- **Site owners and developers** checking a site after a redesign, CMS migration or template change.
- **Content teams** finding thin pages, duplicate titles and missing meta descriptions.
- **Anyone automating SEO checks:** schedule it weekly and compare scores between runs.

### What it checks on every page

| Area | Checks |
|---|---|
| Title | Missing, too long (over 60 characters), too short (under 20 characters), duplicated on another audited page |
| Meta description | Missing, too long (over 160 characters), too short (under 70 characters), duplicated on another audited page |
| Headings | Missing H1, more than one H1, number of H2s |
| Indexing | Canonical missing or pointing to another page, `noindex` and `nofollow` in meta robots or the `X-Robots-Tag` header |
| Social and structured data | Open Graph and Twitter card tags, missing `og:title`, JSON-LD types found, JSON-LD blocks that fail to parse |
| Content | Word count, thin content (under 200 words), images without alt text (with up to 5 example image URLs) |
| Technical | HTTP status, redirects, response time (slow over 2 seconds), HTML size (large over 1 MB), HTTPS, viewport tag, `lang` attribute, hreflang links |
| Links | Internal, external and nofollow link counts per page |

#### Issue codes and score

Every issue has a code and a severity (`error`, `warning` or `notice`). The score starts at 100 and loses a fixed number of points per issue (never below 0):

| Code | Severity | Points |
|---|---|---:|
| `http-error` | error | 40 |
| `missing-title` | error | 15 |
| `missing-meta-description` | error | 10 |
| `missing-h1` | error | 8 |
| `missing-viewport` | error | 6 |
| `thin-content` | warning | 5 |
| `slow-response` | warning | 5 |
| `duplicate-title` | warning | 5 |
| `not-https` | warning | 5 |
| `noindex` | notice | 5 |
| `title-too-long`, `title-too-short` | warning | 4 |
| `missing-canonical` | warning | 4 |
| `images-missing-alt` | warning | 4 |
| `meta-description-too-long`, `meta-description-too-short` | warning | 3 |
| `multiple-h1` | warning | 3 |
| `large-html` | warning | 3 |
| `duplicate-meta-description` | warning | 3 |
| `invalid-json-ld` | warning | 3 |
| `missing-lang` | warning | 2 |
| `canonical-to-other-page` | notice | 2 |
| `missing-open-graph` | notice | 2 |
| `redirected` | notice | 2 |

Duplicate titles and descriptions are detected among the pages audited in the same run for the same site.

### How to use it

1. Open the Actor in Apify Console and click **Try for free**.
2. Add one start URL per site in **Websites to audit** (usually the home page). The Actor follows links on the same host.
3. Set **Max pages per site**. The default is 10; raise it for a full audit.
4. Optional: turn on **Only pages with issues**, add **Exclude URL patterns** such as `/tag/`, or turn on **Include JSON-LD data**.
5. Click **Start**, then sort the dataset by `score` or export it to CSV or Excel.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `startUrls` | array (required) | `["https://www.python.org/"]` | Start page for each site. Links on the same host are crawled (`www.` and the bare domain count as the same host). |
| `maxPagesPerSite` | integer, 1 to 10,000 | 10 | Stop after this many pages per site. A sample run (start URLs exactly `["https://www.python.org/"]`, as in `{}` and the prefill) audits at most 2 pages. |
| `onlyPagesWithIssues` | boolean | false | Only save pages that have at least one issue. Skipped pages are not charged. |
| `includeJsonLd` | boolean | false | Add the parsed JSON-LD blocks to each page. Personal fields such as email, telephone and author are removed. |
| `excludePatterns` | array of strings | none | Regular expressions. Matching URLs are not crawled, for example `/tag/` or `\?page=`. |
| `maxResults` | integer | 0 | Cap on pages saved to the dataset. 0 means no cap. |

#### Example input

```json
{
  "startUrls": [{"url": "https://books.toscrape.com/"}],
  "maxPagesPerSite": 200,
  "onlyPagesWithIssues": true,
  "excludePatterns": ["/tag/"]
}
```

### Output

One dataset item per audited page. This is a real item from a 5-page test run on books.toscrape.com (a public scraping sandbox), shortened to the most used fields:

```json
{
  "site": "https://books.toscrape.com/",
  "url": "https://books.toscrape.com/",
  "finalUrl": "https://books.toscrape.com/",
  "statusCode": 200,
  "redirected": false,
  "responseTimeMs": 205,
  "htmlSizeKb": 50.1,
  "title": "All products | Books to Scrape - Sandbox",
  "titleLength": 40,
  "metaDescription": null,
  "metaDescriptionLength": 0,
  "canonicalUrl": null,
  "metaRobots": "NOARCHIVE,NOCACHE",
  "language": "en-us",
  "viewport": "width=device-width",
  "h1": ["All products"],
  "h1Count": 1,
  "h2Count": 0,
  "wordCount": 326,
  "imagesTotal": 20,
  "imagesMissingAlt": 0,
  "internalLinks": 94,
  "externalLinks": 0,
  "noindex": false,
  "jsonLdTypes": [],
  "issues": [
    {"code": "missing-meta-description", "severity": "error"},
    {"code": "missing-canonical", "severity": "warning"},
    {"code": "missing-open-graph", "severity": "notice"},
    {"code": "duplicate-title", "severity": "warning"}
  ],
  "issueCount": 4,
  "score": 79,
  "checkedAt": "2026-10-30T01:31:18.989426+00:00"
}
```

#### All output fields

| Field | Description |
|---|---|
| `site` | The start URL this page was found from |
| `url`, `finalUrl` | Requested URL and the URL after redirects |
| `statusCode`, `redirected`, `responseTimeMs`, `htmlSizeKb` | HTTP status, whether it redirected, response time in ms, HTML size in KB |
| `xRobotsTag` | `X-Robots-Tag` response header |
| `title`, `titleLength` | Page title and its length |
| `metaDescription`, `metaDescriptionLength`, `metaKeywords` | Meta description, its length, and meta keywords |
| `canonicalUrl`, `metaRobots`, `noindex`, `nofollow` | Canonical link, meta robots, and the parsed noindex and nofollow flags |
| `language`, `charset`, `viewport`, `themeColor`, `faviconUrl` | Basic head tags |
| `feedUrls`, `hreflang` | RSS/Atom feed links and hreflang alternates (`lang`, `url`) |
| `openGraph` | `title`, `description`, `image`, `type`, `url`, `site_name`, `locale` |
| `twitterCard` | `card`, `title`, `description`, `image`, `site` |
| `publishedTime`, `modifiedTime` | Article dates when the page declares them |
| `jsonLd`, `jsonLdTypes`, `jsonLdErrors` | Parsed JSON-LD (only with `includeJsonLd`), the schema.org types found, and the number of blocks that failed to parse |
| `h1`, `h1Count`, `h2Count`, `wordCount` | Headings and visible word count |
| `imagesTotal`, `imagesMissingAlt`, `imagesMissingAltSample` | Image counts and up to 5 image URLs without alt text |
| `internalLinks`, `externalLinks`, `nofollowLinks` | Link counts |
| `issues`, `issueCount`, `score` | Issues found (`code`, `severity`), how many, and the 0 to 100 score |
| `checkedAt` | When the page was fetched (UTC) |
| `error` | Only on pages that could not be fetched |

The `RUN_SUMMARY` record in the key-value store has, per site, the number of pages audited, pages skipped by robots.txt, the average score, a count of every issue code, and a note when the crawl stopped at `maxPagesPerSite` or at your result or cost limit.

### Pricing

This Actor uses pay-per-event pricing. You pay for pages audited, not for compute time.

| Event | Price |
|---|---|
| Page audited (one dataset item) | $0.003 |
| Actor start (charged by Apify, once per GB of run memory; 1 event at the default 1 GB) | $0.00005 |

Examples:

- Audit a 10-page site: 10 × $0.003 + $0.00005 = **about $0.03**.
- Audit 500 pages: **about $1.50**.
- Audit 1,000 pages: **about $3.00**.

Pages skipped by **Only pages with issues** are not saved and not charged. Pages blocked by robots.txt are not fetched and not charged. Pages that return an error (HTTP 400 and above, or no response) are saved with the `http-error` issue, free of charge. They still count toward **Max pages in dataset**.

**Set a maximum cost per run.** In Apify Console set **Maximum cost per run**, or pass `maxTotalChargeUsd` through the API. The Actor stops cleanly when the limit is reached and records it in `RUN_SUMMARY` (`stoppedByCostLimit`).

### Integrations and API

The Actor ID is `fecklesspleb/website-seo-audit` (in API URLs: `fecklesspleb~website-seo-audit`).

- **n8n:** use the Apify node, choose **Run an Actor** with this Actor ID, then read the dataset items. Send low scores to Slack, email or a sheet.
- **Make:** use the Apify module **Run an Actor**, then **Get Dataset Items**.
- **Zapier:** use the Apify app with **Run Actor**, then a dataset items step.
- **Schedules:** create an Apify schedule to run the audit weekly or monthly, and a webhook to be notified when a run finishes.
- **AI agents:** call it through the Apify API, the Apify clients, or Apify's MCP server.

#### Run it with curl

```bash
curl -X POST "https://api.apify.com/v2/acts/fecklesspleb~website-seo-audit/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"startUrls": [{"url": "https://books.toscrape.com/"}], "maxPagesPerSite": 50, "onlyPagesWithIssues": true}'
```

#### Python

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("fecklesspleb/website-seo-audit").call(
    run_input={"startUrls": [{"url": "https://books.toscrape.com/"}], "maxPagesPerSite": 50},
    max_total_charge_usd=1,
)
pages = client.dataset(run["defaultDatasetId"]).list_items().items
for page in sorted(pages, key=lambda p: p.get("score") or 0):
    print(page.get("score"), page["url"], [i["code"] for i in page.get("issues", [])])
```

#### JavaScript

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });
const run = await client.actor('fecklesspleb/website-seo-audit').call({
    startUrls: [{ url: 'https://books.toscrape.com/' }],
    maxPagesPerSite: 50,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.filter((p) => p.score < 80).map((p) => p.url));
```

### Limits

- It reads the HTML the server returns and does not run JavaScript. Pages that build their content only in the browser may show up as thin or missing tags.
- It does not run Lighthouse and does not measure Core Web Vitals or page speed scores. `responseTimeMs` is the server response time seen by the crawler.
- It crawls one host per start URL and does not follow links to other domains or subdomains.
- Links on pages with a `nofollow` meta robots tag are not followed.
- On the Apify platform, pages whose address is private, loopback or link-local (also after a redirect) are not fetched. They are output free with `statusCode: null` and `error: "Blocked: private, loopback and link-local addresses are not allowed"`.
- Duplicate title and description checks only compare pages audited in the same run.
- The crawl is polite: at most about 2 requests per second per host, slower when robots.txt sets a crawl delay.

### Common searches this Actor answers

- website SEO audit tool
- on-page SEO checker for a whole site
- SEO crawler that exports to CSV or Excel
- find pages with missing meta descriptions
- find duplicate page titles
- check H1 tags on every page
- find images missing alt text
- find noindex pages and canonical problems
- bulk SEO check via API

### FAQ

**How do I run an SEO audit of my whole website?**
Put your home page in **Websites to audit** and set **Max pages per site** to the number of pages you want checked (up to 10,000). The Actor follows internal links from page to page.

**Can I audit several websites in one run?**
Yes. Add one start URL per site. Each site is crawled separately and gets its own section in `RUN_SUMMARY`.

**How is the SEO score calculated?**
Each page starts at 100 and loses the points listed in the issue table above for each issue found. The score is a quick way to sort pages; it is not a Google ranking factor.

**Can I find only the pages with missing meta descriptions or titles?**
Turn on **Only pages with issues** and filter the dataset by issue code, for example `missing-meta-description` or `missing-title`.

**Does it check page speed or Core Web Vitals?**
No. It records the server response time and flags responses over 2 seconds, but it does not run Lighthouse.

**Why are some pages missing from the results?**
They are disallowed by robots.txt, on another host, excluded by your patterns, linked only from a `nofollow` page, or beyond **Max pages per site**. The run summary shows pages skipped by robots.txt and whether the crawl stopped early.

**Does it work on JavaScript sites?**
It audits the HTML the server sends. Server-rendered and static sites work well. For sites that render everything in the browser, results reflect the initial HTML only.

**Can I export the audit to Excel or Google Sheets?**
Yes. Apify datasets export to CSV, Excel, JSON and HTML, and you can send them to Google Sheets through n8n, Make or Zapier.

**Is it legal to run an SEO audit on a website?**
The Actor reads public pages the way a search engine does, obeys robots.txt and crawl delays, and never logs in. Use it on sites you own or are allowed to audit.

### Related Actors

- [Broken Link Checker](https://apify.com/fecklesspleb/broken-link-checker): find 404s and dead links and images on the same site.
- [Website Metadata Extractor](https://apify.com/fecklesspleb/website-metadata-extractor): pull titles, meta tags, Open Graph and JSON-LD from a list of URLs without crawling.
- [Tech Stack Detector](https://apify.com/fecklesspleb/tech-stack-detector): see which CMS, frameworks, analytics and hosting a website uses.

# Changelog

This Actor's version history is a separate document: https://apify.com/fecklesspleb/website-seo-audit/changelog.md

# Actor input Schema

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

Home page or any start page of each site. The Actor crawls internal links on the same host. Plain URLs or {"url": ...} objects both work.

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

Stop crawling a site after this many pages.

## `onlyPagesWithIssues` (type: `boolean`):

Skip pages with no issues (they are not charged).

## `includeJsonLd` (type: `boolean`):

Add the parsed JSON-LD blocks to each page. Types are always included.

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

Regular expressions. Matching URLs are not crawled, for example /tag/ or ?page=

## `maxResults` (type: `integer`):

Cap per run. 0 means no cap.

## Actor input object example

```json
{
  "startUrls": [
    "https://www.python.org/"
  ],
  "maxPagesPerSite": 2,
  "onlyPagesWithIssues": false,
  "includeJsonLd": false,
  "maxResults": 0
}
```

# Actor output Schema

## `results` (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://www.python.org/"
    ],
    "maxPagesPerSite": 2
};

// Run the Actor and wait for it to finish
const run = await client.actor("fecklesspleb/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": ["https://www.python.org/"],
    "maxPagesPerSite": 2,
}

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

```

## MCP server setup

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