Most on-page SEO problems are boring and mechanical: a template that drops the meta description, a canonical pointing at the wrong URL, a noindex that survived a staging deploy, product images without alt text. Finding them across hundreds of pages by hand is the expensive part. This guide shows how to audit any list of URLs and get one row per page with the facts, a 0–100 score, an A–F grade and a list of issues in plain English — for $2 per 1,000 results.
What you get
Every audited page returns the raw on-page facts and a verdict:
| Area | Fields |
|---|---|
| Title & description | title, titleLength, metaDescription, metaDescriptionLength |
| Indexing | canonical, canonicalMatches, robotsMeta, isIndexable, statusCode, finalUrl |
| Structure | h1Count, h1, h2Count, h3Count, headingsOutline, wordCount, textToHtmlRatio |
| Images | imageCount, imagesMissingAlt, imagesMissingAltSample |
| Links | internalLinkCount, externalLinkCount, nofollowLinkCount, brokenLinks |
| Social & structured data | openGraphComplete, twitterCardPresent, schemaTypes |
| Verdict | issues, seoScore, seoGrade |
A real row from a recorded run:
{
"url": "https://apify.com/",
"finalUrl": "https://apify.com/",
"statusCode": 200,
"responseTimeMs": 134,
"htmlSizeBytes": 492244,
"title": "Apify: Marketplace of ready-to-run tools for AI",
"titleLength": 47,
"metaDescription": "Thousands of tools to automate your business. Get real-time web data, track competitors, generate leads, and integrate your apps and AI agents.",
"metaDescriptionLength": 143,
"canonical": "https://apify.com/",
"canonicalMatches": true,
"robotsMeta": "index,follow",
"isIndexable": true,
"lang": "en",
"viewportMeta": "width=device-width, initial-scale=1",
"charset": "utf-8",
"h1Count": 1,
"h1": [
"80,266 tools for your AI"
],
"h2Count": 11,
"h3Count": 20,
"headingsOutline": [
{
"level": 1,
"text": "80,266 tools for your AI"
},
{
"level": 2,
"text": "Popular Actors"
},
{
"level": 3,
"text": "TikTok Scraper"
}
],
"wordCount": 1174,
"textToHtmlRatio": 0.02,
"imageCount": 78,
"imagesMissingAlt": 0,
"imagesMissingAltSample": [],
"internalLinkCount": 78,
"externalLinkCount": 58,
"nofollowLinkCount": 20,
"brokenLinks": [],
"hreflang": [],
"openGraphComplete": true,
"twitterCardPresent": true,
"schemaTypes": [
"Organization",
"WebSite",
"SoftwareApplication"
],
"hasFavicon": true,
"inlineScriptCount": 5,
"externalScriptCount": 46,
"cssCount": 4,
"hasAmp": false,
"mixedContentCount": 0,
"issues": [],
"seoScore": 100,
"seoGrade": "A",
"error": null,
"scrapedAt": "2026-10-03T15:21:13.215Z"
}How the score works
Every page starts at 100 and loses points per issue: missing or too-long title (15 / 5), missing or too-long meta description (10 / 5), no H1 or several H1s (10 each), images missing alt text (5), noindex (20), canonical mismatch (10), missing viewport (5), missing lang attribute (5), thin content under 300 words (15) and mixed HTTP content on an HTTPS page (10). The score is floored at 0 and graded A ≥ 90, B ≥ 75, C ≥ 60, D ≥ 40, otherwise F. Because every deduction also appears as a sentence in issues, the score is never a black box — you can see exactly why a page lost points.
Step-by-step
- Open the tool and paste page URLs into URLs — one row comes back per URL. Bare domains get
https://added. - Optional: turn on Check internal links for broken ones to HEAD-check up to 50 internal links per page and list the broken ones in
brokenLinks. It adds requests, so leave it off for quick passes. - Click Start. A page typically audits in under a second, so a few hundred pages finish in minutes.
- Sort the dataset by
seoScore, or filterisIndexable = false, and export to CSV or Excel.
Run On-Page SEO Audit on Apify →
Call it from code
import json
import os
from apify_client import ApifyClientclient = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("webdatatools/seo-page-audit").call(run_input=json.loads("{\n "urls": [\n "https://apify.com\",\n "https://example.com\"\n ],\n "checkLinks": false,\n "maxConcurrency": 5\n}"))
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
print(item)
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('webdatatools/seo-page-audit').call({
"urls": [
"https://apify.com",
"https://example.com"
],
"checkLinks": false,
"maxConcurrency": 5
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);
curl -X POST "https://api.apify.com/v2/acts/webdatatools~seo-page-audit/run-sync-get-dataset-items"
-H "Authorization: Bearer $APIFY_TOKEN"
-H "Content-Type: application/json"
-d '{"urls":["https://apify.com","https://example.com"],"checkLinks":false,"maxConcurrency":5}'Recipes
Audit a whole site from its sitemap
Run the Sitemap Extractor on the domain, then start this audit with inputDatasetId set to that run's dataset (and inputField to the URL column). Every URL in the sitemap gets a scored row without you copying a single link.
Catch template regressions after a deploy
Pick one URL per template and schedule a daily run. A drop in seoScore on the product template, or a new "Missing meta description" issue, means the last deploy broke something — you'll know before Search Console does.
Content QA before publishing
Writers paste draft preview URLs (if they're publicly reachable) and fix anything flagged in issues — thin content, missing H1, images without alt text — before the page goes live.
How much does it cost?
| Apify plan | Per result | Per 1,000 |
|---|---|---|
| Free plan | $0.002 | $2 |
| Starter | $0.0016 | $1.60 |
| Scale | $0.0014 | $1.40 |
| Business | $0.0012 | $1.20 |
| Enterprise | $0.0012 | $1.20 |
| Enterprise+ | $0.0012 | $1.20 |
Primary event rates from a catalog snapshot; this is not the total price of a run. Additional charge events and Actor Start fees may apply; start fees can scale with memory. Check the Actor's Pricing tab on Apify for all events, conditions and current plan-specific rates, and set a maximum run budget before starting.
Pages that fail to load are not charged. Auditing 1,000 pages is 1,000 results, with no subscription and no software to install.
How it compares
| Option | Good at | Watch out for |
|---|---|---|
| This tool | Any URL list, from any source; API, schedules and MCP; scored rows with explicit issues; nothing to install | Reads raw HTML, so content injected by JavaScript after load isn't seen; audits the pages you give it rather than discovering a whole site |
| Desktop SEO crawlers | Full-site discovery, deep reports, JavaScript rendering options | Installed software and licences; harder to automate in a pipeline |
| SEO suite site audits | Dashboards, trends, many extra checks | Monthly subscription, project limits |
| Browser extensions | Instant check of the page you're on | One page at a time, nothing to export |
FAQ
Does it render JavaScript?
No. It fetches the raw HTML, which is what many crawlers see on their first pass. If your titles or content are injected client-side, they won't appear in the audit — which is itself a useful warning.
Why doesn't alt="" count as missing alt text?
An empty alt="" is the correct, deliberate way to mark decorative images for screen readers. Only a missing alt attribute is counted as an issue.
Can it crawl my whole site by itself?
It audits the URLs you give it. To cover a whole site, feed it a sitemap or a crawl result through inputDatasetId, as in the first recipe.
How does broken-link checking work?
With Check internal links on, it HEAD-checks up to 50 internal links per page and lists the ones that fail in brokenLinks. External links are counted but not checked.
Can an AI assistant use it?
Yes. It's callable through the Apify MCP server, so you can ask an assistant to "audit these ten blog posts and tell me which need a meta description" and it runs the audit itself.