SEO Audit & Change Monitor avatar

SEO Audit & Change Monitor

Pricing

from $1.50 / site audit, up to 250 pages

Go to Apify Store
SEO Audit & Change Monitor

SEO Audit & Change Monitor

SEO audit for a whole website: broken links, redirects, duplicate titles and descriptions, sitemap and indexing problems. Get a health score and a fix for every issue, and see what changed since the last run. Up to 5,000 pages per run.

Pricing

from $1.50 / site audit, up to 250 pages

Rating

0.0

(0)

Developer

Ozzie Bagadirov

Ozzie Bagadirov

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

13 minutes ago

Last modified

Categories

Share

Run a technical SEO audit of a website, up to 5,000 pages per run: broken links, redirects, duplicate titles and descriptions, and sitemap and indexing problems. Each run gives you a health score, a list of issues sorted by how serious they are, with how to fix each one, and a row of data for every page. Run it again and it tells you what changed.

What you get

The Output view of every run opens on the Report. The menu above it switches to the other views. The Console shows the Report in a short window that does not scroll, so it shows the score, the coverage and the run's notes; open the address above it in a new tab to read the whole report.

ViewWhat is in it
ReportA readable summary: health score, errors, warnings and notices, what changed since the last run, and every issue with where it is and how to fix it. The header says how much of the site the run covered: every page found was checked, some pages found were left out, or stopped before checking every page found.
IssuesOne row per problem, errors first. Each row has the URL, the exact value that is wrong, and how to fix it.
PagesOne row per page checked: status code, title, headings, word count, indexability, sitemap membership and more.
ChangesOne row per thing that changed since the last run, with the value before and after.

The Issues, Pages and Changes views can be downloaded as CSV, Excel or JSON, or read through the Apify API. The menu also has Summary (JSON): the run's totals, for automation. The Issues and Changes links in a run's Output need your Apify API token when they are read outside the Console, because Apify does not sign links to named datasets. The Report, Pages and Summary links are signed.

What it checks

28 checks, in the three levels most SEO tools use:

  • Errors stop pages from working or from ranking. Fix these first.
  • Warnings hurt how well pages rank or how they look in search results.
  • Notices are worth knowing about but are often fine.
IssueSeverityWhat it meansissue value
Page did not respondErrorThe server did not answer at all.unreachable
Redirect loopErrorThe URL keeps redirecting and never reaches a page.redirectLoop
Server error (5xx)ErrorThe page answered with a 5xx server error.serverError
Page not found (4xx)ErrorThe page answered with a 4xx error, usually 404.brokenPage
Broken linkErrorA link on the page goes to a page that returns 404 or 410, or to a domain that no longer exists. Sites that only block automated checks, such as LinkedIn, are not counted.brokenOutlink
Missing titleErrorThe page has no <title>, or an empty one.titleMissing
Duplicate titleErrorAnother page on the site has exactly the same title. Pages set to noindex, or whose canonical tag points to another page, are not counted.duplicateTitle
Duplicate contentErrorAnother page on the site has exactly the same text. Pages set to noindex, or whose canonical tag points to another page, are not counted.duplicateContent
Hreflang without return linkErrorThe page names a language version that does not name it back. Pages that were not crawled are not counted.hreflangNotReciprocal
Long redirect chainWarningMore than two redirects before the page answers.redirectChainLong
Page stopped loadingWarningThe page started to load and then stopped, so it could not be checked.incompleteResponse
Page too large to checkWarningThe page is over about 1.6 MB, so its content was not checked and its links were not followed.pageTooLarge
Missing meta descriptionWarningThe page has no meta description, or an empty one.metaDescriptionMissing
Missing H1WarningThe page has no <h1> heading.h1Missing
Missing canonicalWarningThe page has no canonical link tag.canonicalMissing
Canonical points to another domainWarningThe canonical tag points at a different domain, so search engines credit that domain instead.canonicalOffDomain
Image without alt textWarningAt least one image has no alt attribute. alt="" is correct for decorative images and is not counted.imagesMissingAlt
Thin contentWarningThe page has fewer than 100 words of visible text.thinContent
Orphan pageWarningThe page is in the sitemap, but no other page that was checked links to it. Only reported when every page the crawl found was checked.orphanPage
Sitemap URL blocked by robots.txtWarningThe sitemap lists this URL, but robots.txt tells crawlers not to fetch it.sitemapUrlBlocked
RedirectNoticeThe URL redirects before the page answers.redirected
Title too longNoticeThe title is over 60 characters, so it may be cut off in search results.titleTooLong
Title too shortNoticeThe title is under 15 characters.titleTooShort
Meta description too longNoticeThe meta description is over 160 characters, so it may be cut off in search results.metaDescriptionTooLong
More than one H1NoticeThe page has more than one <h1> heading.h1Multiple
Not indexableNoticeThe page tells search engines not to index it. In the issues column it reads notIndexable: followed by the reason: meta-robots, x-robots-tag or canonicalised-away. The directives that count are noindex and none, set for all crawlers or for Googlebot.notIndexable
Duplicate meta descriptionNoticeAnother page on the site has exactly the same meta description. Pages set to noindex, or whose canonical tag points to another page, are not counted.duplicateMetaDescription
Page missing from sitemapNoticeThe page can be indexed but is not in the sitemap. Only reported when the site has a sitemap. Subdomains that sitemap lists no pages for are not checked.notInSitemap

