# Site Audit: Full-Site SEO Audit with Fixes (`bjgomes/site-audit-seo`) Actor

Audit every page of a website for SEO problems in one run. Each page gets a score from 0 to 100, its issues and a one-line fix for each. Run it again and each page is marked new, changed or unchanged, with pages that are gone. $0.005 per page, no browser, no monthly fee.

- **URL**: https://apify.com/bjgomes/site-audit-seo.md
- **Developed by:** [Brian Gomes](https://apify.com/bjgomes) (community)
- **Categories:** SEO tools, Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

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

Site Audit checks every page of a website for SEO problems, scores each page from 0 to 100, and gives you a one-line fix for every issue it finds.

Run it again next week and it tells you what changed: which pages are new, which got worse, which issues you fixed, and which pages are gone.

It works over plain HTTP. One request per page, no browser. That keeps it fast and cheap: **$0.005 per page audited**. A 500-page site costs $2.50.

### What it checks on every page

| Check | Issue id | Severity |
|---|---|---|
| The page answers with an error (4xx or 5xx) | `http-error` | error (the page scores 0) |
| The page redirects before it answers. The full chain is listed | `redirect` | warning |
| Slow response, over 3,000 ms by default | `slow-response` | warning |
| Title missing or empty | `title-missing` | error |
| Title over 60 or under 10 characters | `title-too-long`, `title-too-short` | notice |
| Same title as another page | `title-duplicate` | warning |
| More than one title element | `title-multiple` | notice |
| Meta description missing | `meta-description-missing` | warning |
| Meta description over 160 or under 50 characters | `meta-description-too-long`, `meta-description-too-short` | notice |
| Same meta description as another page | `meta-description-duplicate` | warning |
| No H1 | `h1-missing` | warning |
| More than one H1 | `h1-multiple` | notice |
| No canonical link | `canonical-missing` | notice |
| A canonical that points to another URL | `canonical-elsewhere` | notice |
| noindex in meta robots or the X-Robots-Tag header | `noindex` | warning |
| noindex on a page the sitemap lists | `noindex-in-sitemap` | error |
| No `lang` attribute on `<html>` | `lang-missing` | notice |
| Images with no alt attribute (`alt=""` counts as present, since it marks a decorative image) | `images-missing-alt` | warning |
| Internal links that are broken, each listed with its status or error | `broken-internal-links` | error |
| Images, scripts, stylesheets or frames loaded over http on an https page | `mixed-content` | warning |
| HTML over 2 MB | `page-too-large` | warning |
| The page was cut off (over 5 MB, it stopped arriving, or its HTML couldn't be parsed past a point) | `page-incomplete` | warning |

### The score and the fixes

Every page starts at 100. It loses 30 for each error, 10 for each warning and 3 for each notice, and stops at 0. A page that answers with an error status scores 0.

Every issue comes with a fix you can hand to whoever runs the site. Two examples, word for word from the output:

- `title-too-long`: "Shorten the <title> to 60 characters or fewer so search results don't cut it off."
- `noindex-in-sitemap`: "Either remove noindex or remove the page from the sitemap: the two contradict each other."

The summary row at the end gives the site score (the average over the pages audited), the issue counts by type, and the ten fixes that matter most across the whole site, ranked by severity times the number of pages.

### What changed since the last run

This is the part you'll use every week.

The first run audits the site. Every run after that on the same start URL compares against the last one and marks each page:

- **new**: it wasn't there last time
- **changed**: its score, its issues, its status code or its visible text changed
- **unchanged**: nothing changed
- **removed**: it was there last time and isn't now

Each page row also carries `previousScore`, `newIssues` (issues that appeared) and `fixedIssues` (issues that went away). The summary row counts all of it, plus the pages that answered fine last time and now redirect or fail.

So you don't read a 500-row audit every week. You read what moved.

How it works: the Actor keeps a small record of each page's score, issues, status code and a fingerprint of its text, in your own account's key-value store named `site-audit-history`. Delete that store in Storage to start fresh. Turn off **Compare with the previous run** to skip it.

A run that stops early (at your page limit or your maximum charge) never calls a page removed. It keeps the pages it didn't reach in the record for next time.

To be told when something breaks, schedule the Actor with Apify Schedules and connect a webhook or an integration like Slack, email or Google Sheets.

### Sitemap or crawl

- **Auto (the default):** it reads the sitemap first. It looks for Sitemap lines in robots.txt, then /sitemap.xml and /sitemap\_index.xml, follows sitemap indexes and reads gzipped sitemaps. If the site has no sitemap, it follows links from the start page instead.
- **Sitemap only:** audits the pages the sitemap lists, including ones no menu links to anymore. Only pages found in a sitemap get the `noindex-in-sitemap` check.
- **Crawl only:** follows links from the start page, on the same site only (with or without www.), up to the depth you set. Links to files like images, PDFs, CSS and JS aren't followed as pages.

You can also give it a sitemap URL as the start URL, and it reads that sitemap directly.

### Page content as Markdown (optional)

Turn on **Include page content as Markdown** and each row gets the page's main content as Markdown: headings, paragraphs, lists and code blocks. It takes `<main>` or `<article>` when the page has one. Otherwise it takes the body and leaves out navigation, header, footer, sidebars and forms. Up to 50,000 characters per page. Link targets aren't kept. It costs nothing extra.

### Input

| Field | What it does | Default |
|---|---|---|
| `startUrl` | The website, like `https://example.com`, or a sitemap URL. | required |
| `maxPages` | Stop after auditing this many pages. Only pages that answer are counted and charged. 1 to 50,000. | 100 |
| `discovery` | `auto`, `sitemap` or `crawl`. | `auto` |
| `maxDepth` | When crawling, how many clicks from the start page to follow. 0 audits the start page only. | 5 |
| `checkInternalLinks` | Check each link to the same site once and list the broken ones on every page that has them. | true |
| `compareWithPreviousRun` | Mark each page new, changed, unchanged or removed since the last run. | true |
| `includeMarkdown` | Add the page's main content as Markdown. | false |
| `slowThresholdMs` | A page slower than this gets a slow-response issue. | 3000 |
| `maxConcurrency` | Requests in flight at once, 1 to 20. Never more than 4 to one host. | 5 |
| `requestTimeoutSecs` | How long to wait for any one request. | 20 |

The form starts at 10 pages, so your first audit costs $0.05.

Example input:

```json
{
  "startUrl": "https://example.com",
  "maxPages": 500,
  "discovery": "auto",
  "checkInternalLinks": true,
  "compareWithPreviousRun": true
}
```

### Output

Export the dataset as CSV, JSON, Excel, XML or HTML, or read it through the Apify API. It has four kinds of rows:

- one row per page audited (`type: page`)
- one row per page that disappeared since the last run (`type: page`, `change: removed`)
- an error row when something couldn't be read, like a missing sitemap (`type: error`)
- one summary row at the end (`type: summary`)

A page row holds: `url`, `audited`, `score`, `issueCount`, `issues` (each with `id`, `severity`, `message` and `fix`), `change`, `previousScore`, `newIssues`, `fixedIssues`, `statusCode`, `finalUrl`, `redirectCount`, `redirectChain`, `responseTimeMs`, `contentType`, `htmlBytes`, `title`, `titleLength`, `metaDescription`, `metaDescriptionLength`, `h1Count`, `canonicalUrl`, `noindex`, `lang`, `imagesMissingAlt`, `internalLinks`, `brokenInternalLinks` (up to 20 listed, all counted), `mixedContent` (up to 20 listed), `wordCount`, `contentHash`, `foundIn` (sitemap or crawl), `depth`, `markdown`, `markdownTruncated`, `error`, `startUrl`.

Example page row (example data, real field names):

```json
{
  "type": "page",
  "url": "https://example.com/pricing",
  "audited": true,
  "score": 57,
  "issueCount": 3,
  "issues": [
    {
      "id": "title-too-long",
      "severity": "notice",
      "message": "The title is 74 characters.",
      "fix": "Shorten the <title> to 60 characters or fewer so search results don't cut it off."
    },
    {
      "id": "meta-description-missing",
      "severity": "warning",
      "message": "The page has no meta description.",
      "fix": "Add a meta description, 70 to 160 characters, that sums up the page."
    },
    {
      "id": "broken-internal-links",
      "severity": "error",
      "message": "1 internal link(s) are broken.",
      "fix": "Fix or remove the links listed in brokenInternalLinks."
    }
  ],
  "change": "changed",
  "previousScore": 97,
  "newIssues": ["broken-internal-links", "meta-description-missing"],
  "fixedIssues": [],
  "statusCode": 200,
  "finalUrl": "https://example.com/pricing",
  "redirectCount": 0,
  "responseTimeMs": 412,
  "title": "Pricing plans for teams of every size, billed monthly or yearly | Example",
  "titleLength": 74,
  "metaDescription": null,
  "h1Count": 1,
  "canonicalUrl": "https://example.com/pricing",
  "noindex": false,
  "lang": "en",
  "imagesMissingAlt": 0,
  "internalLinks": 38,
  "wordCount": 912,
  "foundIn": "sitemap",
  "depth": 0,
  "error": null
}
```

Any text in the output that starts with `=`, `+`, `-` or `@` (a title, a meta description, Markdown, a header value, a URL) gets a leading `'`, so it can't run as a formula when you open the dataset in a spreadsheet.

### Pricing

You pay per page audited. There's no monthly fee.

- **Each page audited: $0.005.** A page counts when it answers with a status code, errors and redirects included.
- **Free:** pages skipped for robots.txt, pages that never answer, internal link checks, Markdown, removed-page rows, error rows and the summary row.
- **Discounts:** on Apify's discount tiers the price drops to $0.0045 (Bronze), $0.004 (Silver) and $0.0035 (Gold and above).
- Apify's standard start fee applies to each run.

Worked examples, at the base price:

- A 100-page site: $0.50 per audit.
- A 500-page site: $2.50 per audit. Every week, about $10 a month.
- A 5,000-page site: $25 per audit.

For a hard ceiling, set `maxPages` or a maximum charge per run. The run stops cleanly when it reaches your maximum charge.

### Limits

- **No JavaScript rendering.** A page built in the browser by a client-side framework is audited on the HTML the server sends, which may be thin.
- **Some pages are fetched twice when crawling.** Each internal link is checked once per run, up to 100 new links per page, and the link check doesn't read the page body. When crawling, a page that was link-checked is requested again when its turn comes to be audited. You're charged once.
- It stops on its own when a site stops answering: 20 requests in a row with no answer, or 10 minutes spent waiting.
- No proxies, no logins, no forms submitted.

### robots.txt and good manners

- It respects robots.txt (RFC 9309). If robots.txt can't be reached, nothing on that site is fetched, and a row says why.
- It honours Crawl-delay up to 10 seconds, and sends at most 4 requests at a time to one host.
- It identifies itself honestly as `SiteAuditHTTP/0.1`.

### Public websites only

Addresses on private networks, loopback and cloud metadata ranges are refused, including when a public page redirects to one.

### FAQ

**How do I run an SEO audit on my whole website?**
Put your homepage in `startUrl` and run it. Sort the page rows by score, lowest first, and work from the summary row's top ten fixes.

**How do I see only what changed since last week?**
Filter the page rows on `change`. Anything `changed`, `new` or `removed` moved since the last run. `newIssues` and `fixedIssues` say exactly what.

**Why does a page have a lower score than last time?**
Look at `newIssues` on that row. Each one has a message and a fix.

**Can I call it from my own code?**
Yes. Start it and read the results through the Apify API or the Python and JavaScript clients.

### Integrations

This Actor runs on Apify, so everything the platform does works with it.

- **API.** Start a run and read the results from your own code, in any language. [Apify API](https://docs.apify.com/integrations/api)
- **Schedules.** Run it weekly and each run tells you what changed since the last one. [Schedules](https://docs.apify.com/actors/running/schedules)
- **Tasks.** Save your input once as a task and run it again with one click, or on a schedule. [Tasks](https://docs.apify.com/actors/running/tasks)
- **Webhooks.** Get a call to your own URL when a run succeeds or fails. [Webhooks](https://docs.apify.com/integrations/webhooks)
- **Google Sheets and Drive.** Download the results as CSV or Excel and open them in Sheets. Or add a Google Sheets step after this Actor in Make or Zapier. Or send the files to a Google Drive folder. [Datasets and exports](https://docs.apify.com/storage/dataset), [Google Drive](https://docs.apify.com/integrations/drive)
- **Zapier, Make and n8n.** Start a run from another app and send the results on to the next step. [Zapier](https://docs.apify.com/integrations/zapier), [Make](https://docs.apify.com/integrations/make), [n8n](https://docs.apify.com/integrations/n8n)
- **AI agents (MCP).** Claude, ChatGPT and other MCP clients can find and run this Actor through the Apify MCP server. [Apify MCP server](https://docs.apify.com/integrations/mcp)
- **Other Actors.** Pass this Actor's results to another Actor when a run finishes. [Actor-to-Actor](https://docs.apify.com/integrations/actors)

### Works with

- [Sitemap URL Extractor & Status Checker](https://apify.com/bjgomes/sitemap-url-extractor-status-checker): the full URL list of a site, with lastmod and status codes.
- [Broken Link Checker from Sitemap](https://apify.com/bjgomes/broken-link-checker-from-sitemap): the broken links on every page in a sitemap, each with the page it was found on.
- [Bulk PageSpeed Insights for a Sitemap](https://apify.com/bjgomes/bulk-pagespeed-insights-for-a-sitemap): performance score and Core Web Vitals for every URL in a sitemap.

### Support

Found a bug or need a feature? Open an issue on this Actor's **Issues** tab. Include the run ID and the URL you gave it.

# Actor input Schema

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

The website to audit, e.g. https://example.com. Pages are found from its sitemap (robots.txt, /sitemap.xml, /sitemap\_index.xml) or, when it has none, by following links on the same site. A sitemap URL works too.

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

Stop after auditing this many pages. Only pages that answer are counted and charged.

## `discovery` (type: `string`):

Auto reads the sitemap and falls back to following links on the same site when there is no sitemap.

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

When crawling, how many clicks from the start page to follow. 0 audits the start page only.

## `checkInternalLinks` (type: `boolean`):

Request each link to the same site once and list the broken ones on every page that has them.

## `compareWithPreviousRun` (type: `boolean`):

Mark each page new, changed or unchanged since this Actor last audited the same website, and list pages that are gone. The record is kept in your account's key-value store named site-audit-history.

## `includeMarkdown` (type: `boolean`):

Add the page's main content as Markdown (headings, paragraphs, lists and code; navigation, header, footer and sidebars left out; up to 50,000 characters per page).

## `slowThresholdMs` (type: `integer`):

A page that takes longer than this to answer gets a slow-response issue.

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

Requests in flight at once. Never more than 4 at a time to one host, and robots.txt Crawl-delay is honoured.

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

How long to wait for any single request.

## Actor input object example

```json
{
  "startUrl": "https://apify.com",
  "maxPages": 10,
  "discovery": "auto",
  "maxDepth": 5,
  "checkInternalLinks": true,
  "compareWithPreviousRun": true,
  "includeMarkdown": false,
  "slowThresholdMs": 3000,
  "maxConcurrency": 5,
  "requestTimeoutSecs": 20
}
```

# Actor output Schema

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

One row per page with its score, issues and fixes, plus a summary row for the site.

# 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://apify.com",
    "maxPages": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("bjgomes/site-audit-seo").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://apify.com",
    "maxPages": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("bjgomes/site-audit-seo").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://apify.com",
  "maxPages": 10
}' |
apify call bjgomes/site-audit-seo --silent --output-dataset

```

## MCP server setup

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

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/h1X6hwpjrTmt9DrNp/builds/n8vMsAL9k4zSFwyHa/openapi.json
