# SEO Audit Tool & Broken Link Checker: Website Crawler (`atesen-software/website-seo-audit`) Actor

SEO audit tool and broken link checker: crawl a website and get a 0-100 SEO score per page with issues and fix hints: titles, meta, Open Graph, schema, headings, broken links and images, Core Web Vitals hints, robots/sitemap. No browser. CSV export. $1.50/1k pages.

- **URL**: https://apify.com/atesen-software/website-seo-audit.md
- **Developed by:** [Atesen Software](https://apify.com/atesen-software) (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

$1.50 / 1,000 results

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 and Public SaaS Pricing & Link Auditor

Use `auditMode: "pricing"` for source-backed price candidates, metadata and CTA links from public SaaS pages. The Console example audits two publisher-owned pages with one safe link probe per page. Omit `auditMode`, or select `seo`, to keep the existing SEO crawler.

```json
{
  "auditMode": "pricing",
  "startUrls": [
    "https://atesensoftware.com/apps/web/attachment-bundler/",
    "https://attachment-bundler.atesensoftware.com/"
  ],
  "maxPricingPages": 2,
  "maxPricingLinks": 1,
  "pricingRunSeconds": 30,
  "pricingRequestSeconds": 5
}
```

Pricing mode emits one `pricing_page` row per attempted page, up to the page cap: HTTP status, redacted final URL, title, canonical, detected noindex directives, `priceCandidates` with literal currency evidence and verbatim whitespace-normalized surrounding text, allowlisted JSON-LD `Offer`/`PriceSpecification` fields, explicit CTA labels/hrefs, and bounded link-probe results. `$` alone remains an unspecified dollar currency. Billing intervals remain `null`; the context retains actual "per month" or "30 days" wording. No billing period, checkout amount or price correctness is inferred.

Empty pages and JavaScript app shells produce `unknown_no_price_in_static_html`. A 403, 401, 429 or detected challenge is `unknown_blocked`, never a confirmed broken link. Only observed 404/410 link responses are `broken`. Unprobed links, robots refusals, deadlines and network errors remain explicit unknowns. No login, purchase, OAuth, form submission or browser action is performed.

| Pricing input | Default | Bound |
| --- | --- | --- |
| `maxPricingPages` | 2 | 1–20 explicit pages; no automatic page discovery |
| `maxPricingLinks` | 1 | 0–20 safe CTA HEAD probes per page |
| `pricingRunSeconds` | 30 | 1–60 seconds shared wall-time budget |
| `pricingRequestSeconds` | 5 | 1–10 seconds per DNS/request, further limited by remaining run time |

Pricing mode fetches public HTTP(S) HTML on standard ports. Credentials, all query-bearing URLs and common auth/payment/action routes are skipped. Private, loopback, link-local, multicast, reserved and metadata IPv4 addresses are blocked. Each connection is pinned to a freshly validated public IPv4 address; redirects are revalidated and capped at three. IPv6-only hosts are unsupported. Robots rules apply at each destination, including link probes. Requests start at least one second apart per hostname; longer declared crawl delays are honored. Responses are capped at 1 MiB, without automatic retries, proxies or challenge workarounds. Compressed responses that ignore the identity-encoding request remain unknown. These safeguards describe pricing mode; legacy SEO mode retains its existing behavior.

Static extraction excludes scripts, templates, hidden attributes and inline-hidden elements. It does not compute CSS layout or execute JavaScript. Computed visibility, dynamic prices, country-specific prices, taxes and customer offers are outside its evidence. This is an experimental utility without a market-demand or revenue claim.

Pricing mode uses the existing `result` event for successfully audited HTML pages. Failed, blocked or non-HTML page rows have no custom result event. Pricing configuration is unchanged; the live Pricing tab is authoritative. `RUN_SUMMARY` records actual page/request counts and budget/time stops. The legacy CSV outputs below apply to SEO mode.

An explicit zero event-charge budget produces no billable page rows. Pricing mode also stops before fetching a page when that budget is zero. API and CLI callers should set `maxTotalChargeUsd` explicitly and check the actual run options; the event cap is distinct from any platform-usage charges shown for the run.

### Existing SEO mode

Crawl any website and get a **full technical SEO audit for every page** plus a **broken link checker** in the same run, at **$1.50 per 1,000 pages ($0.0015 per audited page; site summary, issue and status rows are free)**. You get a 0–100 SEO score with a letter grade, broken links and images (4xx/5xx and redirect chains), meta/Open Graph/Twitter checks, structured data (JSON-LD) validation, Core Web Vitals risk hints, robots.txt and sitemap checks, and **ready-made CSV reports**.

No browser, no proxy and no setup. Paste a list of domains and press **Start**.

### What does SEO Audit Tool do?

For each website you enter, the actor:

1. Fetches the start page and checks site-wide signals: robots.txt, XML sitemap (URL count), HTTP→HTTPS redirect, www/non-www consistency, HSTS and soft-404 handling.
2. Crawls internal pages (breadth-first, follows robots.txt) up to your page limit.
3. Audits every HTML page against **55+ checks** in 10 categories and scores it from 0 to 100 (A–F).
4. Checks every internal and external link and every image for 4xx/5xx errors and redirect chains.
5. Finds duplicate titles and meta descriptions across the site.
6. Writes one row per page, one summary row per site and three CSV files (pages, issues, broken links).

### Why use this SEO audit tool?

| | **This actor** | Typical browser-based SEO audit actor |
|---|---|---|
| Price per 1,000 pages | **$1.50** | $30–40 |
| Broken link + image checks | ✅ internal and external | often extra or missing |
| Structured data validation | ✅ JSON-LD, microdata, RDFa, required properties | basic |
| Core Web Vitals risk hints | ✅ LCP / CLS / INP risk per page | — |
| Score per category + grade | ✅ 10 categories | single score |
| CSV reports | ✅ PAGES.csv, ISSUES.csv, BROKEN_LINKS.csv | JSON only |
| Memory | 512 MB | 2–4 GB |
| Failed or blocked sites | free status row, not charged | often charged or the run fails |

### What does it check?

- **Meta**: title (missing, length, pixel width, duplicates), meta description (missing, length, duplicates), canonical (missing, points elsewhere, relative, multiple), meta robots and X-Robots-Tag (noindex/nofollow), charset, lang, favicon, hreflang.
- **Headings & content**: missing or multiple H1, skipped heading levels, word count, thin content, text-to-HTML ratio.
- **Technical**: HTTP status, redirect chains, HTTPS, mixed content, URL length/uppercase/underscores/parameters, indexability.
- **Mobile**: viewport meta tag.
- **Performance hints**: TTFB, HTML size, DOM size, render-blocking scripts and stylesheets, number of third-party domains, compression, `document.write`, missing preconnect.
- **Images**: missing alt text, missing width/height (CLS risk), lazy-loaded hero image (LCP risk), legacy formats, broken images.
- **Links**: broken internal/external links, redirected links, empty anchor text, nofollow internal links, few internal links.
- **Structured data**: JSON-LD parse errors, schema types, missing required properties (Product, Offer, Article, Organization, LocalBusiness, Event, Recipe, FAQPage, BreadcrumbList, JobPosting and more).
- **Social**: Open Graph (title, description, image) and Twitter Card tags.

Each issue has a severity (critical / warning / notice), a category, a message and a **fix hint**.

#### Core Web Vitals hints

The actor does not run a browser, so it cannot measure real LCP, CLS or INP. Instead it estimates the **risk** (low / medium / high) of each metric from the HTML: slow server response, render-blocking resources, lazy-loaded hero images, images without dimensions, heavy DOM, many third-party scripts and so on. Use the hints to find pages to fix. For measured values use PageSpeed Insights or the Chrome UX Report.

### How much does it cost?

Pay per event. You pay only for **audited HTML pages**: **$0.0015 per page ($1.50 per 1,000 pages)**, the same on every Apify plan, with no start fee.

These rows are **free**: site summary rows, issue rows, status rows for invalid, unreachable, blocked or non-HTML URLs, and pages that failed to load. Link and image checks are included in the page price.

Example: auditing 20 websites with 50 pages each (1,000 pages) costs about **$1.50**.

### Input

| Field | Default | Description |
|---|---|---|
| `startUrls` | – | Websites or pages to audit. Domains without `https://` are fine. |
| `maxPagesPerSite` | 25 | Maximum pages audited (and charged) per website. |
| `crawl` | true | Follow internal links. Off = audit only the listed URLs. |
| `maxDepth` | 10 | Maximum link depth from the start page. |
| `checkLinks` / `checkExternalLinks` | true | Check links for 4xx/5xx errors. |
| `maxLinkChecksPerSite` | 500 | Unique links checked per site. |
| `checkImages` / `maxImageChecksPerSite` | true / 200 | Check images for errors. |
| `respectRobotsTxt` | true | Skip URLs disallowed by robots.txt. |
| `includeSubdomains` | false | Treat subdomains as part of the site. |
| `includeUrlPatterns` / `excludeUrlPatterns` | – | Only crawl, or skip, URLs that contain these strings. |
| `ignoreQueryParameters` | false | Treat `?a=1` variants as the same page. |
| `outputIssueRows` | false | Also write one dataset row per issue. |
| `maxConcurrency` | 4 | Parallel requests per run. |

Example input:

```json
{
  "startUrls": ["https://example.com", "books.toscrape.com"],
  "maxPagesPerSite": 50,
  "checkExternalLinks": true
}
```

### Output

The dataset contains rows of four types (use the views in the **Output** tab):

- `page`: one row per audited page, with URL, status, title, description, canonical, H1, word count, links, images, schema types, `score`, `grade`, per-category scores (`scoreMeta`, `scoreTechnical`, …), `cwvLcpRisk` / `cwvClsRisk` / `cwvInpRisk`, `issueIds`, `issuesText`, `brokenLinks` and the full `issues` array.
- `site`: one summary per website, with `siteScore`, grade, average category scores, top issues, broken link count, duplicate titles and descriptions, robots.txt, sitemap, HTTPS, www and soft-404 results.
- `issue`: one row per issue (only if `outputIssueRows` is on).
- `status`: free rows for invalid input, unreachable, blocked, HTTP error or non-HTML start URLs.

Example page row (shortened):

```json
{
  "type": "page",
  "url": "https://books.toscrape.com/",
  "statusCode": 200,
  "score": 71,
  "grade": "C",
  "title": "All products | Books to Scrape - Sandbox",
  "metaDescription": null,
  "h1": "All products",
  "wordCount": 412,
  "cwvLcpRisk": "medium",
  "issueIds": "DESC_MISSING; CANONICAL_MISSING; IMG_NO_DIMENSIONS; OG_MISSING",
  "brokenLinkCount": 0
}
```

#### CSV reports

The key-value store of every run contains spreadsheet-ready files (UTF-8, open directly in Excel or Google Sheets):

- `PAGES.csv`: one line per page with all scores and the main fields.
- `ISSUES.csv`: one line per issue with page, severity, category, message and fix hint.
- `BROKEN_LINKS.csv`: source page, broken URL, status code and link type.
- `RUN_SUMMARY`: JSON totals for the run.

### Tips

- **Monitor your site weekly**: create a task with your domains and add a schedule (for example `0 6 * * 1`). Compare `siteScore` and `brokenLinkCount` between runs.
- Start with `maxPagesPerSite: 25` to check a site cheaply, then raise the limit for a full crawl.
- Turn off `checkExternalLinks` for very large sites to finish faster.
- Some sites block automated requests. The actor then writes a free `blocked` status row and charges nothing.

### FAQ

**Does it use a browser?** No. It downloads raw HTML with a browser-like HTTP client, so it is fast and cheap. Content rendered only by JavaScript is not seen, and neither is it by many crawlers.

**Is it legal?** The actor reads only public pages and follows robots.txt by default. Audit sites you own or have permission to analyse.

**Something is missing or wrong?** Open an issue on the **Issues** tab and we will look at it.

### Pricing

Pay per event: **$0.0015 per result** ($1.50 per 1,000 audited pages). Each audited page row is one `result` event. Site summary rows, issue rows, status rows, error rows and empty lookups are not charged. The run stops when it reaches the maximum cost per run. No Apify proxy and no login.

# Actor input Schema

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

Domains or URLs, one per line, e.g. "example.com" or "https://www.example.com/blog". Each site is crawled from its start URL. Start URLs on the same site share one crawl.

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

How many HTML pages to audit per site (start URL included). This is your cost control: you pay per audited page. 10 for a quick check, 50-500 for a full audit.

## `crawl` (type: `boolean`):

Follow internal links from the start URL. Off = audit only the start URLs (good for a list of landing pages).

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

How many clicks away from the start URL to go.

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

Verify the HTTP status of every link found (HEAD, then GET when HEAD is refused). Links blocked by bot protection (401/403/429) are reported as unverified, never as broken.

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

Check links to other websites too (outbound broken links).

## `maxLinkChecksPerSite` (type: `integer`):

Upper limit of unique links checked per site (internal ones first). Pages audited during the crawl are known already and are not counted.

## `checkImages` (type: `boolean`):

Check image status and file size (broken images, images over 200 KB).

## `maxImageChecksPerSite` (type: `integer`):

Upper limit of unique images checked per site.

## `respectRobotsTxt` (type: `boolean`):

Skip pages that robots.txt disallows.

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

Also crawl subdomains (blog.example.com when starting from example.com).

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

Skip URLs containing any of these texts (e.g. "/tag/", "?sort="). Wrap in slashes for a regular expression: //page/\d+/

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

Optional. Only follow links containing one of these texts (e.g. "/blog/"). Start URLs are always audited.

## `ignoreQueryParameters` (type: `boolean`):

Treat example.com/a?x=1 and example.com/a as the same page. Useful for shops with filter and sort parameters.

## `outputIssueRows` (type: `boolean`):

Also write every issue as its own free row (type "issue"), handy for spreadsheets and pivot tables. The full list is always in ISSUES.csv in the key-value store.

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

Pages loaded at the same time per site. Lower it for small or slow sites.

## `auditMode` (type: `string`):

seo preserves the existing crawler. pricing audits explicit public HTML pages, price evidence and bounded safe CTA links; no browser or checkout actions.

## `maxPricingPages` (type: `integer`):

Audit explicit start URLs only; no page discovery. Combined per-run page cap.

## `maxPricingLinks` (type: `integer`):

Maximum safe public CTA links probed with HEAD per page. Auth, payment, query-bearing and action links are skipped.

## `pricingRunSeconds` (type: `integer`):

Shared wall-time deadline for pages, DNS, robots and link probes. Partial/unknown evidence is explicit.

## `pricingRequestSeconds` (type: `integer`):

DNS and individual network request timeout, bounded further by the run deadline. No automatic retries.

## Actor input object example

```json
{
  "startUrls": [
    "https://atesensoftware.com/apps/web/attachment-bundler/",
    "https://attachment-bundler.atesensoftware.com/"
  ],
  "maxPagesPerSite": 25,
  "crawl": true,
  "maxDepth": 10,
  "checkLinks": true,
  "checkExternalLinks": true,
  "maxLinkChecksPerSite": 500,
  "checkImages": true,
  "maxImageChecksPerSite": 200,
  "respectRobotsTxt": true,
  "includeSubdomains": false,
  "excludeUrlPatterns": [],
  "includeUrlPatterns": [],
  "ignoreQueryParameters": false,
  "outputIssueRows": false,
  "maxConcurrency": 4,
  "auditMode": "pricing",
  "maxPricingPages": 2,
  "maxPricingLinks": 1,
  "pricingRunSeconds": 30,
  "pricingRequestSeconds": 5
}
```

# Actor output Schema

## `pages` (type: `string`):

No description

## `sites` (type: `string`):

No description

## `brokenLinks` (type: `string`):

No description

## `pagesCsv` (type: `string`):

No description

## `issuesCsv` (type: `string`):

No description

## `brokenCsv` (type: `string`):

No description

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

No description

## `pricingEvidence` (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://atesensoftware.com/apps/web/attachment-bundler/",
        "https://attachment-bundler.atesensoftware.com/"
    ],
    "auditMode": "pricing",
    "maxPricingPages": 2,
    "maxPricingLinks": 1,
    "pricingRunSeconds": 30,
    "pricingRequestSeconds": 5
};

// Run the Actor and wait for it to finish
const run = await client.actor("atesen-software/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://atesensoftware.com/apps/web/attachment-bundler/",
        "https://attachment-bundler.atesensoftware.com/",
    ],
    "auditMode": "pricing",
    "maxPricingPages": 2,
    "maxPricingLinks": 1,
    "pricingRunSeconds": 30,
    "pricingRequestSeconds": 5,
}

# Run the Actor and wait for it to finish
run = client.actor("atesen-software/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://atesensoftware.com/apps/web/attachment-bundler/",
    "https://attachment-bundler.atesensoftware.com/"
  ],
  "auditMode": "pricing",
  "maxPricingPages": 2,
  "maxPricingLinks": 1,
  "pricingRunSeconds": 30,
  "pricingRequestSeconds": 5
}' |
apify call atesen-software/website-seo-audit --silent --output-dataset

```

## MCP server setup

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