Content checks only run on pages that loaded (status 2xx) and are HTML. A 404 page is reported once, as a 404, and not again for its missing description.

A check the run could not make is listed under Not checked this run, never as passed. The three duplicate checks, for example, compare only indexable pages, so a site that is entirely noindex has nothing for them to compare.

The health score is the share of checked pages that have no errors, from 0 to 100.

How to run a website audit

  1. Enter your website address in Website URL, for example https://www.example.com.
  2. Set Max pages if your site has more than 250 pages.
  3. Click Start. A 250-page site usually takes a few minutes.
  4. Read the Report. Click an issue to see which pages have it.
  5. Fix the errors, then run it again on the same site to see what changed.

The default timeout is 60 minutes, and a 5,000-page audit needs most of it. With a shorter timeout, up to half of it is kept for checking links to other sites and saving the results, so a 5-minute run crawls for about two and a half minutes. With a timeout of about 4 minutes or less, links to other sites are not checked, and the run log says so. With a timeout under about 40 seconds there is not enough time left after the crawl for one slow page, and the run log warns that the run could end before its results are saved.

For website monitoring, add a schedule (weekly is a good start) and connect an integration such as Slack or email, so you hear about changes and catch an SEO regression, like a new 404, at the next run. Every successful run ends with a one-line status, for example: Checked 250 pages. Health score 82/100. 3 changes found. Charged: audit-small. A message template can show it with {{resource.statusMessage}}. Integrations fire on every successful run, whether or not anything changed; to be alerted only on changes, read changes.changesTotal from summary.json in a webhook, Make or n8n step.

Changes since the last run

The first run saves a snapshot of your site. Every later run on the same site compares against the last run with the same URL patterns, link depth and sitemap setting, and lists what changed. The first run with new settings compares against the site's last run and says that what the crawl could reach has changed. The last eight snapshots are kept, plus the last one for each of the five settings used most recently, in your own Apify storage. example.com and www.example.com share one history store. Each spelling compares against its own last crawl, so switching between them reports the change of address once and nothing as removed.

The history is a key-value store named wcm-… in your Storage, and Apify keeps it until you delete it. Delete it, or set a History name, to start the history again.

The Report lists the changes, and shows how the count for each issue moved since the last run. It shows up to 50 changes, worst first: a page that became noindex, then a status that changed, then canonical and redirect changes, then titles and meta descriptions, and URLs newly found or no longer found last. Every kind of change the run found gets at least one row, so a site-wide rewrite of one field cannot fill the table on its own. The Changes view has every row: the URLs no longer found first, then every other change, each in URL order, with changeStatus:

  • NEW: the crawl found the URL this time and not last time
  • GONE: the crawl found the URL last time and not this time. The page was removed, or nothing the crawl checked links to it any more; the row's previousValue is its last status
  • FIXED: something was repaired, such as a page that works again
  • CHANGED: anything else that differs

A page that starts returning 404 gives you one row, not a row for every field of the error page.

If a run stops before checking every page it found (for example, at the page limit), pages it did not reach are not reported as GONE. Newly found and no-longer-found URLs, and the Since last run counts, also need runs that check every page they find: set Max pages above the size of your site, or use Only check URLs matching to keep the crawl under 5,000 pages. Until then you still get every change to the pages the run did check. A run whose maximum cost lowered Max pages below your site's size is a partial run in the same way, so raise the maximum cost if you want NEW and GONE rows.

Every changeType you can filter on

