SEO Audit Crawler
Pricing
from $10.00 / 1,000 page auditeds
SEO Audit Crawler
Audit your own website's on-page SEO: crawl it from a start URL or sitemap and get a 0-100 score per page and site from a published formula, plus title and meta description length, H1s, canonical, robots, hreflang, Open Graph, JSON-LD, broken links and images, redirects and issues.
Pricing
from $10.00 / 1,000 page auditeds
Rating
0.0
(0)
Developer
Michael Costa
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
10 hours ago
Last modified
Categories
Share
What does SEO Audit Crawler do?
SEO Audit Crawler is a website SEO audit tool. Give it your site; it crawls it like a search engine and reports every page's on-page SEO, broken links and images, issues by severity and a 0-100 score from a published formula, plus a site summary with the site's score, issue totals and duplicate titles.
It is not a rank tracker: it reads your own pages as your server sends them, with no third-party SEO data (no rankings, backlinks or traffic estimates), no login and no proxy.
Try it in one click: the input comes pre-filled with crawlee.dev and a 10-page limit. That's 10 pages, about $0.10 (10 × $0.01, plus $0.00005 for the run start). Then replace it with your own site and raise Max pages per run.
Monitor your site: what changed since the last audit, in Slack, email or a webhook
With Compare with the previous audit on, each run says what changed since the last run of the same audit: every
page is new, changed or unchanged, with the fields and issues that changed and its score before and after, and
the site summary counts issues added and fixed by type, new and fixed broken links, pages added and removed, and the
site score's change. Turn on Only return changed
pages as well and each run returns only the new and changed pages, charged as pages ($0.01 each). The unchanged
ones are still crawled and rechecked, at a tenth of the price ($0.001 each): a 100-page site where nothing changed
costs $0.10 a week.
- Put your site in Start URLs, set Max pages per run above the number of pages on the site, turn on Only
return changed pages (
"onlyChangedPages": true), and click Start. This first run returns every page and is the baseline the next runs compare with. - Click Save as a new task (top right of the actor page). The comparison belongs to that exact audit (the same start URLs, mode and settings), so the task keeps comparing with its own last run. Changing the start URLs, mode, max pages, crawl depth or link-check settings starts a new baseline.
- In Apify Console, open Schedules, click Create new, set how often in Schedule setup (for example weekly, Monday at 06:00), then Add your task.
- On the task, open the Integrations tab and pick where the changes go:
- Slack: click Configure, sign in, pick the workspace and channel, and the "run succeeded" event. A
useful message:
{{resource.statusMessage}}and a link to the results,<https://console.apify.com/storage/datasets/{{resource.defaultDatasetId}}|changed pages>. - Gmail: click Connect with Google, set the subject and body, and attach the dataset (for example as CSV). It sends after each successful run.
- HTTP webhook: event
ACTOR.RUN.SUCCEEDED, your URL. Apify POSTs{"eventType": ..., "resource": {...}};resource.defaultDatasetIdis the run's dataset (the changed pages), andGET https://api.apify.com/v2/key-value-stores/<defaultKeyValueStoreId>/records/SUMMARY(with your API token) returns the site summary with itscomparison.
- Slack: click Configure, sign in, pick the workspace and channel, and the "run succeeded" event. A
useful message:
Apify's integrations fire after every successful run, including quiet ones: a quiet run's dataset is empty, and its status message says how many pages were unchanged. The pre-filled example, run twice with Only return changed pages on (local runs, 2026-09-26): the first run returned all 10 pages ($0.10); the second returned 0 and said "10 pages audited, site score 89/100: 0 errors, 10 warnings, 13 notices; 0 broken links; since the audit of 2026-09-26 00:06 UTC: 0 new, 0 changed, removed pages not reported (the crawl was incomplete); 0 issues added, 0 fixed; 0 new broken links, 0 fixed; site score 89 -> 89; 10 unchanged pages rechecked, not returned; ..." (shortened). "Incomplete" because crawlee.dev has more than the example's 10 pages: pages are only reported as removed when the crawl was complete. See Changes since the last audit for what's compared and when.
What data does SEO Audit Crawler return?
| Field | Example | Notes |
|---|---|---|
url, statusCode, indexable | https://crawlee.dev/blog, 200, true | Indexable: a 200, no noindex, no canonical pointing elsewhere. |
score | 74 | 0-100: 100 minus points per issue found; see How the score is calculated. |
title, titleLength | Crawlee Blog - learn how to build better scrapers ..., 107 | |
metaDescription, metaDescriptionLength | Who we are and what we make., 28 | null when missing. |
h1Count, h1 | 0, [] | |
canonicalUrl | https://example.com/about | null when missing. |
robotsMeta, xRobotsTag | null | The robots meta tag and header. |
hreflang, openGraphTags, twitterTags | [{"hreflang": "de", ...}], ["og:title", ...] | |
structuredDataTypes | ["Organization", "BreadcrumbList"] | JSON-LD @type values. |
imageAltCoverage, imagesEmptyAlt, imagesMissingSize | 0.75, 1, 20 | Alt text and width/height attributes of <img>. |
brokenImages, oversizedImages | [], [{"url": ..., "sizeBytes": 205098}] | Each image checked once per run; over 200 KB by its Content-Length. |
wordCount | 1596 | Visible text in <body>. |
internalLinkCount, externalLinkCount | 42, 29 | |
brokenLinkCount, brokenLinks | 1, [{"url": ..., "statusCode": 404, "linkText": ...}] | Every link checked once per run. |
redirectChain, responseTimeMs | [...], 182.4 | Hop by hop. |
issues | [{"code": "title-too-long", "severity": "warning", "message": ...}] | Most severe first; see Issues. |
changeType, changedFields, changedIssues, previousScore | "changed", ["title", "score"], {"added": ["title-too-long"], ...}, 82 | With the comparison on; null otherwise and on the first (baseline) run. |
One row per page, plus a free SUMMARY record for the whole site. The full list is under Output.
How much does it cost to run an SEO audit?
You pay per page audited: $10.00 per 1,000 pages ($0.01 a page), plus $0.00005 each time a run starts. With Only return changed pages on, pages that come back unchanged are rechecked but not returned, at $1.00 per 1,000 unchanged pages ($0.001 a page).
- The example below: 10 pages × $0.01 = $0.10, plus the start fee.
- A month, for example: an agency auditing 5 client sites of 200 pages each, weekly: 5 × 200 × 4 = 4,000 pages × $0.01 = $40.00, plus 4 run starts ($0.0002) if the 5 sites are one run.
- Caps: Max pages per run in the input, and Maximum cost per run in the run options. The run stops cleanly at whichever comes first; no page is requested past the limit, so a capped run is also a fast one.
Free: link and image checks, the page and site scores, the site summary, robots.txt and sitemap files, and pages that were never requested (robots.txt disallows them, or the address is private or invalid; they're listed in the summary). A link that turns out to be a file (a PDF, an image) is checked as a link, not audited as a page, and isn't charged.
Charged: a page that answers with an error (404, 500, a timeout) is audited: its status is the finding.
Only return changed pages: unchanged pages are still crawled and audited (they count in the site summary; the crawl costs the same whether a page changed or not), but they aren't returned: each is charged $0.001, a tenth of a returned page. New and changed pages are returned and charged $0.01, as usual. The first run is the baseline and returns every page. For example, weekly monitoring of a 100-page site:
- Week 1 (baseline): 100 × $0.01 = $1.00.
- A quiet week (nothing changed): 100 × $0.001 = $0.10.
- A week where 5 pages changed: 5 × $0.01 + 95 × $0.001 = $0.145.
Plus the start fee each run. Pages that refused the crawler or failed again (unchanged, but no real answer) aren't charged. In this mode the Maximum cost per run limits the pages read: a page is only read when the remaining budget covers at least a rechecked page, and a changed page is only returned when it covers the full page price (one that isn't is reported again next run).
How to run an SEO audit of your website
- Open SEO Audit Crawler and click Try for free (or Start if you're signed in).
- Put your site in Start URLs (
https://example.com/). - Pick Find pages by:
crawl(follow links) orsitemap(the pages your sitemaps list). - Raise Max pages per run to cover the site, and set Crawl depth.
- Click Start, then open the Output tab: sort the pages by score, filter them by issue, or open the "Site summary" link for the site score. Export as JSON, CSV or Excel.
Example: a 10-page audit of crawlee.dev
The pre-filled input:
{"startUrls": ["https://crawlee.dev/"], "mode": "crawl", "maxPages": 10, "maxDepth": 3,"checkLinks": true, "checkExternalLinks": true, "maxLinkChecks": 500}
One of the 10 pages it returned (real output from a local run on 2026-09-26, other fields left out):
{"url": "https://crawlee.dev/blog","statusCode": 200,"indexable": true,"depth": 1,"foundOn": "https://crawlee.dev/","title": "Crawlee Blog - learn how to build better scrapers | Crawlee for JavaScript · Build reliable crawlers. Fast.","titleLength": 107,"h1Count": 0,"wordCount": 1596,"imageCount": 28,"imagesMissingAlt": 0,"imagesMissingSize": 20,"oversizedImages": [{"url": "https://crawlee.dev/assets/images/gradcracker-scraper-caefb62d1c150c4209a6e564c052fa41.webp", "sizeBytes": 205098}],"brokenImageCount": 0,"internalLinkCount": 42,"externalLinkCount": 29,"brokenLinkCount": 0,"score": 74,"issues": [{"code": "title-too-long", "severity": "warning", "message": "The title is 107 characters; search results usually cut it after about 60."},{"code": "missing-h1", "severity": "warning", "message": "The page has no <h1> heading."},{"code": "oversized-images", "severity": "warning", "message": "1 image is over 200 KB (see oversizedImages)."},{"code": "images-missing-size", "severity": "notice", "message": "20 of 28 images have no width and height attributes, so the page can shift while they load."}]}
Its score: 100 − 3 warnings × 8 − 1 notice × 2 = 74. The same run's SUMMARY record: 10 pages audited, site
score 89 (6 pages scored 90-100, 4 scored 50-89), 9 indexable, 10 warnings and 13 notices, title-too-long on 7
pages, one meta description shared by 6 pages, 63 distinct images checked with 3 over 200 KB (the largest a 2.0 MB
GIF), no broken links or images, average response 60.5 ms.
What that cost: 10 pages × $0.01 = $0.10, plus the $0.00005 start fee.
Input
| Field | What it does |
|---|---|
| Start URLs | Your site, one address per line: a page to crawl from (https://example.com/), or for the sitemap mode a site or a sitemap URL (example.com, https://example.com/sitemap.xml). |
| Find pages by | crawl (default): follow same-site links from the start URLs. sitemap: the pages your sitemaps list, then their links up to the crawl depth. |
| Max pages per run | Default 10, up to 10,000. |
| Crawl depth | Clicks away from the start (or sitemap) pages. Default 3; 0 audits only those pages. |
| Check links for broken ones | On by default. Off: only links to pages the crawl read get a status. |
| Check links to other sites | On by default. Other sites' pages are checked, never crawled. |
| Check images | On by default. Each distinct <img src> gets one HEAD request, wherever it's hosted: broken images, and files over 200 KB by their Content-Length. Off: no image requests (alt text and width/height are still read from the page). |
| Max link checks per run | Default 500, up to 50,000; links and images together. Links past it are counted in linksNotChecked, images in imagesNotChecked. |
| Compare with the previous audit | Off by default. On: each page says what changed since the last run of the same audit, and the summary has a comparison. See Changes since the last audit. |
| Only return changed pages | Off by default. On: turns the comparison on and returns only new and changed pages ($0.01 each); unchanged pages are rechecked at $0.001 each. |
"Same site" means the same host, with or without www.. If your start URL redirects to another host (say
example.com to www.example.co.uk), that host counts as your site too.
{"startUrls": ["https://example.com/"],"mode": "crawl","maxPages": 500,"maxDepth": 5}
Output
One row per page, in the order the crawl reached them. Fields that don't apply are null (or empty lists).
{"url": "https://example.com/about","finalUrl": "https://example.com/about","statusCode": 200,"indexable": true,"depth": 1,"foundOn": "https://example.com/","source": "https://example.com/","responseTimeMs": 182.4,"redirectCount": 0,"redirectChain": [{"url": "https://example.com/about", "statusCode": 200, "location": null, "timeMs": 182.4}],"contentType": "text/html","sizeBytes": 48211,"title": "About us | Example","titleLength": 18,"metaDescription": "Who we are and what we make.","metaDescriptionLength": 28,"h1Count": 1,"h1": ["About us"],"canonicalUrl": "https://example.com/about","robotsMeta": null,"xRobotsTag": null,"lang": "en","hreflang": [{"hreflang": "de", "url": "https://example.com/de/about"}],"openGraphTags": ["og:description", "og:image", "og:title"],"twitterTags": ["twitter:card"],"structuredDataTypes": ["Organization", "BreadcrumbList"],"imageCount": 4,"imagesMissingAlt": 1,"imageAltCoverage": 0.75,"imagesEmptyAlt": 1,"imagesMissingSize": 0,"imageLinksWithoutText": 0,"wordCount": 612,"internalLinkCount": 24,"externalLinkCount": 3,"brokenLinkCount": 1,"brokenLinks": [{"url": "https://example.com/old-post", "statusCode": 404, "outcome": "broken", "error": null,"linkText": "Our old post"}],"linksNotChecked": 0,"brokenImageCount": 0,"brokenImages": [],"oversizedImages": [{"url": "https://example.com/img/team.jpg", "sizeBytes": 412000}],"imagesNotChecked": 0,"score": 62,"issueCount": 4,"issueCounts": {"error": 1, "warning": 2, "notice": 1},"issues": [{"code": "broken-links", "severity": "error", "message": "1 link on the page is broken (see brokenLinks)."},{"code": "images-missing-alt", "severity": "warning", "message": "1 of 4 images have no alt attribute."},{"code": "oversized-images", "severity": "warning", "message": "1 image is over 200 KB (see oversizedImages)."},{"code": "title-too-short", "severity": "notice", "message": "The title is 18 characters; 30-60 is the usual target."}],"error": null,"changeType": "changed","changedFields": ["title", "brokenLinks", "score"],"changedIssues": {"added": ["broken-links", "title-too-short"], "fixed": [],"brokenLinksAdded": ["https://example.com/old-post"], "brokenLinksFixed": 0},"previousStatusCode": 200,"previousScore": 84,"auditedAt": "2026-01-01T00:00:00Z"}
indexable: a 200 answer, nonoindex(robots meta orX-Robots-Tag), and no canonical URL pointing to a different page.imageAltCoverage: the share of<img>elements with analtattribute. An emptyalt=""counts: it's the right markup for decorative images.imagesEmptyAltcounts those, for you to check they really are decorative; an image that is a link's only content and has no alt text is reported asimage-links-without-text.score: see How the score is calculated.previousScore: its value in the previous audit, with the comparison on.brokenImages,oversizedImages,imagesNotChecked: see Images.wordCount: words of visible text in<body>(scripts, styles and inline SVG aside).structuredDataTypes: the@typevalues of the page's JSON-LD blocks (nested ones and@graphincluded). Only the types are reported, not the rest of the data.brokenLinks: links whose check ended in a 4xx or 5xx answer (except 401, 403, 407, 429 and 451, which usually mean a site refused an automated check and may work in a browser), a domain that doesn't exist, or an error (timeout, too many redirects). The first 100 per page;brokenLinkCounthas the full number. Links robots.txt disallows aren't requested, so they're never reported as broken.foundOn: the page whose link led the crawl here (nullfor start and sitemap pages).changeType,changedFields,changedIssues,previousStatusCode: see Changes since the last audit.null(or empty) when the comparison is off.
Issues
| Code | Severity | Points | When |
|---|---|---|---|
http-error / request-failed | error | score 0 | The page answers 4xx/5xx, or couldn't be loaded at all |
broken-links | error | −20 | One or more links on the page are broken |
broken-images | error | −20 | One or more images on the page are broken |
missing-title | error | −20 | No <title> |
redirect-chain / redirect | warning / notice | −8 / −2 | The address redirects twice or more / once |
redirects-off-site | warning | −8 | The address ends up on another site |
not-https | warning | −8 | Served over plain http |
slow-response | warning | not scored | Over 1,000 ms to the response headers |
title-too-long / title-too-short | warning / notice | −8 / −2 | Over 60 / under 30 characters |
multiple-titles | warning | −8 | More than one <title> |
missing-meta-description | warning | −8 | No meta description |
meta-description-too-long / -too-short | notice | −2 | Over 160 / under 70 characters |
missing-h1 / multiple-h1 | warning / notice | −8 / −2 | No <h1> / more than one |
noindex / nofollow | warning / notice | −8 / −2 | The robots meta tag or X-Robots-Tag says so |
missing-canonical / canonicalised | notice | −2 | No canonical URL / it points to another page |
canonical-other-site | warning | −8 | The canonical URL is on another site |
images-missing-alt | warning | −8 | Images without an alt attribute |
image-links-without-text | warning | −8 | A link whose only content is images without alt text (and no aria-label or title) |
oversized-images | warning | −8 | Images over 200 KB by their Content-Length |
images-missing-size | notice | −2 | Images without both width and height attributes (the page can shift while they load) |
invalid-structured-data | warning | −8 | A JSON-LD block that doesn't parse |
missing-open-graph / missing-twitter-card | notice | −2 | No og:title, og:description or og:image / no twitter:card |
missing-lang | notice | −2 | No lang on <html> |
thin-content | notice | −2 | Under 200 words on a 200 page |
The length, size and speed thresholds are common SEO conventions, not search engine rules.
How the score is calculated
Every page gets a score from 0 to 100, worked out from its issues and nothing else, so you can check it by hand:
- Start at 100.
- Subtract 20 for each error, 8 for each warning and 2 for each notice found on the page (the Points column above). Each issue code counts once per page: 5 broken links cost 20, like 1.
- A page that doesn't load (
http-error,request-failed) scores 0. slow-responseisn't counted: response time is measured from Apify's servers and changes from run to run, so it would make the score move without anything on the page changing. It's still listed as an issue.- The score never goes below 0.
The site score (siteScore in the summary) is the average of the scores of every page audited, rounded to the
nearest whole number (halves round up). An example: a page with a broken link (error), an image without alt text
and an oversized image (2 warnings), and images without width and height (a notice) scores 100 − 20 − 16 − 2 = 62.
The points are ours, not a search engine's: a guide to where to start, not a ranking prediction. What counts as an issue is the table above; a change to the formula will be in the changelog.
Images: what is checked and what isn't
Checked: every <img> on an audited page for an alt attribute (missing, or empty), width and height
attributes, and whether it's a link's only content without alt text. With Check images on, each distinct image
address (src) gets one HEAD request per run, wherever it's hosted (a CDN too), counted against Max link checks
per run: an error answer (4xx other than 401, 403, 407, 429 and 451; 5xx; a dead domain; a timeout) makes it
broken, and a Content-Length over 200 KB makes it oversized.
Not measured:
- The real file size when the server doesn't send a Content-Length (the image isn't downloaded to measure it; if HEAD fails and the GET fallback runs, the size is read only when the header is there or the whole body arrives within 2 MB).
- Pixel dimensions, format and compression (whether a JPEG could be a smaller WebP, or a 4000 px image shown at 400 px): the file isn't opened.
srcset,<picture><source>, lazy-loading attributes (data-src) and CSS background images: onlysrcis read, so an image that only has a placeholder there is checked as that placeholder.- Whether an alt text is good, or an empty
alt=""is right: only that it's there. - Images added by JavaScript after the page loads.
Site summary (SUMMARY)
The run's key-value store has a SUMMARY record (the "Site summary" link on the run's Output tab), covering every
page in the results: pagesAudited, siteScore, scoreBands (pages scoring 90-100, 50-89 and 0-49),
lowestScorePages, statusCodes, indexablePages, issueTotals per severity, issues (pages per issue code),
worstPages, duplicateTitles and duplicateMetaDescriptions (with the pages sharing each),
brokenLinks (each broken link once, with the pages it's on), averageResponseTimeMs, slowestPages,
averageWordCount, imagesMissingAlt, brokenImages and oversizedImages (each image once), structuredDataTypes
and notRequested (pages that were never requested,
with the reason). Lists stop at 1,000 entries; the counts are always complete. With the comparison on, it also has
comparison (below); in the only-changed mode it still covers every page audited, returned or not.
Changes since the last audit
With Compare with the previous audit (or Only return changed pages) on, the run remembers each page in a
key-value store in your own account (seo-audit-memory, one record per audit) and compares with it next time:
changeType:new(not in the previous audit),changedorunchanged.nullon the first run, which is the baseline: nothing is reported as new.changedFields: which ofstatusCode,finalUrl(the redirect target),title,metaDescription,h1,canonicalUrl,noindex(robots meta orX-Robots-Tag),brokenLinksandscorechanged. The score only changes when an issue was added or fixed, soscorealways comes withchangedIssuessaying which;previousScorehas the score last time.changedIssues: issue codesaddedandfixedon the page, the page's broken links that are new (brokenLinksAdded) and how many of its broken links were fixed (brokenLinksFixed).previousStatusCode: the status last time.
The summary's comparison has previousAudit (its date, run and dataset IDs, and whether its crawl was complete),
pagesNew, pagesChanged, pagesUnchanged, pagesRemoved and the lists of each, issuesAdded and issuesFixed
per issue code (pages audited both times), newBrokenLinks (with the pages they're on) and fixedBrokenLinks (each
marked works or no longer linked), siteScore, previousSiteScore and siteScoreChange, and
pagesReturned / unchangedNotReturned.
What it deliberately doesn't report:
- Removed pages when the crawl may have missed some. A page is reported removed only when this crawl was
complete. If it stopped at Max pages per run or the maximum cost per run, a start page or sitemap failed, or pages
refused the crawler, failed or answered 5xx (a block or an outage can hide the rest of the site),
pagesRemovedisnullandincompleteReasonssays why; those pages are remembered and compared again next time. - A broken link as fixed when it had no clear answer. A link that was broken and this time wasn't checked (the link-check limit), was refused or was rate-limited stays broken in the memory. The same goes for a page's broken or oversized images when one of its images wasn't checked or its host refused the check.
- Image issues on the first comparison after upgrading to 1.2. An audit remembered by 1.1 never looked at image
sizes or broken images, so what the new checks find on a page the first time it's compared again is recorded,
not reported as added (and its
previousScoreincludes it). From then on, image issues are compared like any other. - Response-time noise.
slow-responseisn't compared: response times vary on every run.
The memory holds the status code and short fingerprints (hashes) of each page's fields, its issue codes and its
broken links, not the pages themselves, so it tells you that a title changed, not what it was: the previous
values are in the previous run's dataset (previousAudit.datasetId, while Apify keeps that run's storage). A page
cut by your maximum cost per run isn't remembered, so it's reported again next run.
Run it on a schedule, or from your own code
- Save your input as a task (Save as a new task, top right of the actor page) and add it to a schedule (Console → Schedules → Create new): for example weekly, early Monday, so the report is ready at the start of the week.
- Collect results: download the dataset as JSON, CSV or Excel; fetch the latest run's results from the API
(
GET https://api.apify.com/v2/actor-tasks/<task id>/runs/last/dataset/items?status=SUCCEEDED&format=csv, with your API token); fetch the summary withGET https://api.apify.com/v2/actor-tasks/<task id>/runs/last/key-value-store/records/SUMMARY; let a webhook tell your system when a run succeeds; or connect it to Make, Zapier or n8n through Apify's integrations. The task's Integrations tab can also post to Slack or email you (Gmail) after each run.
To hear only about what changed, turn on Only return changed pages; the steps are in Monitor your site. Between weekly audits, a daily Bulk URL & Broken Link Checker run with Only report URLs that changed since the last run catches newly broken URLs.
Can I use SEO Audit Crawler from an AI agent (MCP)?
Yes, through Apify's MCP server: add https://mcp.apify.com?tools=humble-echidna/seo-audit to your MCP client (or
let the agent find it with the server's actor search). The agent passes the site and a small page limit, e.g.
{"startUrls": ["https://example.com/"], "maxPages": 20}, and reads each page's issues.
Who it's for
SEO agencies, freelancers and in-house web teams who look after the same sites every week or month. Point it at a client's site, and each run gives you one row per page with its issues, ready to filter in a spreadsheet and hand to whoever fixes the pages, plus a site summary for the report.
Why this one?
- Broken links per page, included in the price. Every link on your pages is checked once per run (HEAD first, GET when a server answers HEAD wrongly), and each page lists its own broken links with their status code and link text. Links to pages the crawl reads reuse that answer instead of a second request.
- Issues you can act on, each with a code, a severity and a plain-English message, most severe first. Filter or sort by them in the dataset, or read the totals in the summary.
- The whole redirect chain, hop by hop, for every page.
- A score you can check by hand. Every page and the site get a 0-100 score from a published formula (100 minus 20, 8 or 2 points per error, warning or notice), not a black box, and the score's change shows in each comparison.
- Images checked in the same run: broken images, files over 200 KB, missing alt text and width/height, image links without text, with one HEAD request per image.
- A free site summary: it's a key-value record, not a dataset row, so it isn't charged.
- Changes since the last audit, per page and for the site: new, changed and removed pages, issues added and fixed by type, new and fixed broken links. With only-changed pages on, unchanged pages cost a tenth of a returned page, and a crawl that was cut short never reports the pages it didn't reach as removed.
- Polite and safe. It identifies itself honestly (User-Agent
HumbleEchidnaApify), follows your robots.txt (read once per site per run) and Crawl-delay, sends at most 2 requests at a time to your site, and only requests public web addresses on the standard ports (80 and 443). Pages robots.txt disallows are never requested and never charged. - Reliable. A page that fails is reported as a page with an error; it never affects the rest of the run. The
run log and the
RUN_STATSrecord say what happened to each start URL.
Limits
- Reads HTML as your server sends it: content, links and tags that JavaScript adds after the page loads aren't seen.
- Only public web addresses on the standard ports (80 for http, 443 for https) are requested, for every redirect hop too. robots.txt is respected on every hop; a site whose robots.txt can't be read (server error) is treated as disallowing everything for the run, as the robots.txt standard says. A site that asks for more than 30 seconds between requests isn't crawled.
- No proxies and no login: pages behind a login or bot protection come back as they answer an automated request (usually 403).
- Pages are read up to 2 MB; a failed request is retried once; each page gets up to 60 seconds in total.
- A site that answers a link check with HTTP 429 (too many requests) is left alone for the rest of the run: its
other links stay unchecked (
linksNotChecked, andlinkHostsThrottledinRUN_STATS). Large sites such as GitHub often do this when a page links to hundreds of their pages. <a href>links are followed and checked, and<img src>images are checked; scripts, stylesheets,srcsetand CSS images aren't. See Images for what isn't measured.- A page reached through a redirect whose target the crawl also reads at the same depth may be requested twice (once as each address); both rows are reported, since both addresses are linked.
FAQ
Why is a page I can open in my browser reported with a 403?
Some sites or firewalls block automated requests. This crawler doesn't get around that; allow the
HumbleEchidnaApify User-Agent if you want it audited.
Why wasn't a page audited?
It may be deeper than the crawl depth, past the max pages, only linked by JavaScript, or disallowed by robots.txt
(then it's in the summary's notRequested, not charged).
Does it compare an audit with the previous one?
Yes, when you turn on Compare with the previous audit: each page says whether it's new, changed or unchanged, with the fields and issues that changed and its score before and after, and the site summary counts issues added and fixed, new and fixed broken links, pages added and removed, and the site score's change since the previous run of the same audit. Only return changed pages returns just the new and changed pages ($10.00 per 1,000) and charges each unchanged page it rechecked $1.00 per 1,000 (a 100-page site with nothing changed: $0.10 a run). The first run with it on is the baseline. See Changes since the last audit.
How is the SEO score calculated?
100, minus 20 for each error, 8 for each warning and 2 for each notice found on the page (each issue code once), 0
for a page that doesn't load, never below 0; slow-response isn't counted. The site score is the average over every
page audited. The full rules and the points per issue are above. It's our weighting
of common SEO checks, meant to show where to start, not a prediction of rankings.
Does it check images?
Yes: alt text (missing or empty), width and height attributes, image links without text, and, with one HEAD request
per distinct image, broken images and files over 200 KB by their Content-Length. It doesn't open the files, so pixel
size, format and compression aren't measured, and it reads src only, not srcset or CSS backgrounds. See
Images.
Why are unchanged pages charged at all?
Because they're still crawled: finding out that a page didn't change takes the same requests and link checks as auditing it, and that's what a monitoring run costs to run. They're charged a tenth of a returned page, and never when the page refused the crawler or failed.
Why doesn't it list removed pages?
It only does when the crawl was complete. Raise Max pages per run above the number of pages on your site (and
check the maximum cost per run covers the pages that change); the summary's incompleteReasons says what cut the
crawl short.
Does it check rankings, backlinks or Core Web Vitals?
No. It reads your pages' HTML and headers only. responseTimeMs is the time to the response headers from Apify's
servers, not a browser speed score.
Something that used to work now fails. Why?
The run log and the RUN_STATS record name the start URL and what went wrong; every other page in the run is
unaffected. Please open an issue with the input you used.
Is it legal to run an SEO audit on a website?
It's built for auditing a site you own or manage. It requests only your start URLs, your sitemaps and the pages and links they lead to, the way a search engine crawler would: no login, honouring robots.txt and Crawl-delay, identified by its User-Agent. It reports what the pages say about SEO (titles, tags, links, status codes), not personal data, and it uses no third-party SEO data. You're responsible for auditing only sites you're allowed to.
Related actors
| Actor | Use it when |
|---|---|
| Bulk URL & Broken Link Checker | Between full audits: check the sitemap daily and get only the URLs whose status, redirect or canonical changed ($1.00 per 1,000 URLs; unchanged URLs are free). |
| Sitemap URL Extractor | You want the full URL list of a site from its sitemaps, with lastmod, for planning a crawl or finding pages missing from the sitemap. |
| Website & Page to Markdown for RAG and LLMs | You want the pages' main content as Markdown, for content reviews. |
Feedback and support
Found a bug, or need a check that isn't here? Open an issue on the Issues tab with the input you used.
Versions
Current version: 1.2. See the Changelog tab for what changed in each version.