SEO Page Audit: Meta, Headings, Links, Schema & Redirects
Pricing
from $6.00 / 1,000 page auditeds
SEO Page Audit: Meta, Headings, Links, Schema & Redirects
SEO page audit over plain HTTP: meta, headings, canonical, schema, hreflang, redirects, links and a 0-100 score per page. $6 per 1,000 pages. Errors are free.
Pricing
from $6.00 / 1,000 page auditeds
Rating
0.0
(0)
Developer
Kaizoku Data
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
12 hours ago
Last modified
Categories
Share
Audit the pages you list, or crawl one site from a start URL, over plain HTTP and get one dataset row per page: redirect chain, canonical, robots directives, title and description with length flags, headings, Open Graph and Twitter tags, JSON-LD types and errors, hreflang, image alt coverage, link counts, mixed content, and an issues[] list with a 0-100 score. No browser, no JavaScript, no proxy, no API key.
Price: $6 per 1,000 audited pages ($0.006 per page). Pages that cannot be audited (network errors, robots.txt blocks, non-HTML, refused access) are free.
- The whole on-page checklist in one row: status, final URL, redirects, response time, canonical (and whether it points at itself), meta robots plus
X-Robots-Tag, title and meta description lengths, H1 count and texts, heading outline,og:andtwitter:tags, JSON-LD types with JSON parse errors, hreflang (with an optional return-link check),lang, viewport, image and alt counts, internal and external links, word count, mixed content on https pages. - A score you can sort by: every row has
issues[](severity, code, message) and ascorefrom 0 to 100. The weights are in How the score works. - Optional extras, included in the page price: link status checks (HEAD, then GET), hreflang return-link check, and a per-site summary of
robots.txtandsitemap.xml. - Polite by design:
robots.txtis obeyed (a disallowed page becomes a freeblocked-by-robotsrow), at most 1 request per second per host by default (4 at most), aCrawl-delayin robots.txt honoured in full (never shortened; over 2 seconds only withallowSlowHosts, over 10 seconds never), an honest User-Agent, no JavaScript, and it never works around blocks. - Quick start: press Start. The prefilled input audits
https://quotes.toscrape.com/(a public scraping sandbox: one page, a few seconds, $0.006).
Use it on sites you own or are allowed to audit. The Actor only fetches the URLs you give it and, if you switch crawling on, links on the same host. It reads server-rendered HTML: content that a browser adds with JavaScript is not seen (see Limits).
Input
| Field | What it does |
|---|---|
Page URLs / start URLs (startUrls) | One URL per line, at most 500. A bare host gets https://. Each URL is audited as one page. With crawling on, they are the crawl's starting points. Default: https://quotes.toscrape.com/. |
Crawl the whole site (crawlSite) | Off by default. On: follow links to other pages on the same host (exact host name; www.example.com and example.com are different hosts) until Max pages. Disallowed links become free blocked-by-robots rows. |
Max pages (crawl only) (maxPages) | The most page rows the whole crawl produces, audited and free rows together. Default 1, maximum 500. Not used when crawling is off: then every listed URL is audited. |
Requests per second per host (requestsPerSecond) | 1 to 4, default 1. Applies to every request to a host (pages, robots.txt, sitemaps, link and hreflang checks). A Crawl-delay in robots.txt is honoured in full (never shortened) when it is longer than this gap. |
Allow slow hosts (allowSlowHosts) | Off by default. Off: a site whose robots.txt asks for a Crawl-delay over 2 seconds is skipped with free slow-host rows. On: it is audited and the delay is honoured in full for every request to that host (pages, link and hreflang checks, sitemaps), so the run takes longer; hosts that ask for more than 10 seconds are still skipped (free slow-host rows, "over the 10 s maximum this Actor honours"). The waiting is billed as run time on your account: at a 10 second delay one page costs about 9% of its price in compute at $0.20 per compute unit. |
Check link status (checkLinks) | Off by default. Checks the first N unique links of each page with HEAD, falling back to GET. Included in the page price. |
Links to check per page (maxLinksPerPage) | Default 10, maximum 25. Internal links come first. A link seen on many pages is checked once per run; at most 5,000 link requests per run. |
Check hreflang return links (checkHreflang) | Off by default. Fetches up to 10 alternate pages per page and checks that they link back. |
Summarise robots.txt and sitemap.xml (siteFiles) | Off by default. Adds one rowType: "site" row per site (at most 10 sites). Not charged. |
Timeout per page (timeoutSecs) | 5 to 60, default 30. |
Example inputs
1. Audit a handful of pages and check their links:
{"startUrls": ["https://example.com/", "https://example.com/pricing", "https://example.com/blog"],"checkLinks": true,"maxLinksPerPage": 15}
2. Crawl a small site, 200 pages, and summarise its robots.txt and sitemaps:
{"startUrls": ["https://example.com/"],"crawlSite": true,"maxPages": 200,"siteFiles": true}
3. International site: hreflang return links, gentle rate:
{"startUrls": ["https://example.com/en/", "https://example.com/de/"],"checkHreflang": true,"requestsPerSecond": 1}
Output
One dataset row per page. Abbreviated example:
{"rowType": "page","inputIndex": 0,"url": "http://example.com/shop","status": "audited","finalUrl": "https://example.com/shop/","httpStatus": 200,"redirectChain": [{ "url": "http://example.com/shop", "status": 301 }, { "url": "https://example.com/shop", "status": 301 }],"redirectCount": 2,"responseTimeMs": 212,"https": true,"lang": "en","title": "Shop: Blue Widgets for Home and Garden","titleLength": 38,"titleFlag": "ok","metaDescription": null,"metaDescriptionFlag": "missing","canonical": { "url": "https://example.com/shop/", "count": 1, "isSelf": true },"robots": { "meta": "index, follow", "xRobotsTag": null, "noindex": false, "nofollow": false },"h1Count": 1,"h1Texts": ["Blue Widgets"],"headings": { "h1": 1, "h2": 4, "h3": 6, "h4": 0, "h5": 0, "h6": 0, "skippedLevels": false },"openGraph": { "title": "Shop", "image": "https://example.com/og.png" },"twitter": { "card": "summary" },"jsonLd": { "blocks": 2, "types": ["BreadcrumbList", "Organization"], "parseErrors": [] },"hreflang": [{ "lang": "en", "href": "https://example.com/shop/", "valid": true }],"images": { "count": 14, "missingAlt": 2, "emptyAlt": 3 },"links": { "total": 63, "internal": 51, "external": 12, "internalUnique": 40, "externalUnique": 11, "nofollow": 3 },"wordCount": 612,"viewport": "width=device-width, initial-scale=1","mixedContent": { "found": false, "count": 0, "examples": [] },"issues": [{ "severity": "warning", "code": "meta-description-missing", "message": "No meta description." },{ "severity": "warning", "code": "images-missing-alt", "message": "2 of 14 images have no alt attribute." }],"score": 90,"scoreVersion": 1,"processedAt": "2026-10-20T10:00:00.000Z"}
With Check link status a linkCheck object is added (checked, ok, restricted, skipped, broken[], brokenInternal, brokenExternal). With Check hreflang return links an hreflangCheck object is added (missingReturn[], unreachable[]). With Summarise robots.txt and sitemap.xml each site gets a row with rowType: "site", robotsTxt and sitemaps[] (kind, URL count, newest lastmod, a few sample URLs).
The run summary (key-value store record RUN_SUMMARY) has counts, charged events, bytes downloaded, failures by code and why the run stopped.
Status values (only audited is charged)
status | Meaning | Charged |
|---|---|---|
audited | The page was fetched (any HTTP status, a 404 page included) and analysed. | Yes, $0.006 |
blocked-by-robots | robots.txt disallows this URL (or a redirect target). It was not requested. | No |
non-html | The URL returned something other than HTML (PDF, JSON, image, ...). | No |
error | See errorCode and message. | No |
Error codes: invalid-url, blocked (private, local or internal address), platform (see Privacy and safety), dns, timeout, network, too-large (HTML over 5 MB), redirects (more than 5, or a loop), site-blocked (the site answered 403, 429 or a bot challenge), host-blocking (the host already refused twice in this run), robots-unreachable (its robots.txt could not be read, so the Actor does not guess), slow-host (the host's robots.txt asks for a Crawl-delay over 2 seconds and allowSlowHosts is off, or over the 10 s maximum this Actor honours, which is skipped even with allowSlowHosts; the delay is never shortened; free, nothing was fetched from the host except its robots.txt; also reported for a redirect target, a link, an hreflang alternate or a sitemap on such a host), analysis-failed (analysis timeout, memory limit or nesting over 1,000 levels), push-failed (the row could not be saved), unexpected.
A page that answers 4xx or 5xx other than 403 and 429 is a valid audit result: it is charged and gets a critical http-error issue.
How the score works
score = 100 - 40 per critical issue code - 15 per error code - 5 per warning code - 0 per info code, never below 0. Each issue code counts once per page, however many times it occurs. scoreVersion is 1; if the weights change, the version changes.
| Severity | Codes |
|---|---|
| critical (-40) | http-error (status 400 or above) |
| error (-15) | title-missing, h1-missing, canonical-multiple, canonical-invalid, noindex, jsonld-parse-error, mixed-content, broken-internal-links |
| warning (-5) | not-https, redirect-chain-long (3+ redirects), slow-response (first byte over 2,000 ms), title-too-long (over 60 characters), meta-description-missing, h1-multiple, h1-empty, canonical-missing, canonical-not-self, nofollow, lang-missing, viewport-missing, hreflang-invalid-code, hreflang-no-self, hreflang-missing-return, images-missing-alt, thin-content (under 100 words), broken-external-links |
| info (0) | redirect-temporary (302/307 in the chain), title-too-short (under 30), meta-description-too-short (under 70), meta-description-too-long (over 160), heading-levels-skipped, open-graph-incomplete, twitter-card-missing, jsonld-none, hreflang-no-x-default |
The score is a checklist summary of what the Actor can see in the HTML and headers. It is not a prediction of rankings and not a Google metric. The title and description length thresholds are common rules of thumb in characters, not pixel widths. responseTimeMs is time to first byte measured from the Actor's server, not a Core Web Vital.
Pricing
Pay per event: $0.006 per page with status audited, plus Apify's standard Actor-start event. There is no additional start fee from this Actor. Link checks, hreflang checks and site-file summaries are included in the page price.
| Pages audited | Cost |
|---|---|
| 1 | $0.006 |
| 10 | $0.06 |
| 100 | $0.60 |
| 500 | $3.00 |
| 1,000 | $6.00 |
Set Max total charge in the run options to cap spending: the run stops cleanly when the limit is reached and never charges for a row it did not deliver. Apify subscription discounts apply on top.
Use it via API, n8n or Make
Get an API token in Apify Console, Settings, API & Integrations, and keep it in an environment variable (APIFY_TOKEN below). Never paste it into shared code.
cURL (starts a run, waits for it and returns one row per page; the synchronous endpoint waits up to 5 minutes, so use it for a few pages and the clients below for big crawls):
curl -X POST "https://api.apify.com/v2/acts/ataism~seo-page-audit/run-sync-get-dataset-items?token=$APIFY_TOKEN" \-H "Content-Type: application/json" \-d '{"startUrls": ["https://quotes.toscrape.com/"], "maxPages": 1}'
Python (pip install "apify-client>=3"):
import osfrom apify_client import ApifyClientclient = ApifyClient(os.environ["APIFY_TOKEN"])run = client.actor("ataism/seo-page-audit").call(run_input={"startUrls": ["https://example.com/"],"crawlSite": True,"maxPages": 50,})for page in client.dataset(run.default_dataset_id).iterate_items():if page.get("status") != "audited":print("not audited (free):", page["url"], page["status"])continueerrors = [i["code"] for i in page["issues"] if i["severity"] in ("critical", "error")]print(page["score"], page["finalUrl"], errors)
(With apify-client versions before 3.0, call() returns a dict: use run["defaultDatasetId"].)
JavaScript (npm install apify-client):
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: process.env.APIFY_TOKEN });const run = await client.actor('ataism/seo-page-audit').call({startUrls: ['https://example.com/', 'https://example.com/pricing'],checkLinks: true,});const { items } = await client.dataset(run.defaultDatasetId).listItems();for (const page of items) console.log(page.score, page.finalUrl, page.issues?.length ?? 0);
n8n:
- Start with any trigger that gives you page URLs (a schedule, a form, a sheet row).
- Add the Apify node, operation Run an Actor and get dataset, Actor
ataism/seo-page-audit, input{"startUrls": ["{{ $json.url }}"]}. - Filter on
scoreor onissuesin the next node and send the failures to Slack, email or a sheet.
Make:
- Add the Apify module Run an Actor, choose
ataism/seo-page-audit, put your URLs in the input JSON and set Run synchronously to Yes. - Add the Apify module Get Dataset Items and map the run's default dataset ID (one bundle per page).
- Route low scores to your next module (a sheet, a ticket, a message).
Privacy and safety
- What is stored: only the dataset rows you asked for and the run summary (plus a
PROGRESSrecord used to resume after a restart). Page text is not stored: rows hold counts, flags, tags, link counts, and short fields (title, descriptions, H1 texts, Open Graph and Twitter text). - Contact details are removed from those short fields. Email addresses and phone numbers are replaced by
[email removed]and[phone removed], and the same is applied to URLs in the output. Page titles and headings can still contain other personal information (a person's name on a profile page, for example); this Actor does not detect names. Audit pages whose content you are entitled to process. - Public addresses only. Only
httpandhttpsURLs to public addresses are fetched. Private, loopback, link-local and internal addresses are blocked, including after redirects (at most 5, each re-checked) and at connection time. - Platforms are refused. YouTube, TikTok, Instagram, Facebook, X/Twitter, Vimeo, SoundCloud, Spotify, Twitch, Dailymotion, Reddit, LinkedIn and Google domains return error code
platformbefore any DNS lookup or request. - No workarounds. If a site answers 403 or 429, or shows a bot challenge, the row is an uncharged
site-blockederror and the Actor stops sending requests to that host after two such answers. It does not retry, rotate addresses or fake a browser. - You are responsible for having the right to audit the sites you give it (your own sites, or sites whose owners allow it). Check the target's terms. The Actor obeys
robots.txtand keeps request rates low, but that does not replace permission.
Limits and honest caveats
- No JavaScript. Single-page apps that render content in the browser will show little or no content in the HTML (low
wordCount, missing H1,thin-content). A fetched page is analysed as the server sent it. - HTML up to 5 MB per page; larger pages are
too-largeerrors, not charged. At most 500 URLs in the list and 500 pages per crawl, at most 25 link checks per page and 5,000 per run, at most 10 hreflang alternates per page, at most 8 sitemap files per site (5 MB each, 10 MB unpacked). Sitemap files are fetched under the same rules as pages: robots.txt, the refusal rules below and the request rate. - Run-level safeguards for unpaid work: the run stops after 50 pages that could not be audited or that only led to a page already audited (for example a link that redirects to the home page), 100 MB downloaded for them, or 10 minutes spent on them, and after 60 minutes in total (
stoppedByin the run summary says which). - Crawl-delay: a
Crawl-delayin robots.txt is honoured in full for every request to that host (pages, redirect hops, link and hreflang checks, sitemap files), counted from the moment its robots.txt was answered, so the first page waits too. Waiting is not part of the request timeout. Over 2 seconds the host is "slow": it is skipped (freeslow-hostrows) unless you setallowSlowHosts, because waiting between requests makes the run long and its compute cost can exceed the page price; over 10 seconds it is skipped even then (freeslow-hostrows; the delay is never shortened to fit). Link and hreflang checks of a page do not wait past their 60 s budget: a link whose host would make them wait longer is counted asskipped. - Speed: 1 request per second per host by default, so a 500-page crawl of one site takes about 8 to 10 minutes; link checks on one host add to that. Several hosts in a list are fetched in parallel.
- robots.txt: one file per host per run; the group whose user agent is exactly
seo-page-audit-actoris used if present, otherwise*; the longest matching rule wins. If a robots.txt answers 5xx or 429 or cannot be fetched, that host's pages are not fetched (freerobots-unreachablerows). A robots.txt that is missing (404) means everything is allowed. Only the first 512 KB of a robots.txt are read (the last, possibly cut, line is dropped); rules after that point are ignored. - Link checks: 401, 403, 429 and 999 answers are reported as
restricted, not broken, and are never retried; GET is tried only when HEAD gets 405, 501 or fails to connect. Two 403/429 answers from a host stop all further link, hreflang and page requests to it in this run. Links the robots.txt of their host disallows are skipped. - Hreflang is read from
<link rel="alternate" hreflang>tags in the HTML only (not from HTTP headers or sitemaps). - X-Robots-Tag: a
bot: directivetoken applies that bot's name to the tokens after it; unprefixed tokens and those forgooglebotorbingbotare applied. - Word count is whitespace-separated tokens in the HTML body without scripts and styles, so it is only a rough measure for languages written without spaces.
- Crawl: same exact host, links from
<a href>only; file links (images, PDFs, archives and similar) are not followed; a page that redirects to an already audited URL is dropped from a crawl (not charged); pages with anofollowrobots directive are audited but their links are not followed. - Memory: default 1,024 MB. Pages are analysed in worker threads limited to 384 MB each and 20 s, so a pathological page (for example 20,000 nested elements, or HTML nested more than 1,000 levels deep) becomes a free
analysis-failedrow instead of stopping the run; the number of pages in flight follows the run's memory (1,024 MB gives 2). - Time per page for the optional checks: link and hreflang checks share a 60 s budget per page, and each request has a 10 s timeout; links not reached in time are counted as
skipped.
FAQ
Why was my page not charged? Only audited rows are charged. Check status, errorCode and message.
Why is a 404 page charged? A page that answers 404 or 500 was fetched and analysed, and the result (status, redirect chain, response time, headers) is the audit finding. Refusals (403, 429, bot challenges), network errors, robots.txt blocks and non-HTML answers are free.
Can it audit pages behind a login or a JavaScript app? No.
Does the default run work without setup? Yes. It audits https://quotes.toscrape.com/ (a public sandbox site meant for scraping and testing), one page.
Open-source components
HTML parsing uses cheerio (MIT) and the Apify SDK (apify 3.7.2, Apache-2.0). Their dependencies are permissively licensed.