# SEO Audit Tool & Meta Tag Checker: Crawler, Broken Links (`data-done-right/seo-audit-tool`) Actor

SEO audit tool and SEO crawler: audit every page for technical SEO (status codes, redirect chains, titles, meta descriptions, H1s, canonicals, noindex, alt text, hreflang, Open Graph, JSON-LD). Works as a meta tag checker and broken link checker. Every page gets issues and a 0-100 score.

- **URL**: https://apify.com/data-done-right/seo-audit-tool.md
- **Developed by:** [Data Done Right](https://apify.com/data-done-right) (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 $25.60 / 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.
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 Tool & Meta Tag Checker: Crawler, Broken Links

This **SEO audit tool** crawls a website the way a search engine does and checks every page for technical SEO problems: HTTP status codes, redirect chains, titles, meta descriptions, H1s, canonicals, noindex, image alt text, hreflang, Open Graph, structured data (JSON-LD) and broken links. Each page comes back as one row with an `issues` list and a 0-100 score. A site `SUMMARY` record groups the duplicate titles, the broken links (with the pages that contain them) and the worst pages. It is built for SEO consultants, agencies, developers and site owners who want a repeatable technical SEO audit they can schedule, export to CSV/Excel or plug into their own tools through the API.

### What does this SEO audit check?

For every crawled page:

| Area | What you get | Issues it can raise |
|---|---|---|
| HTTP | `statusCode`, full `redirectChain`, `responseTimeMs`, `pageSizeBytes` | `http-4xx`, `http-5xx`, `fetch-failed`, `redirect-loop`, `redirected`, `redirect-chain`, `slow-response`, `large-html` |
| Title | `title`, `titleLength` | `title-missing`, `title-too-long` (>60), `title-too-short` (<30), `title-multiple`, `title-duplicate` |
| Meta description | `metaDescription`, `metaDescriptionLength` | `meta-description-missing`, `-too-long` (>160), `-too-short` (<70), `-multiple`, `-duplicate` |
| Headings | `h1`, `h1Count`, `headings` outline | `h1-missing`, `h1-multiple`, `heading-level-skipped` |
| Indexing | `canonical`, `canonicalIsSelf`, `robotsMeta`, `xRobotsTag`, `indexable`, `noindex` | `noindex`, `nofollow`, `canonical-missing`, `canonical-elsewhere`, `canonical-multiple`, `meta-refresh` |
| Content | `wordCount`, `lang`, `viewport` | `thin-content` (<200 words), `html-lang-missing`, `viewport-missing` |
| Images | `imagesCount`, `imagesMissingAlt`, samples | `images-missing-alt` |
| Links | `internalLinks`, `externalLinks`, `nofollowLinks`, `inlinks`, `brokenLinks`, `redirectedLinks` | `broken-internal-links`, `broken-external-links`, `redirected-links` |
| International | `hreflang` list | `hreflang-invalid` |
| Social | `openGraph`, `twitterCard` | `open-graph-missing`, `twitter-card-missing` |
| Structured data | `structuredDataTypes` (JSON-LD `@type`s, `@graph` included), `structuredDataErrors`, `microdataTypes` | `structured-data-invalid` |
| Security | `mixedContent` (HTTP resources on an HTTPS page) | `mixed-content` |

Duplicate titles and descriptions are compared across the whole crawl. Pages that are noindex or canonicalized to another URL are left out of the duplicate groups, because they are not competing in search.

**Page score:** 100 minus a fixed penalty for each distinct issue type (error 15, warning 6, notice 2). A page that returns 4xx/5xx or cannot be fetched scores 0.

### How to do an SEO audit of a website

1. Enter the **Website URL** (usually your home page).
2. Set **Max pages to audit** (start with 100) and, if you like, turn on **Also audit pages from the sitemap** so pages that nothing links to are found too.
3. Turn on **Check external links** if you also want outgoing links checked.
4. Run it. Open the dataset to see one row per page (worst pages first), and the `SUMMARY` record in the key-value store for the site overview.
5. Export to CSV, Excel or JSON, or schedule the run weekly to catch new problems.

### How to check broken links on a website

The crawl already knows the status of every page it visits. In addition, with **Check internal links that were not crawled** (on by default), every other internal link (pages beyond the page or depth limit, PDFs, images that are linked) gets a quick HEAD request (GET if the server refuses HEAD). With **Check external links**, each unique outgoing URL is checked once, too. You get:

- `brokenLinks` on each page row: the URL, status code or error (timeout, DNS, connection) and anchor text.
- `brokenLinks` in `SUMMARY`: every broken URL with `foundOnCount` and up to 20 `foundOn` pages, internal links first.
- `redirectedLinks`: links that point to a redirect instead of the final URL, with the final URL and number of hops.
- External sites that refuse bots (401, 403, 429, 999) are listed as `restrictedLinks`, not as broken, to avoid false alarms.

### Website SEO checker with a score for every page

Every page gets a 0-100 SEO score and a plain list of its issues (missing meta description, duplicate title, broken links, redirect chain, noindex...). Sort by score to see the worst pages first.

### SEO audit tool for agencies

Run it for each client site, schedule it monthly, and export to CSV, Excel or Google Sheets for your report. You can also call it from the Apify API, Make, Zapier or n8n.

### Use it in Claude, ChatGPT and Cursor

Connect Apify's MCP server (mcp.apify.com) and your AI assistant can audit a site itself. For example: "audit example.com and list the 10 worst SEO problems." The only required input is `startUrl`.

### Input example

```json
{
  "startUrl": "https://docs.apify.com/",
  "maxPages": 100,
  "maxDepth": 10,
  "useSitemap": true,
  "checkLinks": true,
  "checkExternalLinks": true,
  "maxLinkChecks": 1000,
  "excludeUrlPatterns": ["/tag/", "?sort="],
  "reportPages": "all"
}
```

`reportPages` can be `all`, `issues` (any issue), `warnings` (warnings or errors) or `errors`. You pay only for the pages returned; the `SUMMARY` always covers every audited page.

### Output example

One dataset row per page (shortened, from a real run):

```json
{
  "url": "https://docs.apify.com/api",
  "statusCode": 200,
  "score": 74,
  "issueCount": 7,
  "errors": 0,
  "warnings": 3,
  "notices": 4,
  "issues": [
    {"type": "meta-description-missing", "severity": "warning", "message": "No meta description."},
    {"type": "images-missing-alt", "severity": "warning", "message": "Images without an alt attribute.", "detail": 7},
    {"type": "title-duplicate", "severity": "warning", "message": "Same title as other pages on the site.", "detail": "2 pages share it"},
    {"type": "redirected-links", "severity": "notice", "message": "Links that point to a redirect instead of the final URL.", "detail": 1}
  ],
  "title": "Apify Documentation",
  "titleLength": 19,
  "metaDescription": null,
  "h1": ["Apify API documentation"],
  "canonical": "https://docs.apify.com/api",
  "canonicalIsSelf": true,
  "indexable": true,
  "wordCount": 511,
  "responseTimeMs": 41,
  "pageSizeBytes": 56631,
  "redirectChain": [],
  "inlinks": 9,
  "internalLinks": 27,
  "externalLinks": 24,
  "brokenLinksCount": 0,
  "redirectedLinks": [{"url": "https://docs.apify.com/cli", "finalUrl": "https://docs.apify.com/cli/docs", "hops": 1, "status": 200, "internal": true}],
  "imagesMissingAlt": 7,
  "hreflang": [{"lang": "en", "href": "https://docs.apify.com/api"}, {"lang": "x-default", "href": "https://docs.apify.com/api"}],
  "openGraph": {"title": "Apify Documentation", "image": "https://apify.com/og-image/docs-article"},
  "twitterCard": {"card": "summary_large_image"},
  "structuredDataTypes": [],
  "mixedContent": []
}
```

The `SUMMARY` record (key-value store) looks like this:

```json
{
  "site": "docs.apify.com",
  "pagesAudited": 10,
  "averageScore": 89.7,
  "pagesWithErrors": 1,
  "pagesAffectedByIssue": {"redirected-links": 10, "heading-level-skipped": 8, "images-missing-alt": 3, "broken-internal-links": 1},
  "worstPages": [{"url": "https://docs.apify.com/api", "score": 74, "issues": ["meta-description-missing", "images-missing-alt"]}],
  "duplicateTitles": [{"value": "Apify Documentation", "count": 2, "pages": ["https://docs.apify.com/api", "https://docs.apify.com/"]}],
  "duplicateMetaDescriptions": [],
  "brokenLinksCount": 1,
  "brokenLinks": [{"url": "https://docs.apify.com/residential_proxy", "status": 404, "internal": true, "foundOnCount": 1, "foundOn": ["https://docs.apify.com/proxy"]}],
  "redirectedLinksCount": 6,
  "stoppedBecause": "maxPages (10) reached; 127 more URLs were queued"
}
```

Problems that did not stop the run (robots.txt unreachable, unreadable sitemap) are listed in `notes` and in a `NOTES` record.

### How the crawler behaves

- **Same site only.** It follows links on the start URL's host. `www` and non-`www` count as one site; subdomains only when you turn on **Include subdomains**. If the start URL redirects (http to https, or to a new domain), the final host becomes the site.
- **Respects robots.txt.** It identifies itself as `SEOAuditBot` (User-Agent `Mozilla/5.0 (compatible; SEOAuditBot/1.0; Apify SEO Audit Tool actor; honors robots.txt)`), skips disallowed URLs (also when a redirect points into them), honors `Crawl-delay` up to 10 s, and does not follow links from pages marked `nofollow`. If robots.txt itself returns a 5xx error or cannot be reached, the site is treated as disallowed (RFC 9309) and the run says so. External link checks are single requests to each linked URL, like a visitor clicking the link; they do not crawl other sites.
- **Polite by default.** 5 parallel requests (adjustable 1 to 10), at most 2 at a time to any external host, one retry on timeouts, 429 and 503.
- **Caps that also cap cost:** Max pages, Max link depth, Max link checks.

### How much does it cost

Pay per event, platform usage included:

- **`page`: $0.032 per audited page returned** ($32 per 1,000 pages). Internal link checks are included.
- **`external-link-check`: $0.0008 per unique external URL checked** ($0.80 per 1,000), only when **Check external links** is on.

Example: a 500-page site with 2,000 unique external links costs 500 x $0.032 + 2,000 x $0.0008 = $17.60. When the run's spending limit is near, the actor lowers the page count up front and stops external checks before they would eat into the pages you will receive.

### Use cases

- **Technical SEO audit** for a client site before a proposal or a migration.
- **Weekly broken link check** on a schedule, exported to Google Sheets or Slack.
- **Pre-launch QA**: catch noindex left on, missing canonicals, staging links and mixed content.
- **Content clean-up**: thin pages, duplicate titles and meta descriptions, missing H1s and alt text.
- **Agencies and developers**: call it through the Apify API and feed the JSON into your own reports.

### FAQ

#### What is a technical SEO audit?

A check of the things that decide whether search engines can crawl, index and understand a site: status codes, redirects, canonicals, robots directives, titles, descriptions, headings, structured data, hreflang and internal links. That is exactly what this actor reports, page by page.

#### How much does an SEO audit cost?

A manual audit by an agency or consultant is priced by the hour or by the project. The crawl-and-check part costs $0.032 per page here, so a 200-page site is $6.40. You still need a person to decide which fixes matter most.

#### Is there a free SEO audit tool?

Apify's free plan includes $5 of platform credit every month. That covers a full audit of about 150 pages at $0.032 per page.

#### How do I check my website's SEO score?

Enter your home page URL and run. Each page row has a `score` from 0 to 100 and its list of issues, and the run summary gives the site-wide totals.

#### How do broken link checkers work?

They request every linked URL and read the HTTP status. A 4xx or 5xx status, a timeout or a DNS failure means the link is broken. This tool sends a HEAD request and falls back to GET when a server refuses HEAD, which avoids most false alarms.

#### Is a broken link checker accurate?

Mostly. Some sites (LinkedIn, some shops behind bot protection) answer automated requests with 403, 429 or 999 even though the page works in a browser. These are reported separately as `restrictedLinks` rather than as broken.

#### Does it render JavaScript?

No. It reads the HTML the server sends, like most SEO crawlers in their default mode. Sites that build their content only in the browser (some single-page apps) will show few links and thin content. Server-rendered and static sites (most WordPress sites, docs sites and server-rendered frameworks) work well.

#### Can I audit a site that blocks crawlers in robots.txt?

No. Disallowed URLs are skipped and listed as skipped. To audit your own staging site, allow `SEOAuditBot` in its robots.txt.

### Limits

- One website per run (run it several times or schedule it for more sites).
- No JavaScript rendering, no login-protected pages, no Core Web Vitals (see the PageSpeed tool below).
- HTML larger than 5 MB is read only up to 5 MB.
- Results are written when the crawl ends (duplicate titles and broken links need the whole site). If the run's timeout is near, the crawl stops early on its own and still delivers what it audited (`stoppedBecause` says so). A run you abort by hand returns nothing.
- Images are counted and checked for alt text, but image URLs are not link-checked unless they are linked with `<a href>`.

### Related tools

- **Sitemap Extractor: All URLs + New Pages Monitor**: export every URL from a site's sitemaps and get only new pages on later runs.
- **Bulk PageSpeed & Core Web Vitals Checker**: Lighthouse scores and Core Web Vitals for a list of URLs.
- **Keyword Suggestions Tool: Google, YouTube, Amazon, Bing**: find the keywords to target once the technical issues are fixed.

# Actor input Schema

## `startUrl` (type: `string`):

The page to start from, usually the home page. The crawler stays on this website (same domain; www and non-www count as the same site) and follows links from here. Example: https://www.example.com/.

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

Stops after this many pages. Each audited page you receive is one billed result, so this also caps the cost.

## `maxDepth` (type: `integer`):

How many clicks away from the start URL to go. 0 = only the start URL, 1 = the start URL and the pages it links to.

## `useSitemap` (type: `boolean`):

Add the URLs listed in the site's XML sitemap (found via robots.txt or /sitemap.xml) to the crawl, so pages without internal links are audited too. Each row then shows inSitemap.

## `sitemapUrls` (type: `array`):

Exact sitemap or sitemap index URLs to read instead of auto-discovery. Implies 'Also audit pages from the sitemap'.

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

Treat subdomains (blog.example.com, docs.example.com) as part of the site. Off = only the start URL's host (plus its www / non-www twin).

## `includeUrlPatterns` (type: `array`):

Optional. e.g. /blog/ or /en/. Prefix with re: for a regular expression. The start URL is always audited.

## `excludeUrlPatterns` (type: `array`):

Optional. e.g. /tag/, ?sort=, /cart. Prefix with re: for a regular expression. Links to skipped URLs are still link-checked.

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

Send a quick HEAD request to every internal link the crawl did not visit (beyond Max pages or depth, PDFs, images), so broken links are found site-wide. Included in the page price.

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

Also check every outgoing link to other websites for 4xx/5xx errors, timeouts and redirects. Billed per unique external URL checked (external-link-check event).

## `maxLinkChecks` (type: `integer`):

Upper limit on extra link checks, applied separately to internal links that were not crawled and to external links (so 1000 = up to 1000 of each). Links that point to pages already audited never count against it.

## `reportPages` (type: `string`):

Which audited pages to return. You pay only for returned pages; the SUMMARY record always covers every audited page.

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

Requests in flight at once. Keep it low for small servers. A Crawl-delay in robots.txt is honored (up to 10 s).

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

A page or link that does not answer in time is reported as a timeout (one retry).

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

Usually not needed. Use it only if your own server rate-limits the crawler by IP.

## Actor input object example

```json
{
  "startUrl": "https://docs.apify.com/",
  "maxPages": 10,
  "maxDepth": 10,
  "useSitemap": false,
  "includeSubdomains": false,
  "checkLinks": true,
  "checkExternalLinks": false,
  "maxLinkChecks": 100,
  "reportPages": "all",
  "maxConcurrency": 5,
  "requestTimeoutSecs": 20,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

All result items of this run (the default dataset), as JSON, CSV, Excel and more.

## `records` (type: `string`):

Key-value store records: the run summary, notes on any problems, and any files.

# 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 = {
    "startUrl": "https://docs.apify.com/",
    "maxPages": 10,
    "maxLinkChecks": 100,
    "reportPages": "all"
};

// Run the Actor and wait for it to finish
const run = await client.actor("data-done-right/seo-audit-tool").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 = {
    "startUrl": "https://docs.apify.com/",
    "maxPages": 10,
    "maxLinkChecks": 100,
    "reportPages": "all",
}

# Run the Actor and wait for it to finish
run = client.actor("data-done-right/seo-audit-tool").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 '{
  "startUrl": "https://docs.apify.com/",
  "maxPages": 10,
  "maxLinkChecks": 100,
  "reportPages": "all"
}' |
apify call data-done-right/seo-audit-tool --silent --output-dataset

```

## MCP server setup

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

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/FWgtd3bVx1RO4GIKb/builds/bYssNFfcb8i7ZPbe0/openapi.json
