# SEO Audit & Broken Link Checker: Technical SEO (`maliksaid/technical-seo-audit`) Actor

SEO audit tool and site audit crawler with a 0 to 100 SEO score per page and per site. Works as a broken link checker, 404 checker and redirect checker. Checks titles, meta descriptions, H1s, canonicals, noindex, hreflang, sitemap and robots.txt. One row per page with issue codes.

- **URL**: https://apify.com/maliksaid/technical-seo-audit.md
- **Developed by:** [Malik Said](https://apify.com/maliksaid) (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 $4.80 / 1,000 page audits

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 & Broken Link Checker: Technical SEO

This technical SEO audit tool crawls your website and checks every page for SEO problems. It works as a site audit crawler, a broken link checker and a redirect checker in one run. You get one row per page with its status code, redirects, title, meta description, headings, canonical tag, robots directives, hreflang, broken links and images without alt text. Each row lists clear issue codes and an SEO score from 0 to 100. It also checks robots.txt and your XML sitemap. A site summary gives the site SEO score, counts every issue and lists duplicate titles, broken links and sitemap gaps.

It is built for site owners, SEO consultants and agencies who audit their own or their clients' sites.

### SEO audit checks

| Area | What you get |
|---|---|
| Status and speed | HTTP status, response time, content type |
| Redirects | Every hop with its status, the final URL, redirect chains and loops |
| Titles and descriptions | Text and length, missing, too long, too short, duplicates |
| Headings | H1 count and text |
| Indexing | Canonical URL and whether it points to the page itself, meta robots, X-Robots-Tag, noindex, nofollow |
| International | hreflang tags |
| Content | Word count of visible text |
| Links | Internal and external link counts. Every link is checked once per run. Links with status 400 or higher, or that fail to load, are reported as broken, with their link text |
| Images | Image count and images with no alt attribute |
| Structured data | Schema.org types from JSON-LD and microdata |
| Mobile and sharing | Viewport tag, page language, Open Graph title, description and image, Twitter card type |
| robots.txt | Whether it exists and which sitemaps it declares |
| XML sitemap | Pages missing from the sitemap, sitemap URLs that redirect, fail or are noindex, and sitemap URLs no page links to |
| SEO score | A score from 0 to 100 for each page and an average for the site |

### SEO audit issue codes

| Code | Meaning |
|---|---|
| `MISSING_TITLE` | No title tag, or an empty one |
| `TITLE_TOO_LONG` | Title longer than 60 characters |
| `TITLE_TOO_SHORT` | Title shorter than 30 characters |
| `DUPLICATE_TITLE` | An earlier page in the crawl has the same title |
| `MISSING_META_DESCRIPTION` | No meta description, or an empty one |
| `META_DESCRIPTION_TOO_LONG` | Meta description longer than 160 characters |
| `META_DESCRIPTION_TOO_SHORT` | Meta description shorter than 70 characters |
| `DUPLICATE_META_DESCRIPTION` | An earlier page in the crawl has the same meta description |
| `MISSING_H1` | No H1 heading |
| `MULTIPLE_H1` | More than one H1 heading |
| `MISSING_CANONICAL` | No canonical link tag |
| `CANONICAL_TO_OTHER` | The canonical tag points to a different URL |
| `NOINDEX` | Meta robots or X-Robots-Tag contains noindex or none |
| `NOFOLLOW` | Meta robots or X-Robots-Tag contains nofollow or none |
| `LOW_WORD_COUNT` | Fewer than 200 words of visible text |
| `BROKEN_LINK` | At least one link on the page is broken |
| `IMAGES_WITHOUT_ALT` | At least one image has no alt attribute |
| `MISSING_VIEWPORT` | No meta viewport tag, so mobile browsers show a zoomed-out desktop page. No score effect |
| `MISSING_HTML_LANG` | The html tag has no lang attribute. No score effect |
| `MISSING_OG_TAGS` | No og:title and no og:image, so shared links show no preview. No score effect |
| `REDIRECT` | The URL redirects once |
| `REDIRECT_CHAIN` | The URL redirects more than once before the final page |
| `REDIRECT_LOOP` | The redirects lead back to a URL already in the chain |
| `CLIENT_ERROR_4XX` | The page, or its redirect target, returns a 4xx status |
| `SERVER_ERROR_5XX` | The page, or its redirect target, returns a 5xx status |
| `SLOW_RESPONSE` | Response headers took longer than 2 seconds |
| `FETCH_FAILED` | The page could not be loaded at all |
| `NOT_IN_SITEMAP` | An indexable page that the XML sitemap does not list |
| `SITEMAP_URL_NOT_INDEXABLE` | The sitemap lists this URL, but it redirects, fails, is noindex or has a canonical to another URL |

The two sitemap codes appear only when the site has a readable XML sitemap.

On-page checks such as title and H1 run only on HTML pages that load with a 2xx status.

**Duplicates.** Each page is saved and charged as soon as it is audited. So a duplicate is flagged on the second and later pages that share a title or description. The `OUTPUT` summary lists every duplicate group with all of its pages, including the first one.

### SEO score: how it is calculated

Each page starts at 100. Each issue code takes off points:

| Points | Issue codes |
|---|---|
| 30 | `NOINDEX` |
| 15 | `MISSING_TITLE` |
| 10 | `BROKEN_LINK`, `SLOW_RESPONSE` |
| 8 | `MISSING_H1`, `MISSING_META_DESCRIPTION`, `DUPLICATE_TITLE` |
| 6 | `LOW_WORD_COUNT` |
| 5 | `CANONICAL_TO_OTHER`, `MISSING_CANONICAL`, `DUPLICATE_META_DESCRIPTION`, `IMAGES_WITHOUT_ALT`, `NOFOLLOW`, `SITEMAP_URL_NOT_INDEXABLE` |
| 3 | `TITLE_TOO_LONG`, `TITLE_TOO_SHORT`, `META_DESCRIPTION_TOO_LONG`, `META_DESCRIPTION_TOO_SHORT`, `MULTIPLE_H1`, `NOT_IN_SITEMAP` |

The score never goes below 0. Pages with a 4xx or 5xx status, a redirect loop or a failed load score 0. Redirect rows get no score, because the target page is scored as its own row. The site SEO score is the average of all scored pages. The score is a simple guide to where to start. Search engines use their own signals.

### robots.txt and XML sitemap check

Before the crawl, the Actor reads robots.txt for each start URL. It then reads every XML sitemap that robots.txt declares, including sitemap index files. If robots.txt declares none, it tries `/sitemap.xml`. It reads up to 50 sitemap files and 50,000 URLs, for at most 60 seconds. If one of these limits stops the read, `sitemapReadComplete` is false and `NOT_IN_SITEMAP` is not reported, because the page may be listed in a sitemap the run did not read.

The audit summary then shows:

- `pagesNotInSitemap`: indexable pages the crawl found that the sitemap does not list.
- `sitemapUrlsNotIndexable`: sitemap URLs that redirect, return an error, are noindex or have a canonical to another URL.
- `sitemapUrlsNotLinked`: sitemap URLs that no crawled page links to. These are often orphan pages. This list needs a full crawl, so it is filled only when the crawl ends before **Maximum pages**. Set **Maximum pages** to 0 to get it.
- `siteIssues`: `ROBOTS_TXT_MISSING`, `SITEMAP_MISSING`, `SITEMAP_NOT_IN_ROBOTS_TXT` and `SITEMAP_URLS_NOT_LINKED`.

Example from a test on a small site with five sitemap URLs: one linked page was missing from the sitemap, one sitemap URL was noindex, one redirected, and one page was in the sitemap but no page linked to it. All four showed up in the summary.

### SEO audit data you get

One dataset row per page. The example section below shows a full record.

A redirecting URL gets its own row. Example from a test on crawler-test.com: `/redirects/redirect_3_302` returned 302, then 301, then 301, then 200 at `/redirects/redirect_target`. Its row has `redirectCount: 3`, all four hops in `redirectChain`, and the issue `REDIRECT_CHAIN`. The target page is then audited as its own row.

Fields that can be empty:

- `title`, `metaDescription`, `canonicalUrl`, `metaRobots` and `xRobotsTag` are empty when the page does not have them.
- `htmlLang`, `ogTitle`, `ogDescription`, `ogImage` and `twitterCard` are empty when the page does not have them. `hasViewport` is false then.
- Each entry in `brokenLinks` has `url`, `status`, `error`, `anchorText` (the link text, or an image's alt text) and `linkType`. So you can find the link on the page.
- `canonicalIsSelf` is empty when there is no canonical tag.
- `inSitemap` is empty when the site has no readable XML sitemap, or the sitemap check is off.
- `seoScore` is empty for redirect rows.
- `finalStatus` is empty for a redirect loop, or when robots.txt blocks the redirect target.
- `httpStatus` is empty when the page failed to load. `error` then says why.
- On-page fields stay empty or zero for redirects and error pages.

#### Audit summary

The key-value store record `OUTPUT` holds the site summary:

- `pagesAudited` and `pagesWithIssues`
- `siteScore`: the site SEO score from 0 to 100
- `scoreDistribution`: pages scoring 90 to 100, 50 to 89 and 0 to 49
- `lowestScoringPages`: the 10 pages with the lowest score
- `siteIssues`, `robotsTxt` and `sitemap`: the robots.txt and XML sitemap results described above
- `issueCounts`: pages per issue code, highest first
- `statusCounts`: pages per HTTP status
- `duplicateTitles` and `duplicateMetaDescriptions`: each group with its pages
- `brokenLinks`: each broken URL with its status, its link text and up to 10 pages that link to it
- `thresholds`: the limits used for the length and word-count checks
- `scoreWeights`: the points each issue code takes off the score

### Site audit: how to use it

1. Enter one or more start URLs of a site you own or may audit.
2. Set **Maximum pages**. Use 0 to audit the whole site.
3. Run the Actor. Open the **Audited pages** table, then the **Audit summary**.
4. Export the rows to CSV or Excel, or connect them to Google Sheets through Apify integrations.

### Site audit input options

| Option | Default | Meaning |
|---|---|---|
| Start URLs | `https://books.toscrape.com/` | Where the crawl starts |
| Maximum pages | 100 | Stop after this many pages. Set 0 for no limit |
| Include subdomains | Off | Also crawl subdomains such as blog.example.com. www. always counts as the same site |
| Follow links to other pages | On | Turn off to audit only the start URLs, for example a list of landing pages |
| Skip URLs matching | None | URL patterns that are never crawled or charged, for example `**/tag/**` |
| Check links for errors | On | Check each link found on each page |
| Check links to other sites | On | Also check links to other websites |
| Check robots.txt and XML sitemap | On | Read robots.txt and the XML sitemaps, and compare them with the crawl |
| Parallel requests | 5 | Pages loaded at the same time, from 1 to 20 |
| Proxy | Off | Most sites need none |

### Site audit crawler: how it crawls

- It reads robots.txt on every host and skips URLs that robots.txt disallows. Skipped pages are not saved and cost nothing.
- It follows links on the same site only. Links to images, PDFs and other files are checked but not crawled.
- It reads the XML sitemaps while the crawl starts, three files at a time, for at most 30 seconds. A slow sitemap no longer delays the first pages.
- It loads HTML with plain HTTP requests. It does not run JavaScript. Content that only appears after JavaScript runs is not seen.
- Some sites block automated checks, for example with status 403 or 429. Such links show up as broken with that status.

### SEO audit pricing

The Actor uses pay-per-event pricing.

| Event | Free plan | GOLD plan and higher |
|---|---|---|
| Run start | $0.01 | $0.01 |
| Page audit | $0.006 ($6.00 per 1,000) | $0.0048 ($4.80 per 1,000) |

| Pages in one run | Free plan | GOLD plan and higher |
|---|---|---|
| 100 | $0.61 | $0.49 |
| 1,000 | $6.01 | $4.81 |

Costs include the run start fee.

Pages skipped by robots.txt cost nothing. Link checks are included in the page price.

### SEO audit example: input and output

This input audits up to 100 pages of a public test shop and checks every link:

```json
{
  "startUrls": [{ "url": "https://books.toscrape.com/" }],
  "maxItems": 100,
  "checkLinks": true,
  "checkExternalLinks": true,
  "checkSitemap": true
}
```

The home page record from that run on 21 October 2026:

```json
{
  "id": "https://books.toscrape.com/",
  "url": "https://books.toscrape.com/",
  "httpStatus": 200,
  "finalUrl": "https://books.toscrape.com/",
  "finalStatus": 200,
  "redirectChain": [],
  "redirectCount": 0,
  "responseTimeMs": 174,
  "contentType": "text/html",
  "depth": 0,
  "title": "All products | Books to Scrape - Sandbox",
  "titleLength": 40,
  "metaDescription": null,
  "metaDescriptionLength": 0,
  "h1Count": 1,
  "h1": [
    "All products"
  ],
  "canonicalUrl": null,
  "canonicalIsSelf": null,
  "metaRobots": "NOARCHIVE,NOCACHE",
  "xRobotsTag": null,
  "noindex": false,
  "nofollow": false,
  "hreflang": [],
  "wordCount": 297,
  "internalLinkCount": 73,
  "externalLinkCount": 0,
  "brokenLinkCount": 0,
  "brokenLinks": [],
  "imageCount": 20,
  "imagesWithoutAlt": 0,
  "structuredDataTypes": [],
  "htmlLang": "en-us",
  "hasViewport": true,
  "ogTitle": null,
  "ogDescription": null,
  "ogImage": null,
  "twitterCard": null,
  "inSitemap": null,
  "seoScore": 87,
  "issues": [
    "MISSING_META_DESCRIPTION",
    "MISSING_CANONICAL",
    "MISSING_OG_TAGS"
  ],
  "issueCount": 3,
  "error": null,
  "scrapedAt": "2026-10-23T22:51:10.000Z"
}
```

The same run gave a site SEO score of 87 over 100 pages. This test site has no robots.txt and no sitemap, so `inSitemap` is empty. It has no Open Graph tags, so `MISSING_OG_TAGS` appears (this issue costs no points). The summary lists `ROBOTS_TXT_MISSING` and `SITEMAP_MISSING`.

### SEO audit FAQ

#### Does it give an SEO score?

Yes. Each page gets `seoScore` from 0 to 100, and the summary gives `siteScore` for the whole site. The table above shows how many points each issue costs.

#### Does it check my XML sitemap?

Yes. It finds the sitemap through robots.txt or `/sitemap.xml`. It flags pages missing from the sitemap, sitemap URLs that are not indexable, and sitemap URLs that no page links to.

#### How do I find broken links on my site?

Leave **Check links for errors** on. Each row lists the broken links on that page in `brokenLinks`. The `OUTPUT` summary lists each broken URL with the pages that link to it.

#### Does it check redirect chains?

Yes. Each redirecting URL gets its own row with every hop, its status codes and the final URL. Chains and loops get the issue codes `REDIRECT_CHAIN` and `REDIRECT_LOOP`.

#### Can it audit a whole website?

Yes. Set **Maximum pages** to 0. The crawl then follows every internal link that robots.txt allows.

#### Does it render JavaScript?

No. It reads the HTML the server sends. Pages that build their content with JavaScript may show a low word count or missing tags.

#### Can I export the site audit to Excel or Google Sheets?

Yes. Export the dataset as CSV, Excel or JSON, or send it to Google Sheets through Apify integrations.

### SEO audit data use

- Audit only sites you own or have permission to audit.
- The Actor reads page structure: titles, tags, links and status codes. It does not collect contact details or other personal data.

### Related Actors

- [US Building Permits](https://apify.com/maliksaid/us-building-permits): new building permits from ten US cities.
- [Drug Approvals Monitor](https://apify.com/maliksaid/drug-approvals-monitor): new drug approvals from the FDA, EMA and Health Canada.
- [Regulator Circulars Monitor](https://apify.com/maliksaid/regulator-circulars-monitor): new circulars from the Central Bank of Jordan and Saudi NCA.

### Support

Report a problem in the Issues tab. Include the run link.

# Actor input Schema

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

Pages where the audit starts. The Actor follows links to other pages on the same site. Audit only sites you own or have permission to audit.

## `maxItems` (type: `integer`):

Stop after saving this many pages. The default keeps test runs short and cheap. Set 0 for no limit.

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

Also crawl subdomains of the start URLs, for example blog.example.com. The www. prefix always counts as the same site.

## `crawlLinks` (type: `boolean`):

On: audit the start URLs and the pages they link to on the same site. Off: audit only the start URLs, for example a list of landing pages. Links on those pages are still checked for errors.

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

Pages whose URL matches one of these patterns are not crawled, saved or charged. \*\* matches any text, \* matches text within one path segment. Example: **/tag/** or \**?filter=*

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

Check every link found on each page once per run. Links that return status 400 or higher, or fail to load, are reported as broken.

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

Also check links that point to other websites. Turn off to check only links inside your site.

## `checkSitemap` (type: `boolean`):

Read robots.txt and the XML sitemaps it lists (or /sitemap.xml). Flags pages missing from the sitemap, sitemap URLs that redirect, fail or are noindex, and sitemap URLs no crawled page links to.

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

How many pages the Actor loads at the same time. Keep it low for small servers.

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

Optional. Most sites need no proxy. Use one only if your site blocks data-centre traffic.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://books.toscrape.com/"
    }
  ],
  "maxItems": 100,
  "includeSubdomains": false,
  "crawlLinks": true,
  "excludeUrlPatterns": [],
  "checkLinks": true,
  "checkExternalLinks": true,
  "checkSitemap": true,
  "maxConcurrency": 5,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `pages` (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": [
        {
            "url": "https://books.toscrape.com/"
        }
    ],
    "maxItems": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("maliksaid/technical-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": [{ "url": "https://books.toscrape.com/" }],
    "maxItems": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("maliksaid/technical-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": [
    {
      "url": "https://books.toscrape.com/"
    }
  ],
  "maxItems": 100
}' |
apify call maliksaid/technical-seo-audit --silent --output-dataset

```

## MCP server setup

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