changeTypechangeStatusWhen
url-addedNEWFound this time, not found by the previous crawl
url-removedGONEFound by the previous crawl, not found this time: removed, or no longer linked
status-changedFIXED / CHANGEDFIXED when it went from 4xx/5xx back to 2xx
title-changedCHANGEDThe title text differs
meta-description-changedCHANGEDThe meta description differs
canonical-changedCHANGEDThe canonical URL differs
canonical-off-domainCHANGED / FIXEDThe canonical newly points to another domain, or stopped
became-noindexCHANGEDThe page went from indexable to not
became-indexableFIXEDAnd back again
redirect-chain-grewCHANGEDMore redirects than last time
redirect-chain-removedFIXED / CHANGEDFewer redirects. FIXED when the URL now answers directly
h1-count-changedCHANGEDThe number of H1 headings differs
images-missing-alt-changedFIXED / CHANGEDFIXED when fewer images lack alt text

How much does a site audit cost?

You pay once per audit. The price depends on how many pages were checked, not on how long the run took. Apify platform usage is included.

Pages checkedPrice per audit
Up to 250$2.00
251 to 1,000$5.00
1,001 to 5,000$15.00

Paid Apify plans get a discount. Before the crawl starts, the run log shows the most this run can cost.

  • Runs that check fewer than 10 pages are free. A page counts as checked when your site answered it and did not refuse the crawler.
  • You are never charged for more pages than you set in Max pages.
  • A run that cannot reach your site is free, and so are the pages your site refuses. A page that answers 401, 403 or 429 was not read, so it is never charged for.
  • If a run's maximum cost is below the price for your Max pages, the run checks fewer pages: at most 1,000 or 250, whichever the maximum cost covers, or 9 (free) if it covers neither. The run log says so before the crawl starts, and it is the first note on the Report.

Input

Only Website URL is required. In the form, the last five fields are under Crawl settings and Change tracking.

