Versions follow MAJOR.MINOR.PATCH (src/version.py); Apify shows MAJOR.MINOR from .actor/actor.json.
Every run logs its version and records it in the RUN_STATS key-value record.
- README: real screenshots of the input form and the Output table, from our own runs.
- Sitemap mode: a sitemap that also lists another site's URLs is read to the end. It used to stop at that batch
and mark the audit incomplete, which held back removed pages in the comparison.
- A 0-100 score per page and for the site, from a published formula (README: How the score is calculated): 100,
minus 20 per error, 8 per warning and 2 per notice issue code on the page (each code once), 0 for a page that
doesn't load (
http-error, request-failed), never below 0. slow-response isn't counted (response times vary run
to run). New field score; SUMMARY gets siteScore (the average over every page audited, halves rounded up),
scoreBands (90-100, 50-89, 0-49) and lowestScorePages; RUN_STATS gets siteScore; the status message
shows it. The README's issue table has the points for every code, checked by a test.
- Image checks. Read from the page:
imagesEmptyAlt (alt="" present but empty), imagesMissingSize (no width
and height attributes; new notice images-missing-size), imageLinksWithoutText (a link whose only content is
images without alt text; new warning image-links-without-text). With the new Check images input (on by
default), each distinct <img src> gets one HEAD request per run (GET only when HEAD answers an error), wherever
it's hosted, counted against Max link checks per run: brokenImages / brokenImageCount (new error
broken-images) and oversizedImages over 200 KB by Content-Length (new warning oversized-images);
imagesNotChecked counts the rest. SUMMARY lists broken and oversized images once each. RUN_STATS counts
imagesChecked apart from linksChecked. Not measured, and the README says so: files without a Content-Length,
pixel size, format and compression, srcset/<picture>/lazy-loading/CSS images.
- Comparison (1.1's
compareWithPreviousAudit / onlyChangedPages): a page whose score changed has score in
changedFields and previousScore; since the score is computed from the compared issue codes, a score change
always comes with an issue added or fixed, so it counts as a change (and in the only-changed mode the page is
returned). SUMMARY.comparison gets siteScore, previousSiteScore and siteScoreChange. Broken and oversized
images are carried like broken links when an image had no clear answer (not checked, or refused). An audit
remembered by 1.1 isn't flooded on upgrade: the image issues found on a page the first time it's compared again
are recorded, not reported as added (pages the crawl doesn't reach stay marked until it does). The memory record
gains checks, legacy and siteScore; its format is otherwise unchanged, and turning Check images off is a
separate audit (its own baseline), while the default keeps 1.1's comparison.
- No new charged events: image checks and scores are free, like link checks. Image requests share the existing
link-check cap, so a run's worst case is unchanged.
- Changes since the last audit (
compareWithPreviousAudit, off by default): each page gets changeType (new,
changed or unchanged), changedFields (status code, redirect target, title, meta description, H1s, canonical
URL, noindex, broken links), changedIssues (issue codes added and fixed, broken links new on the page, how many
were fixed) and previousStatusCode. The SUMMARY record gets a comparison: the previous audit's date, run and
dataset IDs; pages new, changed, unchanged and removed; issues added and fixed per code; new and fixed broken
links. The first run with it on is the baseline (nothing reported as new).
- Only return changed pages (
onlyChangedPages, off by default; turns the comparison on): new and changed
pages are written and charged as usual ($10.00/1k); unchanged pages are still audited and counted in SUMMARY but
not written, and are charged the new unchanged-page event, $1.00/1k (a custom event, not a dataset row, so
never billed twice; charged in batches of 100; never for a page that refused, failed or answered 5xx again). The
baseline run charges every page as a row. In this mode the maximum cost per run limits the pages read: before a
page is read, the least it can cost is reserved (the unchanged price, or the page price on the baseline); a
changed page is written only if the rest of the page price fits, else it's reported again next run. Nothing is
reserved for unchanged pages while the event isn't priced on Apify.
- The memory is one record per audit (mode, start URLs and settings, hashed) in the user's own
seo-audit-memory
key-value store: per page the status code and short fingerprints of the fields, issue codes and broken links, no
page content (about 200 bytes a page). Only pages the user got (written, or unchanged in the only-changed mode)
update it, so a page cut by the maximum cost per run is reported again next run.
- Guards: pages are reported removed only when the crawl was complete (not cut by max pages or the maximum cost, no
failed start page or sitemap, no page that refused, failed or answered 5xx); otherwise
pagesRemoved is null,
incompleteReasons says why, and those pages stay remembered. A broken link without a clear answer this run (not
checked, refused, rate-limited) isn't reported fixed. slow-response isn't compared (timing noise).
- With both options off, the crawl, the requests, the charges and every existing field are as in 1.0.1; the four new
fields are
null (or empty) and SUMMARY.comparison is null.
RUN_STATS gains compare, onlyChangedPages, baseline, pagesNew, pagesChanged, pagesUnchanged,
pagesRemoved, unchangedNotReturned, crawlComplete and incompleteReasons; pagesAudited now counts
unchanged pages that weren't returned (pushed is still the rows charged); unchangedPagesCharged and
unchangedPagesFree count the rechecked pages charged and not charged.
- The link-check scheduling (hosts take turns, at most 2 checks at a time each; a host that answers 429 is left
unchecked for the rest of the run) moved to the shared
common/mms_linkcheck, so Bulk URL & Broken Link Checker
uses it too. Same results; one difference in timing: a worker no longer waits for a busy host while another
host's link is ready.
First release.
- Audits the on-page SEO of a site the user gives it: pages are found by crawling same-site links from start URLs,
or from the site's sitemaps (robots.txt
Sitemap: lines, else /sitemap.xml; the same discovery as Sitemap URL
Extractor), up to a crawl depth and a max pages per run.
- Per page: HTTP status, redirect chain, response time, content type and size, title and its length, meta
description and its length, H1 count and text, canonical URL, robots meta and X-Robots-Tag, indexable,
<html lang>, hreflang alternates, Open Graph and Twitter tags present, JSON-LD @type values (types only),
image count and alt coverage, internal/external link counts, word count, broken links, and a list of issues with a
severity (error, warning, notice).
- Broken links: every distinct link on the audited pages is checked once per run with the same checker as Bulk URL &
Broken Link Checker (HEAD first, GET when HEAD answers an error); links to pages the crawl reads reuse that
answer. Optional, with a per-run cap and an on/off switch for other sites' links. Hosts take turns (at most 2
checks at a time each), and a host that answers 429 is left unchecked for the rest of the run, so one
rate-limiting site can't stall the run.
- A site summary in the
SUMMARY key-value record (not charged): status codes, issue totals per severity and per
issue, worst pages, duplicate titles and meta descriptions, every broken link with the pages it's on, slowest
pages, averages, structured data types, and the pages that were never requested.
- robots.txt respected on every request and redirect hop (a disallowed page is never requested and not charged);
Crawl-delay honoured; at most 2 requests at a time per site; public addresses on ports 80 and 443 only.
- No third-party SEO data (no rankings, backlinks or traffic estimates): only what the site itself serves.
- Charged per page audited, through Apify's standard
apify-default-dataset-item event. Max pages per run and the
maximum cost per run are honoured before a page is requested.
- Failure isolation: a page that can't be read, or a start URL that fails, only affects itself.