Complete SEO Audit Tool - Website Crawler with 0-100 Scores avatar

Complete SEO Audit Tool - Website Crawler with 0-100 Scores

Pricing

from $30.00 / 1,000 pages

Go to Apify Store
Complete SEO Audit Tool - Website Crawler with 0-100 Scores

Complete SEO Audit Tool - Website Crawler with 0-100 Scores

SEO checker, analyzer and website audit tool: crawl a site and get a 0-100 SEO score for every page, plus a site audit report. Technical and on-page SEO: meta tags, headings, Core Web Vitals, broken links, images and schema, each issue with its priority and a fix hint, as JSON.

Pricing

from $30.00 / 1,000 pages

Rating

5.0

(4)

Developer

My Smart Digital

My Smart Digital

Maintained by Community

Actor stats

27

Bookmarked

793

Total users

101

Monthly active users

7 days ago

Last modified

Share

Complete SEO Audit Tool

Crawl any website and get a 0-100 SEO score for every page, the exact issues behind that score, and a fix hint for each one — as structured JSON you can pipe into a report, a dashboard or an AI agent.

No account on the audited site, no browser extension, no manual checking. Give it a URL, get an audit.

Use it as a website SEO checker for technical SEO and on-page SEO, or as an SEO analyzer for a whole site rather than a single URL: you get an SEO report for every page and a site audit summary across them.

Who it's for

  • SEO freelancers and agencies running audits for clients, who need the findings as data instead of a PDF.
  • Developers wiring an SEO check into CI, a monitoring job or an internal dashboard.
  • AI agents that need a factual, scored snapshot of a site before writing recommendations.

What you get

Two kinds of records in the dataset:

  • One page record per audited page — overall score, nine category scores, and the full detail behind each one.
  • One site-summary record per site (the site a start URL lands on, with the start URLs that led to it) — averages across its pages, top issues ranked by how many of its pages they hit (one line per issue, whatever figure each page reports), score distribution, crawl coverage, and the titles and descriptions its indexable pages share, with every page of each group. Several sites audited in one run each get their own summary, never a blend of them; a site that could not be audited at all still gets one, with 0 pages and no score, and crawlCoverage.turnedAway counts the pages a site's anti-bot protection, a login or a rate limit answered in place of the page (see Limits). A run that reaches its timeout or its maximum cost before the end of the crawl stops in time to write these summaries, and says so in stoppedBy.

Each issue comes twice: as a plain sentence (easy to display) and in a structured form with an ID, a category, a priority, an estimated impact and a fix hint (easy to sort and filter).

Input

{
"startUrls": ["https://example.com"],
"crawlPages": true,
"maxPages": 5,
"maxConcurrency": 5,
"respectRobotsTxt": true,
"excludeUrlPatterns": ["/tag/", "/author/"],
"includeSubdomains": false
}

startUrls also accepts objects: [{ "url": "https://example.com" }].

Parameters

ParameterDefaultWhat it does
startUrls (required)—URLs to audit. Every start URL that leads to an HTML page is audited; one that leads to a file (an image, a PDF, a feed) is skipped and not billed. An address written without https:// (www.example.com) is read as https://. An entry that is not a web address is left out and named in the run's status message, and a run left without any address fails at once, with nothing billed.
crawlPagestrueFollow internal links and audit several pages. Set to false to audit only the start URLs.
maxPages5Pages audited per site (the site a start URL lands on), start URLs included. 5 for a preview, 20 for a standard audit, 50+ for a deep one. Drives both duration and cost.
maxConcurrency5Pages audited in parallel. Lower it on slow or rate-limiting sites.
respectRobotsTxttrueSkip URLs disallowed by robots.txt.
excludeUrlPatterns[]Substrings to skip ("/tag/"), or regex wrapped in slashes ("/\\/page\\/\\d+/"). Useful to keep CMS archive pages out of the crawl. Links to files need no pattern: they are never followed.
includeSubdomainsfalseAlso crawl subdomains of the start URL.

Every audit module can be switched off individually (auditMetaTags, auditHeadings, auditContent, auditTechnical, auditPerformance, auditSchema, auditLinks, auditImages, auditAccessibility). All are on by default.

What gets checked