FieldDefaultWhat it does
Website URL—Where to start. A domain name, not an IP address. The crawl covers the whole domain, including its subdomains; on a hosted address such as name.wordpress.com, only that address. Public pages only.
Max pages250The most pages to check, from 10 to 5,000.
Check links to other sitesOnChecks up to 500 external links per run.
Use sitemap.xmlOnAlso starts from the sitemap, and runs the three sitemap checks.
Max link depth10How many links deep to follow from the start page and from sitemap pages.
Only check URLs matching—Patterns matched against the full URL, such as **/blog/**.
Skip URLs matching—Patterns such as **/tag/** to leave parts out.
Requests per minute120Lower it if your site is slow.
History name—Usually left empty. Set a name to keep a separate change history for the same site, or to start a fresh one.

When the URL patterns, the link depth limit, the crawl-trap filter or your robots.txt leave pages out, the orphan page check is skipped: a page that was left out may be the one that links to it. The Report says so. Links skipped only because of a session id, a tracking tag, or add-to-cart, replytocom, orderby or filter in the address do not count.

Example input:

{
"startUrl": "https://www.example.com",
"maxPages": 500
}

Output

The Issues view, one row per problem:

{
"severity": "error",
"issue": "brokenOutlink",
"name": "Broken link",
"category": "Links",
"url": "https://www.example.com/blog/launch",
"detail": "Links to https://partner.example.org/old-page — 404",
"howToFix": "Update the link or remove it."
}

The Pages view, one row per page (shortened here):

{
"url": "https://www.example.com/blog/launch",
"status": 200,
"severity": "error",
"issueCount": 2,
"issues": ["metaDescriptionMissing", "brokenOutlink"],
"title": "We have launched",
"titleLength": 16,
"metaDescription": null,
"canonical": "https://www.example.com/blog/launch",
"indexable": true,
"h1Count": 1,
"wordCount": 412,
"imagesMissingAlt": 0,
"inSitemap": true,
"depth": 0,
"responseTimeMs": 184,
"outLinks": [
{
"url": "https://partner.example.org/old-page",
"internal": false,
"status": 404,
"statusReason": null
}
]
}

A page row also has the redirect chain, hreflang tags, and up to 500 links with their status. depth counts links from the start page or, for pages found in the sitemap, from the sitemap.

The Changes view, one row per change:

{
"url": "https://www.example.com/pricing",
"changeType": "title-changed",
"changeStatus": "CHANGED",
"field": "title",
"previousValue": "Pricing",
"currentValue": "Plans and pricing",
"previousCrawledAt": "2026-09-09T08:00:00.000Z",
"crawledAt": "2026-09-16T08:00:00.000Z"
}

Run totals for automation are in the summary.json record in the run's key-value store: the health score, counts by severity and by issue, status codes, the change headline and the price band charged. It also gives the ids of the run's three datasets, in datasets.pages, datasets.issues and datasets.changes, and, when the maximum cost lowered Max pages, maxCostCap with the pages asked for, the pages checked and the maximum cost.

Use it from the API

Apify's usual routes — run-sync-get-dataset-items, runs/last/dataset and defaultDatasetId — return the Pages rows. The Issues and Changes rows are in datasets of their own, and the run object gives their ids:

const run = await client.run(runId).get();
const { items } = await client.dataset(run.storageIds.datasets.changes).listItems();

In Make or n8n, map storageIds.datasets.changes instead of defaultDatasetId.

FAQ

Does it run JavaScript?

No. It reads the HTML your server sends. Pages that build their content in the browser will look empty. In return, runs are fast and cheap.

Does it follow robots.txt?

Yes. Pages on your site that robots.txt disallows are not fetched. A Crawl-delay set for all crawlers in the robots.txt of the address you enter is followed, down to one request a minute. If that robots.txt answers with a server error or cannot be read, the run log and the Report both say so, and the crawl goes ahead as if it allowed everything. Search engines read a 5xx robots.txt as "do not crawl this site", so it is worth fixing.

What counts as my site?

The whole domain you enter, including its subdomains. Starting at blog.example.com also covers example.com and shop.example.com. Use Only check URLs matching to stay in one section. On a hosted address such as name.wordpress.com or name.substack.com, only that address is checked; links to the platform and to other sites on it are checked as links to other sites. If the address you enter redirects to another domain, that domain is checked instead, and the run log says so.

It has to be a domain name. An IP address, localhost, or any host without a dot is refused before anything is crawled, and so is an address that carries a username or password. The Input form cannot check that for you, so the run stops at once and says which it was.

Can it check pages behind a login?

No. Public pages only: the Actor cannot sign in. Do not put a secret token in the address: it is saved with the results and in the change history.

Some sites, such as LinkedIn and Stack Overflow, refuse automated requests but work fine in a browser. Those links keep their real status with blocked-by-target as the reason, and they are not reported as broken. Only 404, 410 and domains that no longer exist count as broken links.

Why did the crawl stop before my page limit?

A run stops early rather than fail when it is about to run out of memory or time. You get everything it found, the Report says why it stopped, and the price is set by the pages it checked. Very large pages and pages with several hundred links use the most memory. A run gets 1 GB by default and at most 2 GB: set Memory to 2 GB in the run options to go further. A run that already had 2 GB can check the rest of the site in parts with Only check URLs matching or Skip URLs matching. A run stopped for time can be run again with a longer timeout.

What does it look like in my server logs?

Like an ordinary desktop browser: it sends a desktop browser's own headers — Chrome's, Edge's, Safari's or Firefox's — from Apify's cloud servers, whose addresses change. There is no crawler name or fixed IP address to allow, and only the robots.txt rules for all crawlers (User-agent: *) apply. If your site's bot protection blocks the run, lower Requests per minute, or relax the protection while the run is going. Pages the protection refuses are not charged for, so a run it blocks costs nothing unless the run still read 10 pages or more.

What happens if I abort a run?

Results are saved only after the crawl and the link check have finished. If you abort a run before then, nothing is saved and you are not charged. For a shorter audit, lower Max pages instead.

What happens if a run is restarted?

If Apify moves a run to another server during the crawl, the crawl starts again from the beginning and you are charged once. If you resurrect a run that already completed its audit, it stops at once, because its results are already in the run's storage. Start a new run to check the site again.

Does my data go anywhere?

No. Results and history stay in your Apify account. The Actor only sends requests to your site, to wherever your own redirects lead, and to the external links your pages contain: up to 500 links a run, one or two requests each plus any redirects, and at most 2 requests a second to any one host, each redirect step included. You can turn the external link check off.

How do I get help?

Open an issue in the Issues tab on this Actor's page in Apify Store (not the Issues view of a run). Support is in writing only. Issues there are public, so share the run from the issue form rather than pasting private addresses or data.

Limits

  • Up to 5,000 pages per run.
  • HTML pages only. PDFs and images are listed with their status but not read.
  • Up to 500 links per page are recorded and checked.
  • Pages over about 1.6 MB are listed with their status but not read.
  • A redirect chain lists each URL, but not the status code of each step.
  • SSL certificates are not checked. A page with an expired or wrong certificate is checked like any other page. A link to another site that fails its certificate check is listed with the reason external-check-tls and is not counted as broken.
  • Sitemaps are read up to 50,000 URLs and 50 sitemap files. Past that, the check for pages missing from the sitemap is skipped, and the Report says so.
  • If the site's pages link to more than 100,000 different URLs in total, the orphan check is skipped, and the Report says so.