# SEO Audit Tool: On-Page Checks, Crawl & Broken Links (`private_image_hg9/seo-audit-onpage`) Actor

Audit web pages or crawl whole sites for on-page SEO: titles, meta descriptions, headings, canonicals, indexability, hreflang, Open Graph, JSON-LD, images without alt, mixed content and broken links. One row per page with issues and a 0-100 score. Pay only for pages audited.

- **URL**: https://apify.com/private_image_hg9/seo-audit-onpage.md
- **Developed by:** [Michael Shanker](https://apify.com/private_image_hg9) (community)
- **Categories:** SEO tools, Developer tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$3.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.

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: On-Page Checks, Crawl & Broken Links

Check a list of pages, or crawl whole websites, for the on-page SEO problems that cost rankings and clicks: **missing or overlong titles and meta descriptions, missing H1s, noindex tags, canonicals that point elsewhere, broken hreflang, missing Open Graph and structured data, images without alt text, mixed content, redirect chains, error pages and broken internal links.**

You get **one row per URL** with every fact read from the page, a list of issues (each with a code, a severity and a plain-English message) and a **score from 0 to 100**. The run summary adds the most common issues and, for crawls, the **duplicate titles and meta descriptions** across each site.

**You pay only for HTML pages that answered HTTP 200 and were audited: $3 per 1,000 pages.** Redirects, 404s and other error pages, PDFs and images, pages robots.txt keeps the tool away from, and unreachable or invalid addresses still get a row, free.

### Who it's for

- **SEO consultants and agencies** running quick technical audits for clients or prospects.
- **Site owners and marketers** checking a launch, a migration or a batch of new landing pages.
- **Developers** adding SEO checks to CI or monitoring: call it from the API, filter rows on `issues.code`, alert on `score`.

### What it checks

| Area | What's in the row | Issues it can raise |
|---|---|---|
| Title | `title`, `titleLength` | missing, longer than 60 or shorter than 30 characters, more than one `<title>` |
| Meta description | `metaDescription`, `metaDescriptionLength` | missing, over 160 or under 70 characters, more than one |
| Headings | `h1` (list), `h2Count` | no H1, several H1s, empty H1 |
| Indexing | `indexable` + `indexabilityReason`, `metaRobots`, `xRobotsTag`, `canonical`, `canonicalPointsElsewhere` | noindex in meta robots or in the X-Robots-Tag header, robots.txt blocks Googlebot, canonical missing, invalid, duplicated or pointing to another URL, nofollow |
| International | `lang`, `hreflang` (list of language and URL) | no `lang` attribute, invalid hreflang codes, hreflang set that leaves out the page itself |
| Sharing and schema | `openGraph`, `twitterCard` (tags present), `jsonLdTypes` | no Open Graph or missing og:title/description/image, no twitter:card, no JSON-LD, JSON-LD that isn't valid JSON |
| Content and media | `wordCount`, `imagesTotal`, `imagesWithoutAlt` | under 200 words, images with no alt attribute |
| Links | `internalLinks`, `externalLinks`, `nofollowLinks`, and with link checks on `linksChecked`, `brokenLinks` | broken internal links |
| Delivery | `statusCode`, `redirectChain`, `responseTimeMs`, `contentType`, `bytes`, `hasViewport`, `mixedContentCount` | plain http, redirect or redirect chain, slow response (over 2 s), page over 5 MB, no viewport tag, http resources on an https page, meta refresh |
| Site-wide (crawl) | in the run summary | duplicate titles and duplicate meta descriptions across a site's indexable pages |

**Score:** 100, minus 15 for each error, 5 for each warning and 1 for each notice, never below 0. A page with no issues scores 100. Errors are things that break pages for users or search engines (missing title, mixed content, broken links); warnings usually cost rankings or clicks; notices are worth a look.

### How it compares

| | This tool | Other SEO audit tools in the Store |
|---|---|---|
| Price per 1,000 pages | **$3** | from about $2 to about $40 (the most used one charges about $40) |
| What you're charged for | only HTML pages audited; redirects, error pages, files and blocked pages are free | check each tool's pricing |
| Pages or crawl | both: an exact list of URLs, or a breadth-first crawl of each site | varies |
| How pages are read | plain HTTP, like a search engine's first pass: fast and cheap, but JavaScript isn't run | some run a real browser |

If your pages build their titles, meta tags or content with JavaScript in the browser, this tool sees the HTML the server sends, not the rendered page. Server-rendered and static sites (most CMS, e-commerce and documentation sites) are read completely.

### Input

| Field | What it does |
|---|---|
| **Pages or websites** (`startUrls`) | One per line: `https://www.example.com/pricing` or a bare domain like `example.com` (https is used). Request-list style objects (`{"url": "..."}`) work too. Repeats are audited once. |
| **What to audit** (`mode`) | `pages` (default): exactly the listed URLs, following any redirects. `crawl`: start at each URL and follow links on the same site, breadth first. |
| **Max pages per site** (`maxPagesPerSite`) | Crawl mode: how many URLs of each site are checked (default 50, max 5,000). Every row counts (redirects and error pages too), but only audited pages are charged. |
| **Also use the sitemap** (`includeSitemap`) | Crawl mode: add the pages listed in the site's sitemap (from robots.txt Sitemap lines, or `/sitemap.xml`, gzipped or not, sitemap indexes included). |
| **Check internal links** (`checkLinks`) | HEAD-check every internal link on each audited page (up to 100 per page, each link once per run) and list broken ones. No extra charge; runs take longer. |
| **Pages at once** (`maxConcurrency`) | Pages fetched in parallel across all sites (default 10). |
| **Requests per second per site** (`maxRequestsPerSecondPerHost`) | Default 2. www and the bare domain share one limit. |

Crawl a site:

```json
{
  "startUrls": ["https://www.example.com/"],
  "mode": "crawl",
  "maxPagesPerSite": 500,
  "includeSitemap": true,
  "checkLinks": true
}
```

Audit a list of landing pages:

```json
{
  "startUrls": ["https://www.example.com/pricing", "https://www.example.com/features", "example.org"],
  "mode": "pages"
}
```

### Output

One row per URL (shortened):

```json
{
  "url": "https://shop.example/products",
  "finalUrl": "https://shop.example/products",
  "status": "audited",
  "statusCode": 200,
  "score": 60,
  "indexable": false,
  "indexabilityReason": "noindex in meta robots",
  "title": "All Products – Mugs, Bowls, Plates, Vases and Kitchen Accessories from Shop Example",
  "titleLength": 83,
  "metaDescription": null,
  "h1": ["Products", "Shop all"],
  "canonical": "https://shop.example/products",
  "canonicalPointsElsewhere": false,
  "metaRobots": "noindex, follow",
  "lang": "en",
  "openGraph": [],
  "jsonLdTypes": [],
  "wordCount": 71,
  "imagesTotal": 2,
  "imagesWithoutAlt": 0,
  "internalLinks": 2,
  "mixedContentCount": 3,
  "hasViewport": false,
  "issues": [
    {"code": "title_too_long", "severity": "warning", "message": "The title is 83 characters; search results usually show about 60."},
    {"code": "noindex", "severity": "warning", "message": "Meta robots says noindex: search engines will leave this page out."},
    {"code": "mixed_content", "severity": "error", "message": "3 resource(s) load over http:// on this https page, e.g. http://cdn.shop.example/mug-small.jpg"}
  ],
  "errors": 1, "warnings": 4, "notices": 5,
  "redirectChain": [],
  "responseTimeMs": 212,
  "site": "shop.example",
  "crawlDepth": 1,
  "linkedFrom": "https://shop.example/"
}
```

`status` is one of:

- `audited`: an HTML page that answered HTTP 200 and was audited. **The only charged status.**
- `redirect`: crawl mode only, a redirect that wasn't followed because it leaves the site or lands on a page already in the audit (`finalUrl` says where it goes).
- `http_error`: the URL answered 4xx or 5xx (`statusCode`).
- `not_html`: a PDF, image or other file.
- `blocked_by_robots`: robots.txt asks tools like this one not to read it, so it wasn't.
- `failed`: no answer (timeout, connection refused, DNS failure).
- `invalid_url`: not a usable public web address, or a redirect to a local or private network address.

The `SUMMARY` record in the run's key-value store has counts per status, pages charged, the average score, how often each issue appeared, and per site the duplicate title and meta description groups (with their URLs), whether the page limit was reached and how many sitemap pages were found. Download rows as JSON, CSV or Excel, or send them to Google Sheets, Zapier, Make or n8n.

### Pricing

Pay per event: **$0.003 per page audited ($3 per 1,000)**. No start fee. Link checks, robots.txt and sitemap requests, redirects and every non-audited row are free. Set a maximum charge per run in the run options: the tool never starts a page it couldn't pay for and stops cleanly when the cap is reached (the summary says so).

### Good to know

- **No JavaScript.** Pages are read over plain HTTP. Content that only appears after scripts run in a browser isn't seen.
- **Polite by default.** It obeys robots.txt for its own user agent (`SEOAuditBot`), honors `Crawl-delay` up to 10 seconds, starts at most 2 requests per second per site, retries 429 and 5xx answers with backoff, and names itself in its User-Agent. It also checks robots.txt for Googlebot, so a page this tool may read but Google may not is marked not indexable.
- **Stays on the site.** A crawl follows links on the same host, with `www.` and the bare domain counted as one site. Other subdomains (blog.example.com) and other domains are counted as external links but not crawled. If the start page redirects to a new domain, the crawl continues there.
- **What a crawl skips.** Links marked `rel="nofollow"`, links on pages with meta robots nofollow, and links to files (PDF, images, archives, CSS, JS) aren't queued. URLs with different query strings are different pages.
- **Duplicates.** Only indexable pages are compared (a page canonicalised elsewhere naturally shares its title). Rows are saved as pages are audited, so the second and later pages of a duplicate group get the `duplicate_title` or `duplicate_meta_description` issue naming the first; the summary lists every group in full.
- **Link checks** treat 4xx (except 401, 403 and 429, which usually mean the site blocks tools rather than a missing page), 5xx and no answer as broken. Links robots.txt disallows aren't checked.
- **Limits.** Pages are read up to 5 MB (larger ones are audited on their first 5 MB and flagged). Up to 10 sitemap files are read per site. Local, private and internal network addresses are never fetched, also when a redirect or a domain points to one.

This tool isn't affiliated with or endorsed by Google, X, or any website it reads.

### More tools from the same developer

- [Broken Link Checker: 404s, Redirects & Dead Links](https://apify.com/private_image_hg9/broken-link-checker)
- [Sitemap URL Extractor: All Site URLs, Lastmod & News](https://apify.com/private_image_hg9/sitemap-url-extractor)
- [Bulk URL Status Checker: Redirect Chain & Response Time](https://apify.com/private_image_hg9/bulk-url-status-redirect-checker)
- [Wayback Machine Scraper: Snapshots, Changes & Page Text](https://apify.com/private_image_hg9/wayback-machine-snapshots)
- [Website Screenshot & PDF: Full Page, Mobile, Batch URLs](https://apify.com/private_image_hg9/website-screenshot-pdf)
- [Domain WHOIS, DNS & SSL Checker: Expiry, SPF & DMARC](https://apify.com/private_image_hg9/domain-whois-dns-ssl-checker)

# Actor input Schema

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

One address per line: a full URL like https://www.example.com/pricing or a bare domain like example.com (https is used). In 'pages' mode each URL is audited once. In 'crawl' mode each one is the starting point of a crawl of its site. Repeats are audited once.

## `mode` (type: `string`):

'pages': exactly the URLs you list (redirects are followed). 'crawl': start at each URL and follow links on the same site, breadth first, up to the page limit below. www.example.com and example.com count as one site; other subdomains like blog.example.com are separate sites.

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

In crawl mode, how many URLs of each site are checked, counting every row of the site (audited pages, redirects, error pages and pages robots.txt keeps the tool away from). Only audited HTML pages are charged.

## `includeSitemap` (type: `boolean`):

Read the site's sitemap (the Sitemap lines in robots.txt, or /sitemap.xml) and add its pages to the crawl, so pages no link points to are found too. The crawl takes turns between links found on pages and sitemap pages.

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

Send a quick HEAD request to every internal link on each audited page (up to 100 per page, each link once per run) and list the broken ones (4xx, 5xx or no answer). No extra charge, but runs take longer.

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

How many pages are fetched in parallel across all sites. Each site still gets no more requests per second than the setting below.

## `maxRequestsPerSecondPerHost` (type: `integer`):

The most requests started per second to one site (www and bare domain together). A robots.txt Crawl-delay slows this further, up to 10 seconds. Keep it low on small sites.

## Actor input object example

```json
{
  "startUrls": [
    "https://crawlee.dev/"
  ],
  "mode": "pages",
  "maxPagesPerSite": 50,
  "includeSitemap": false,
  "checkLinks": false,
  "maxConcurrency": 10,
  "maxRequestsPerSecondPerHost": 2
}
```

# Actor output Schema

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

One row per URL: the page's SEO facts, its issues and a 0-100 score, or why it wasn't audited.

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

Counts per status, pages charged, the most common issues, duplicate titles and descriptions per site, and whether the spending cap was reached.

# 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/"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("private_image_hg9/seo-audit-onpage").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/"] }

# Run the Actor and wait for it to finish
run = client.actor("private_image_hg9/seo-audit-onpage").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/"
  ]
}' |
apify call private_image_hg9/seo-audit-onpage --silent --output-dataset

```

## MCP server setup

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

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/MpGkDGPHm8cdeerhq/builds/33IwRXSE4SsOgcWfA/openapi.json