CategoryChecks
Meta tagsTitle and description with length in characters and rendered pixels, titles and descriptions repeated across the indexable pages of a site (flagged on each page that repeats one, every page of each group listed in its site-summary), Open Graph, Twitter Cards as X reads them (a twitter: tag in a name attribute or, as X also accepts, a property one; the card's title, description and image taken from the Open Graph tags when their twitter: tag is absent, listed in fromOpenGraph and counted in complete; the card type, which nothing stands in for, earns the points), the character encoding (<meta charset>, its http-equiv form, or the server's Content-Type header, which the HTML standard accepts on its own: charset.declaredIn), canonical (including self-reference), robots meta (a page without one loses nothing, indexing being the default; a noindex one is reported and takes the points). A <head> ended early: a head may only hold title, meta, link, script, style, base, noscript and template elements, and browsers end it, as Google does, at the first other element or stray text (a hidden <div>, a tracking-pixel <img>, an <iframe>), so the head elements written below it are read as part of the body. The audit names that element and what it pushes out (headClosedEarly). A canonical outside the <head> is flagged (canonical.inHead: false), since Google only accepts it there: it then earns no points and is not used for indexability. A robots meta tag counts wherever it is, as it does for Google. Meta tags are read as Google reads them: their names in any letter case (name="Description" is the page's description, as name="ROBOTS" is its robots tag), and every robots and googlebot tag of the page, their rules added up, since Google applies the more restrictive one: a noindex in the second of two robots tags makes the page not indexable (robots.tags lists them). More than one <title> or meta description is flagged with each text (count, values; a theme and an SEO plugin each writing their own, most often), the first being the one read. The title is the page's own <title>, never the one of an inline SVG icon
HeadingsFull H1-H6 tree with the text of each heading as a reader sees it: words split where a line break or a block splits them on the page ("Welcome to<br>Paris" reads "Welcome to Paris", not "Welcome toParis"), code a theme leaves inside a heading (a <style> block, the <noscript> copy of a logo) left out, spaces of the markup folded, original case kept. Multiple or missing H1, skipped levels
ContentWord and character count of the page's content: the main element when it holds the page's content, else the whole page (contentArea), always without navigation, the site's header and footer, sidebars, pop-ups, cookie banners, accessibility toolbars and screen-reader-only text. Paragraphs, sentences, average sentence length, a reading-ease score and level in the page's own language (told by its text: Flesch for English, Kandel-Moles for French, Amstad for German, Fernández-Huerta for Spanish, Franchina-Vacca for Italian, Douma for Dutch, Martins for Portuguese; other languages and pages under 30 words are not scored, score: null with the reason; under 60 it costs the content score points and is reported, Reading ease 42/100 (Difficult, Flesch formula)), keyword density with top keywords, the language's function words left out. Whether the H1 and the title tag share a keyword (h1TitleConsistency, with the sharedWords; case, accents and plurals aside, every H1 of the page compared): an H1 and title share no keyword warning only when both exist and share none, consistent: null when one is missing, which is reported on its own
TechnicalThe HTTP status the server actually returned for the page (a 404, 410 or 5xx page is reported as such and flagged not indexable), HTTPS and certificate validity, a mobile viewport (width=device-width), robots.txt parsing, sitemap discovery (sitemaps declared in robots.txt first, then the usual locations; a sitemap only counts once the server actually returns one, never an HTML page served in its place, and a file is reported missing only when the server says so, with a 404, a 410 or an HTML page in its place), indexability from the robots and googlebot meta tags, the X-Robots-Tag HTTP header, the page status and, when the canonical points to another URL (relative ones included, resolved as a browser does), that URL's status. A server that turns a check away (login, anti-bot wall, rate limit) leaves it unverified: the canonical is never reported as broken, and robots.txt or the sitemap is never reported missing nor counted against the technical score (verified: false, with the statusCode the server answered). Before that, the browser that loaded the page asks for robots.txt and the sitemap itself. Whether the page needs JavaScript to show its content, measured rather than guessed: the words a visitor reads once the page's scripts have run are looked up in the HTML the server sent (rendering.contentShareInHtml), which is all that search engines that do not run scripts, and most AI crawlers, ever read. Below half of them, on a page of 50 words or more, the page gets a Content requires JavaScript to render warning; the same goes for its internal links (internalLinksMissingWithoutJS). A page whose HTML the browser cannot hand back (over 10 MB) is left unverified (verified: false), with nothing reported. Which AI crawlers robots.txt lets read the page (aiCrawlers): the search crawlers that decide whether it can be shown and cited in AI search answers (OAI-SearchBot for ChatGPT search, Claude-SearchBot, PerplexityBot) and the training crawlers (GPTBot, ClaudeBot, Google-Extended, Applebot-Extended, CCBot, meta-externalagent), each judged on the rules it obeys under the robots.txt standard (RFC 9309): its own group, else the * group, the longest matching rule winning. A blocked crawler comes with the line that blocks it. A search crawler kept out is a warning (robots.txt blocks AI search crawlers from this page), a training crawler kept out only a note, since it changes nothing in Google Search nor in AI search answers; neither counts in the score, keeping a crawler out being the site's choice. When robots.txt could not be read, nothing is concluded (verified: false). The page's hreflang annotations (hreflang), judged on Google's guide to localized versions: a value Google does not support (a country code alone such as jp, a reserved region such as en-UK, a UN region number such as es-419, an underscore, a three-letter language code), each with the reason; a set that leaves out the page itself (neither its URL nor its canonical URL); one code for two URLs; URLs not fully qualified (/fr/, //example.com/fr/); annotations outside the <head>, where Google does not read them (an element the head may not hold, such as an <img> or stray text, ends it early and pushes the links below it into the body); hreflang combined with a media attribute. Each is a warning, a missing x-default only a note, and none counts in the score: hreflang tells Google which version to show for which language and country, it does not make the page better or worse. Scored on four checks, as the share of their points the page earns: HTTPS 30, a mobile viewport 20, a sitemap 15 and robots.txt 15 (a sitemap or robots.txt the server turned away is left out rather than counted as missing), each failed one coming with its issue. Structured data is scored once, under Schema. The page's status, indexability, canonical, rendering, AI crawler access and hreflang come out as issues with their fix, and do not move this score; a page left out of the index by its status or a noindex rule has its overall score capped instead (see Scoring)
SchemaSchema.org structured data in JSON-LD, microdata and RDFa, with every type found: the items of a JSON-LD array or @graph, and each type of an item that has several. A JSON-LD block that is not valid JSON is listed in jsonLdErrors with the parser's message: one no parser can read (a stray quote, a missing comma) is a warning and its types are not counted, one that only carries comments or a raw line break in a string is noted and read anyway. Open Graph tags are not structured data and do not count. For the JSON-LD items Google turns into rich results (breadcrumb, product snippet and merchant listing, software app, local business and its subtypes such as Restaurant or Hotel, video, event, recipe, job posting), richResults says whether each has every property Google requires, and lists the missing ones in Google's notation (offers.price); markup for a rich result Google no longer shows (FAQ since 7 May 2026, How-to since September 2023) is noted. Scored 100 for readable schema.org markup, whatever its format and however many types it describes (Google asks for markup that truly represents the page, and reads an item nested in another as it reads one written apart), minus 20 for each JSON-LD block no parser can read and for each rich result whose items miss a required property, down to 30; 0 without readable markup
PerformanceLargest contentful paint (LCP) and cumulative layout shift (CLS), the two Core Web Vitals a crawl can measure, scored against Google's thresholds (LCP over 2.5 s or 4 s, CLS over 0.1 or 0.25) and reported as issues with a fix hint (LCP on a page loaded on its own only, see Limits); total blocking time, first contentful paint, time to first byte, page load time and a speed index estimate
LinksInternal and external counts with unique totals and samples, split by navigation / footer / content, nofollow vs dofollow, redirects with their target, broken links verified by an actual request: HEAD first, then GET when the server refuses HEAD (many do), since GET is what a visitor's click sends. A link the server turns away rather than answers (a login, a rate limit, a region block, an anti-bot wall such as a Cloudflare challenge or DataDome) is listed under unverified with the status it got, never reported broken nor counted against the score. The links checked are the first 10 of the page (checked, see Limits), and the links score loses up to 50 points for the share of broken ones among those that answered: 5 points per broken link when ten answered
ImagesAlt and title coverage, with images marked decorative (alt="", or role="presentation") counted apart and never as missing alt text, since that is the correct markup for an image that carries no content; an empty alt still counts as missing on an image that is the only content of its link, whose alt text is the link's anchor text. Real file size in KB (inline data: images measured from their own payload), dimensions, format, heavy image detection (a file over 200 KB is flagged isLarge and reported, judged on its weight and never on its pixel size: a 2,400-pixel WebP of 50 KB costs a visitor nothing, a 600-pixel PNG of 1 MB slows the page; on a responsive image, the file weighed is the one the browser actually loaded, servedUrl), and the HTTP status of every image, as the browser that loaded the page received it, so a picture that never loads is reported as such (broken) instead of being counted as a tiny file. An image the browser received or shows is never reported broken, even when a server turns the audit's own check away; an image the browser has not loaded (lazy loading far below the fold) whose check is turned away (a rate limit, an anti-bot wall, a login), or that nobody could reach (a timeout, a host name that does not resolve), is listed as unverified with the reason, never broken nor counted against the score. Image addresses written without their scheme (//cdn.example.com/photo.jpg, common on Shopify) are resolved as a browser does, so they are weighed and checked too
AccessibilityWCAG checks run by axe-core, the engine behind the accessibility audits of Lighthouse and of the browsers' developer tools: text below the WCAG AA contrast ratio (colorContrast, with the number of elements), links and buttons without an accessible name, form fields without a label, ARIA roles and attributes used incorrectly, zoom disabled by the viewport tag, invalid language codes, frames without a title, invalid list markup, no main landmark, no way to skip repeated content. Each failure is listed in violations with its impact, its number of elements, up to three of them (CSS selectors) and a link to the rule's explanation; the rules that applied and passed are in rulesPassed. Plus the lang attribute, a real skip link (skipLinks: a link to an anchor of the page, placed before its first link to another page, whose target exists) and a count of the elements named by ARIA (ariaLabels, a figure, not a quality). The score is the share of the checks that apply to the page and pass, each weighted by the impact of its failure as Lighthouse weighs it (critical 10, serious 7, moderate 3). Images and headings are judged in their own categories

Output

page record

{
"type": "page",
"pageUrl": "https://example.com/",
"title": "Example — Home",
"httpStatus": 200,
"audit": {
"overallScore": 90,
"categoryScores": {
"metaTags": 90, "headings": 100, "content": 80, "technical": 81,
"schema": 80, "performance": 100, "links": 100, "images": 96,
"accessibility": 90
},
"issues": {
"critical": ["1 image returning an error (4xx/5xx)"],
"warnings": [
"Title tag length is 65 characters (optimal: 30-60)",
"1 JSON-LD block cannot be parsed, so search engines get nothing from it (block 3: Expected ',' or '}' after property value in JSON at position 593 (line 1 column 594))",
"Color contrast issues detected (4 elements below WCAG AA)",
"2 links without an accessible name"
],
"info": ["Reading ease 42/100 (Difficult, Flesch formula): long sentences or long words", "Sitemap not found", "robots.txt keeps AI training crawlers off this page: GPTBot, CCBot", "4 redirects found", "No skip links found"],
"structured": {
"critical": [
{
"id": "BROKEN_IMAGES",
"message": "1 image returning an error (4xx/5xx)",
"category": "images",
"priority": 5,
"estimatedImpact": "high",
"fixHint": "Fix or remove the image URLs the server rejects: visitors see a broken image, and search engines index neither the file nor its alt text.",
"pagesAffected": ["https://example.com/"]
}
],
"warnings": [
{
"id": "TITLE_TOO_LONG",
"message": "Title tag length is 65 characters (optimal: 30-60)",
"category": "meta",
"priority": 3,
"estimatedImpact": "medium",
"fixHint": "Adjust title length to 30-60 characters for better visibility in SERPs.",
"pagesAffected": ["https://example.com/"]
},
{
"id": "INVALID_JSON_LD",
"message": "1 JSON-LD block cannot be parsed, so search engines get nothing from it (block 3: Expected ',' or '}' after property value in JSON at position 593 (line 1 column 594))",
"category": "schema",
"priority": 3,
"estimatedImpact": "medium",
"fixHint": "Fix the JSON syntax of each block listed in technical.structuredData.jsonLdErrors (the block number is its rank among the page's JSON-LD scripts): the usual cause is a double quote left unescaped in a text value, often in HTML pasted into a description. Then check the page with Google's Rich Results Test.",
"pagesAffected": ["https://example.com/"]
},
{
"id": "LOW_CONTRAST",
"message": "Color contrast issues detected (4 elements below WCAG AA)",
"category": "accessibility",
"priority": 3,
"estimatedImpact": "medium",
"fixHint": "Darken the text or lighten its background until the contrast ratio reaches 4.5:1 (3:1 for large text).",
"pagesAffected": ["https://example.com/"]
},
{
"id": "UNNAMED_LINKS",
"message": "2 links without an accessible name",
"category": "accessibility",
"priority": 3,
"estimatedImpact": "medium",
"fixHint": "Give each of these links text a screen reader can read out: visible text, or an aria-label on a link that only holds an icon; a linked image needs alt text naming the destination. Search engines read that text as the link's anchor text too.",
"pagesAffected": ["https://example.com/"]
}
],
"info": [
{
"id": "LOW_READABILITY",
"message": "Reading ease 42/100 (Difficult, Flesch formula): long sentences or long words",
"category": "content",
"priority": 1,
"estimatedImpact": "low",
"fixHint": "Shorten the longest sentences (under 20 words) and prefer common, shorter words. Reading ease is not a ranking factor for Google, but text that reads easily keeps visitors reading.",
"pagesAffected": ["https://example.com/"]
}
]
}
},
"metaTags": {
"title": {
"present": true,
"value": "Example — Home",
"length": 65,
"lengthPixels": 423,
"optimal": false,
"unique": true,
"recommendation": "Title too long (65 characters, ~423px). Recommended: 30-60 characters (~195-390px)."
}
},
"headings": {
"h1": { "count": 1, "texts": ["Main heading"], "optimal": true },
"h2": { "count": 9, "texts": ["Section one", "Section two"] }
},
"content": {
"wordCount": 1808,
"paragraphCount": 108,
"sentenceCount": 123,
"averageWordsPerSentence": 14.7,
"contentArea": "main",
"readability": { "score": 42, "level": "Difficult", "language": "en", "formula": "Flesch", "recommendation": "Low readability (Difficult). Shorten sentences and prefer shorter words." },
"keywordDensity": { "topKeywords": [{ "keyword": "hotel", "density": 1.27, "count": 18 }] }
},
"technical": {
"aiCrawlers": {
"verified": true,
"robotsTxt": "found",
"checked": ["OAI-SearchBot", "Claude-SearchBot", "PerplexityBot", "GPTBot", "ClaudeBot", "Google-Extended", "Applebot-Extended", "CCBot", "meta-externalagent"],
"searchBlocked": [],
"trainingBlocked": ["GPTBot", "CCBot"],
"blocked": [
{ "name": "GPTBot", "owner": "OpenAI", "use": "training", "for": "OpenAI model training", "rule": "Disallow: /", "line": 12, "group": "GPTBot" },
{ "name": "CCBot", "owner": "Common Crawl", "use": "training", "for": "Common Crawl's open dataset, used to train many AI models", "rule": "Disallow: /", "line": 12, "group": "CCBot" }
]
},
"hreflang": {
"present": true,
"annotations": 4,
"selfReferencing": true,
"xDefault": false,
"invalid": [
{ "hreflang": "en-UK", "href": "https://example.com/en/", "reason": "\"UK\" is not an assigned ISO 3166-1 country code, so Google ignores the country (the United Kingdom is GB)" }
]
},
"structuredData": {
"jsonLd": true, "microdata": false, "rdfa": false,
"types": ["Hotel", "BreadcrumbList", "WebSite"],
"schemas": [{ "type": "Hotel", "format": "json-ld" }, { "type": "BreadcrumbList", "format": "json-ld" }, { "type": "WebSite", "format": "json-ld" }],
"jsonLdErrors": [
{ "block": 3, "error": "Expected ',' or '}' after property value in JSON at position 593 (line 1 column 594)", "readable": false }
],
"richResults": [
{ "type": "Hotel", "feature": "Local business", "status": "eligible", "missing": [], "items": 1 },
{ "type": "BreadcrumbList", "feature": "Breadcrumb", "status": "eligible", "missing": [], "items": 1 }
],
"recommendation": "A JSON-LD block cannot be parsed: fix the JSON syntax (see jsonLdErrors) so that search engines can read it."
}
},
"performance": {
"timeToFirstByte": 468.1,
"pageLoadTime": 1214,
"firstContentfulPaint": 720,
"largestContentfulPaint": 1356,
"cumulativeLayoutShift": 0.0213,
"totalBlockingTime": 87,
"speedIndex": 1084.2,
"coreWebVitals": { "lcp": 1356, "cls": 0.0213 },
"loadedWithOtherPages": 0
},
"links": {
"internal": { "count": 13, "unique": 13, "inNavigation": 7, "inFooter": 10, "inContent": 4, "sample": ["https://example.com/pricing"] },
"external": { "count": 0, "nofollow": 0, "dofollow": 0, "sample": [] },
"checked": 10,
"broken": { "count": 0, "urls": [], "verified": [] },
"redirects": { "count": 4, "urls": [{ "url": "https://example.com/pricing", "status": 301, "redirectUrl": "https://example.com/plans" }] },
"unverified": { "count": 1, "urls": [{ "url": "https://example.com/account", "statusCode": 401, "reason": "The server turned the check away (HTTP 401): login, rate limit or region block" }] }
},
"images": {
"total": 13, "withAlt": 12, "withoutAlt": 0, "decorative": 1, "withTitle": 0, "optimized": 13, "largeImages": 0, "broken": 1, "unverified": 0,
"images": [
{ "url": "https://example.com/hero.webp", "alt": "Hotel reception", "width": 2000, "height": 1334,
"hasAlt": true, "decorative": false, "sizeKB": 57.2, "format": "webp", "isLarge": false, "httpStatus": 200, "broken": false },
{ "url": "https://example.com/team.jpg", "alt": "The team", "hasAlt": true, "decorative": false,
"format": "jpeg", "isLarge": false, "httpStatus": 404, "broken": true },
{ "url": "https://example.com/divider.svg", "alt": "", "hasAlt": false, "decorative": true,
"sizeKB": 0.4, "format": "svg", "isLarge": false, "httpStatus": 200, "broken": false }
]
},
"accessibility": {
"ariaLabels": 7, "langAttribute": true, "skipLinks": false,
"colorContrast": { "passed": false, "issues": 4 },
"engine": "axe-core 4.13.0",
"rulesPassed": ["aria-allowed-attr", "aria-hidden-body", "aria-required-attr", "aria-roles", "aria-valid-attr", "aria-valid-attr-value",
"button-name", "bypass", "html-lang-valid", "label", "landmark-one-main", "list", "listitem", "meta-viewport"],
"violations": [
{ "id": "color-contrast", "impact": "serious", "elements": 4, "description": "Elements must meet minimum color contrast ratio thresholds",
"helpUrl": "https://dequeuniversity.com/rules/axe/4.13/color-contrast?application=axeAPI",
"examples": [".cookie-banner .decline > span", "footer > p.legal", ".hero > .btn-ghost"] },
{ "id": "link-name", "impact": "serious", "elements": 2, "description": "Links must have discernible text",
"helpUrl": "https://dequeuniversity.com/rules/axe/4.13/link-name?application=axeAPI",
"examples": [".social > a:nth-child(1)", ".social > a:nth-child(2)"] }
],
"notChecked": []
},
"crawlDepth": 0
}
}

Duplicate titles and descriptions. metaTags.title.unique and metaTags.description.unique are settled as each page is written, against the pages of its site written before it: false when one of them already uses the same text (whitespace aside), with that page in duplicateOf and a Duplicate title tag or Duplicate meta description warning. A page cannot know about the pages audited after it, so the first page of a group keeps true: the site-summary lists every page of each group (duplicateTitles, duplicateDescriptions) and counts them all in topIssues. As in SEO crawlers, only indexable pages are compared: the field is null on a page answered with an error, set to noindex or canonicalized to another URL, and when the tag is missing.

site-summary record

{
"type": "site-summary",
"site": "example.com",
"startUrls": ["https://www.example.com"],
"pagesCrawled": 3,
"stoppedBy": null,
"averageScore": 85,
"categoryAverages": {
"metaTags": 92, "headings": 93, "content": 70, "technical": 78,
"schema": 57, "performance": 100, "links": 100, "images": 100, "accessibility": 88
},
"topIssues": [
{ "id": "DUPLICATE_TITLE", "issue": "Duplicate title tag (same title as another page of the site)", "severity": "warning", "count": 2, "pages": ["https://example.com/contact", "https://example.com/pricing"] },
{ "id": "LARGE_IMAGES", "issue": "Large images (over 200 KB)", "severity": "warning", "count": 2, "pages": ["https://example.com/", "https://example.com/pricing"] },
{ "id": "TITLE_TOO_LONG", "issue": "Title tag longer than 60 characters", "severity": "warning", "count": 1, "pages": ["https://example.com/"] },
{ "id": "HEADING_STRUCTURE_ISSUE", "issue": "Skipped heading levels", "severity": "warning", "count": 1, "pages": ["https://example.com/pricing"] }
],
"distribution": { "excellent": 2, "good": 1, "needsImprovement": 0, "poor": 0 },
"crawlCoverage": {
"pagesCrawled": 3,
"indexable": 3,
"nonIndexable": { "noindex": 0, "canonicalized": 0, "errors4xx": 0, "errors5xx": 0, "redirects": 0 },
"turnedAway": 0,
"maxDepth": 1
},
"duplicateTitles": [
{ "title": "Example — Plans, pricing and contact", "count": 2, "pages": ["https://example.com/pricing", "https://example.com/contact"] }
]
}

Top issues. topIssues counts each critical issue and warning once per page it hits, by its id: "3 large images found" on one page and "5 large images found" on another are one issue, Large images (over 200 KB), on 2 pages, and the page records keep each page's own figure. The issues that hit the most pages come first, then critical before warning, then the higher priority; 20 issues at most, info-level ones left out. A page that cannot be indexed is counted apart for each reason (Page not indexable: Page returns HTTP 404, Page not indexable: Page has noindex directive), since each needs its own fix.

Scoring

Each category is scored independently against SEO best practice. overallScore is the average of the page's category scores, links aside: a broken link lowers categoryScores.links and comes out as a BROKEN_LINKS warning, but does not move overallScore. A category the page could not be scored on (an audit turned off in the input, an audit that failed on the page, accessibility checks that could not run) is left out of the average rather than counted as 0. A page search engines leave out of their index scores 39 or less overall, whatever its categories: a page answered with an HTTP error (4xx, 5xx), whose content Google ignores, or a page carrying a noindex rule (a robots or googlebot meta tag, or the X-Robots-Tag header), which Google drops from its results. Nothing else such a page does reaches search results until that is fixed, and its NOT_INDEXABLE critical issue says why; its category scores still say how it will fare once it is. For the same reason, Lighthouse weighs its indexing check so that a page blocked from indexing fails its SEO category. A canonical pointing to an error page is reported but caps nothing, Google taking a canonical as a strong signal rather than a rule. On the site-summary record, averageScore is the average of its pages' overallScore, and categoryAverages the average of each category over the pages scored on it.

ScoreReading
80-100Excellent
60-79Good
40-59Needs improvement
0-39Poor

Every point a page loses comes with the issue that says why, in issues (info-level ones included), so its scores move when you fix things.

Cost

You are billed per page analyzed — see the Pricing section on this page for the current rate. maxPages is therefore your cost control: a 5-page preview costs a fraction of a 50-page deep audit, and a run never audits more than maxPages × the number of start URLs. Pages skipped by robots.txt or excludeUrlPatterns are not analyzed, and links to files (images, PDFs, documents, archives, media, feeds) are not followed at all. A link that fails to load, turns out to be a page already audited under another address, or leads to something other than an HTML page (an image or a feed served without a file extension) is not billed, and the next link of the site takes its place: each site gets its maxPages pages whenever it links to that many. Nor is a page the site turns away (an anti-bot challenge, a login, a rate limit), after which no further page of that site is requested (see Limits).

Every run also has a maximum cost: the one you set in the run options, or the limit Apify sets for the run when you set none. The audit never goes past it: a page is only audited when it can be billed within that maximum, and the run then ends with the site summaries of the pages audited (stoppedBy: "maxCharge") and a status message that says how many there are.

Limits

  • The page is read once its network has settled, 10 seconds at most after it loads: no request in flight for half a second. A request kept open for more than 4 seconds, such as a tracker the server never answers, does not hold the page. Content the page's scripts add after that may not be seen. A page that still has not finished loading 60 seconds after it was requested (a heavy page whose ads or videos keep loading) is audited as it stands at that point rather than dropped, and has no pageLoadTime or speedIndex.
  • Reading ease is an estimate. Syllables are counted from vowel groups, and a heading, a list item or a button label counts as a sentence of its own: a page made of short lines (a menu, a price list) scores as easy reading. Text that tabs, accordions or carousels hide with a stylesheet is counted, as search engines read it; text marked hidden in the markup (the hidden attribute, an inline display: none) is not, nor the labels of a form that is not displayed, nor a hidden mobile menu or cart drawer.
  • Performance metrics are lab measurements taken during the crawl, from one location and one connection. They indicate a trend, they are not field data from real users and will not match a Lighthouse or CrUX report exactly.
  • Core Web Vitals are measured up to the moment the page is audited, once it has settled (see above): an element painted or a layout shift happening later is not counted. Interaction to Next Paint and First Input Delay need a visitor's input and are not reported. speedIndex is an estimate from the first paint and the load time, not a frame-by-frame capture.
  • Pages a crawl loads at the same time share one CPU core, so their paint and blocking times run longer than they would on their own (a page that loads in 2.7 s alone can take 7 s next to another one). Each page says how many other pages were loading or being audited meanwhile (loadedWithOtherPages), and its LCP is only scored and flagged when that number is 0, as for the first page of a run with a single start URL. Set maxConcurrency to 1 to get it on every page, for a longer run.
  • Accessibility covers what automated checks can see, about a third of WCAG's success criteria: it is not a full WCAG audit, and a page that passes still needs a human review (keyboard use, whether alt texts make sense, captions). Text over an image or a gradient, whose contrast cannot be measured, is left out rather than guessed. The checks run in the page and take about a second on most pages audited alone, several times that on a heavy page sharing its CPU with the other pages of the crawl; those that do not finish within the time the page can give them, 25 seconds at most, are listed in notChecked with the reason and left out of the score, never counted as passed. On a crawl of heavy pages (large shops, news front pages), lowering maxConcurrency lets more of them finish.
  • An image marked decorative is taken at its word. Whether a picture with alt="" really carries no content (a product photo, a chart) cannot be told from the markup: such images are listed with "decorative": true for you to review, not reported as an issue. A tracking pixel a tag manager inserts as an <img> without alt is counted as an image without alt text.
  • Image weight is the size the server declares (Content-Length), except for inline data: images, whose payload is measured directly. An image that answers 4xx or 5xx is reported with its httpStatus and no sizeKB: it never arrived, so it has no weight of its own. An image whose server declares no size has no sizeKB either, and is never flagged as large. The file weighed is the one the browser loaded for the audit's desktop screen (servedUrl when a responsive image or a <picture> source replaces the src): a phone may download a lighter or heavier version. An image that has not started loading when the page is read (lazy loading far below the fold) is weighed from its src. When a server turns the audit's own check away but served the file to the browser, the weight is the size it declared to the browser.
  • A broken image is one a visitor gets an error for. An image is broken when the server answers 4xx or 5xx to the browser that loaded the page, or, for an image the browser has not loaded, to the audit's check, and only when nothing shows it reaches a visitor: an image whose file the browser or the check received, or that the browser shows from another source (a <picture> source or an inline image covering a missing src), is never broken; its httpStatus still says what the file named answered. Nor is a hidden tag (display: none, visibility: hidden) whose file the browser has not loaded, such as a template or a tracking probe that only carries a data-src; a hidden carousel slide whose image the browser loaded and got an error for is. A plain 403 counts when the same server served another image of the page; when it served none, like a rate limit or an anti-bot wall, it leaves the image unverified. So does no answer at all: an image whose server neither the check nor the browser could reach (a timeout, a host name that does not resolve, a refused connection) is listed as unverified with the failure ("No answer to the audit's check (ETIMEDOUT): neither weighed nor checked", followed by the browser's own error when it asked for the file too), never reported broken, since the network that failed may be ours.
  • Links are checked on a sample: the first 10 links of each page, internal ones first. A plain 403 counts as broken when the same server answered the audit's other links, since visitors get it too; when it refused all of them, it is an anti-bot wall and the links stay unverified. A page that answers 200 to say it does not exist (a "soft 404") is not detected. Each check waits 5 seconds at most for the server's answer: a server that does not answer in time, or never accepts the connection, leaves its link and its image unverified, with the reason.
  • AI crawler access is what robots.txt says. A firewall or CDN setting that turns AI crawlers away (Cloudflare's AI bot blocking, for instance) does not show in robots.txt and is not seen. Fetchers that open a page on a user's request (ChatGPT-User, Perplexity-User) are not checked, their owners saying robots.txt may not apply to them. Google's AI Overviews and AI Mode come from Googlebot: no AI crawler token affects them.
  • hreflang is read from the page's own <link> elements. Annotations declared in the XML sitemap or in an HTTP Link header are not read: a page that declares its versions there shows "present": false, with nothing reported. Whether each version links back (Google ignores two pages that do not point to each other) and whether the URLs named answer 200 are not checked either: the audit judges what the page itself says. A country code that is also a language code (se is Northern Sami, kr Kanuri) is a valid value and cannot be told from a mistake.
  • Rich results are checked for their required properties only, in JSON-LD. The audit checks the properties Google lists as required for each rich result, as its Search Central guides gave them on 18 October 2026: not the recommended ones, not the format of a value (a date, a price, a URL), not Google's content guidelines, and not items written in microdata or RDFa. A breadcrumb of a single crumb, a location given as text and an online event are not judged, and a reference by @id to a node the page does not carry counts as given. Google's Rich Results Test remains the reference. In JSON-LD, types are read from the items at the top of each block and of its @graph, not from the items nested in their properties (the Offer of a Product); in microdata and RDFa every typed element counts, nested or not. Only schema.org types count: RDFa in another vocabulary, such as the foaf types Drupal writes, is left out.
  • Sites behind an anti-bot or a login are out of scope, and their pages are not billed. A page the site turns away instead of serving it is neither audited nor billed: a login or a rate limit (HTTP 401, 403, 429), and the page an anti-bot protection serves in its place, whatever its status: a Cloudflare challenge ("Just a moment...", sometimes served with a 200), an error page of Akamai's edge (an Invalid URL 400), an AWS WAF challenge or CAPTCHA, a DataDome block, an Imperva challenge. Auditing it would score the protection's page, not the site's. The site summary counts these pages in crawlCoverage.turnedAway, the run's status message says how many and what answered (No page audited: the site turned the audit away (HTTP 400 from Akamai's edge), nothing was billed.), and no further page of that site is requested, since it would get the same answer. A protection is recognized by its own marks: one that serves an ordinary-looking page without them is not, and a real page that merely carries a protection's script (Cloudflare's bot detection, a Turnstile widget on a form) is audited as usual. To audit a site you run, let the audit through its protection for the time of the run.
  • A crawl that outlasts the run's timeout stops before it (1 hour by default, often a minute or two when an AI agent runs the actor): the pages audited are kept and billed, a page still being audited at that point is dropped and not billed, and the site summaries cover the pages audited (stoppedBy: "timeout"). The status message says how many pages were audited; raise the timeout for a deeper audit. A run cut short before its first page fails with the reason, and nothing is billed.
  • One crawl per site, limited to the site a start URL lands on unless includeSubdomains is on: a start URL that redirects (www.example.com to example.com, or to a country subdomain) is crawled on the site it redirects to. Start URLs that land on the same site share its maxPages budget, and each of them is audited even past it. Links are followed exactly as the site writes them, and a page reached through two URLs is audited and billed once. Only HTML pages are audited: the images, PDFs and other files a site links to are not.