SEO Metadata Auditor for Crawled Datasets avatar

SEO Metadata Auditor for Crawled Datasets

Pricing

from $1.50 / 1,000 audited pages

Go to Apify Store
SEO Metadata Auditor for Crawled Datasets

SEO Metadata Auditor for Crawled Datasets

Audit titles, meta descriptions, H1s, canonicals, thin content and URLs straight from a crawl dataset you already have. No re-crawl.

Pricing

from $1.50 / 1,000 audited pages

Rating

0.0

(0)

Developer

Logan Bishop

Logan Bishop

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

a day ago

Last modified

Share

Audit the crawl you already have. Find missing, duplicate and too-long titles and meta descriptions, H1 problems, canonical chains, thin content and messy URLs, with a site score and a reason for every point lost.

You ran Website Content Crawler (or any crawler) once. This Actor reads that dataset and runs an SEO metadata audit on it. It never fetches your site, never crawls and calls no external API, so there is nothing to block, nothing to wait for and no second crawl to pay for. Every issue says what was found, why it matters and how to fix it, and every score lists the exact points it lost.

What it checks

  • Titles: missing, empty, too short, too long (characters and an estimated pixel width), duplicated across pages, placeholder or domain-only titles, keyword stuffing, identical to the H1.
  • Meta descriptions: missing, empty, too short or long, duplicated, identical to the title, boilerplate.
  • Headings: no H1, several H1s, empty or very long H1, skipped levels (h1 to h3), the same H1 on several pages.
  • Canonicals: missing, invalid, pointing elsewhere, pointing at a page that is not a live indexable 200, chains and loops, another host, scheme mismatch, slash/case/www differences.
  • Indexability and status: noindex, nofollow, noindex combined with a canonical elsewhere, HTTP 4xx/5xx and redirects found in the crawl.
  • Content: thin pages and pages with identical text.
  • URLs: long URLs, uppercase, underscores, tracking parameters, http pages on an https site, URL variants of the same page, pages too deep.
  • Social, language, images: Open Graph and Twitter card tags, lang attribute, images without alt text.

The Actor looks across the whole dataset, so it finds problems a one-page checker cannot: duplicate titles, descriptions, H1s and content, URL variants and canonical chains.

It only checks what your dataset can prove

If the dataset has no heading data, the H1 checks are skipped and listed, not flagged on every page. The same goes for status codes, Open Graph tags, images and the rest. The run summary lists every skipped check and why, and each audited page repeats it in skippedChecks. That keeps the results trustworthy on crawler output that lacks some fields.

How to use it

  1. Run your crawler. With Website Content Crawler, switch on Save Markdown so headings (H1 checks) and word counts are available. Copy the dataset ID of the run.
  2. Run this Actor. Set Crawl dataset (or paste a few Pages), pick a Strictness profile and click Start.
  3. Read the results. Open the dataset for one record per page, the Report for a client-ready summary, or download the Issue list CSV for a spreadsheet.

Chain it after Website Content Crawler

JavaScript (apify-client):

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
// 1. Crawl the site (replace the input with your own; Save Markdown gives the H1 checks their data)
const crawl = await client.actor('apify/website-content-crawler').call({
startUrls: [{ url: 'https://example.com' }],
maxCrawlPages: 200,
saveMarkdown: true,
});
// 2. Audit the crawl without crawling again
const audit = await client.actor('hiredhandai/seo-metadata-auditor').call({
datasetId: crawl.defaultDatasetId,
profile: 'standard',
});
// 3. Read the results
const { items } = await client.dataset(audit.defaultDatasetId).listItems();
console.log(items.filter((page) => page.score < 70).map((page) => page.url));
const summary = await client.keyValueStore(audit.defaultKeyValueStoreId).getRecord('OUTPUT');
console.log(summary.value.siteScore, summary.value.topIssues.slice(0, 3));

Python (apify-client):

import os
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
audit = client.actor("hiredhandai/seo-metadata-auditor").call(
run_input={"datasetId": "YOUR_CRAWL_DATASET_ID", "profile": "standard"}
)
store = client.key_value_store(audit["defaultKeyValueStoreId"])
print(store.get_record("OUTPUT")["value"]["siteScore"])
for page in client.dataset(audit["defaultDatasetId"]).iterate_items():
if page["score"] < 70:
print(page["url"], page["scoreReasons"])

It also works as a step in Make, n8n, Zapier and Google Sheets flows: crawl, then pass the crawl's defaultDatasetId to this Actor with your tool's Apify integration, and post the top issues to Slack, email or a sheet.

Other crawlers

Field names are detected automatically. The common shapes it understands, first match wins:

FactLooked up in
URLurl, crawl.loadedUrl, loadedUrl, finalUrl, pageUrl, link, address
Titlemetadata.title, title, pageTitle, meta.title
Descriptionmetadata.description, description, metaDescription, meta.description
Canonicalmetadata.canonicalUrl, canonicalUrl, canonical, canonical_url
HTTP statuscrawl.httpStatusCode, httpStatusCode, statusCode, status, response.status
Languagemetadata.languageCode, lang, language, htmlLang
Robotsmetadata.robots, robots, metaRobots, noindex, x-robots-tag header
Headingsheadings, else markdown (ATX # lines), else html, else h1 / h1s
Page texttext, markdown, body, or wordCount

Anything else: use Field mapping, for example {"title": "meta.pageTitle", "url": "link"}.

For Cheerio Scraper or Web Scraper, return the fields this Actor reads from your pageFunction:

async function pageFunction(context) {
const { $, request } = context;
return {
url: request.loadedUrl,
title: $('title').first().text(),
description: $('meta[name="description"]').attr('content') || null,
canonical: $('link[rel="canonical"]').attr('href') || null,
h1: $('h1').map((i, el) => $(el).text()).get(),
lang: $('html').attr('lang') || null,
robots: $('meta[name="robots"]').attr('content') || null,
wordCount: $('body').text().split(/\s+/).filter(Boolean).length,
};
}

Input

FieldTypeDefaultDescription
datasetIdstringThe crawl dataset to audit. Pick it in the Console, or pass its ID through the API. Use this or pages.
pagesarrayUp to 5,000 inline pages, to try the Actor or for small API jobs.
fieldMapobjectauto-detectMap an audit field to your field name or dot path, e.g. {"title": "meta.pageTitle"}. Keys: url, title, description, canonical, statusCode, h1, headings, lang, robots, text, markdown, html, openGraph, twitter, images, depth, wordCount.
siteUrlstringmost common hostMain site address, used to spot canonicals that point to another host.
profilestandard / strict / lenientstandardThreshold set for lengths, thin content, URL length and depth (table below).
rulesDisabledarray[]Rule codes to ignore, e.g. ["O005", "U006"]. Disabled rules do not affect the score.
crawlIsCompletebooleanfalseTurn on only if the dataset holds every page of the site. Then a canonical pointing to a page missing from the dataset (C008) is reported.
onlyStatusCodesstring200Statuses that get the full metadata checks, e.g. 200,2xx,301. Other pages only get a status issue. Pages without a status count as 200.
includeUrlPatternsarray[]Regular expressions; audit only URLs matching one of them.
excludeUrlPatternsarray[]Regular expressions; skip URLs matching any of them. Skipped pages are not audited and not charged.
analyzeContentbooleantrueWord counts, thin content and identical-text detection. Turn off for a lighter run.
outputIssueFilebooleantrueAlso write every issue to ISSUES_CSV.
maxPagesinteger100000Upper limit on pages read and audited (and charged).

URL patterns are limited to 50 per list and 200 characters each, nested quantifiers such as (a+)+ are rejected, and patterns are matched against the first 2,048 characters of the URL.

Profiles

SettingStandardStrictLenient
Title length (characters)30 to 6040 to 5520 to 70
Title width (estimated pixels)580540620
Description length70 to 160100 to 15550 to 175
Thin content (warning / error, words)under 200 / under 50under 300 / under 100under 150 / under 30
URL length115100150
Query parameters324
Crawl depth436
OtherO005 and T008 become warningsT008 off; H002, H006, U009 become info

A title counts as too long when its estimated width is over the pixel limit or it is more than 10 characters over the character limit. Pixel widths are estimates for an Arial-like 20 px font, not a measurement of what any search engine shows.

Output

One item per audited page in the default dataset. Real output from the prefilled example input (flat field names, no Open Graph or status data, so those checks are listed as skipped):

{
"url": "https://example.com/products/widget",
"normalizedUrl": "https://example.com/products/widget",
"statusCode": null,
"score": 85,
"grade": "B",
"indexable": true,
"title": "Handmade Widget for Home and Office Use",
"titleLength": 39,
"titlePxEstimate": 381,
"description": null,
"descriptionLength": 0,
"canonical": "https://example.com/products/widget",
"h1": ["Handmade Widget"],
"wordCount": 310,
"issueCounts": { "error": 1, "warning": 0, "info": 0 },
"issues": [
{
"code": "D001",
"category": "description",
"severity": "error",
"message": "Meta description is missing.",
"evidence": {},
"fix": "Add a unique 70-160 character meta description that summarizes the page.",
"penalty": 15
}
],
"scoreReasons": ["-15 D001 meta description missing"],
"skippedChecks": [
"H004, H007: only H1 text is available, not the heading outline",
"I001-I003: no robots (noindex/nofollow) data in the dataset",
"M001: no image data in the dataset",
"O001-O005: no Open Graph data in the dataset",
"S001, S002: no status code field in the dataset",
"U009: no crawl depth in the dataset"
],
"sourceIndex": 1
}

Each run also saves three records in the key-value store:

  • OUTPUT: the site summary as JSON: siteScore and siteGrade, issue totals, topIssues, categoryScores, duplicateGroups (title, description, H1, content, URL variants), worstPages, fieldCoverage, rulesSkipped, and the run's recordsPushed and notPushedDueToSpendingLimit.
  • REPORT_MD: a client-ready Markdown report built from the same data. Text that comes from your pages is escaped.
  • ISSUES_CSV: url, code, severity, category, message, fix, one row per issue, safe to open in a spreadsheet (cells that start with =, +, - or @ are neutralized).

How scoring works

Every page starts at 100. Each issue subtracts 15 (error), 6 (warning) or 1 (info), and each category has a cap so one problem area cannot sink a page by itself: title 35, description 30, headings 25, canonical 30, indexability 40, content 20, URL 15, social/language/images 10, status 60. The score never goes below 0, grades are A (90+), B (80+), C (65+), D (50+), F, and scoreReasons lists every deduction, including the cap lines, so the numbers always add up.

The site score is the mean page score rounded, minus a duplicate-title penalty: 2 points for every 5% of pages in duplicate-title groups beyond the first 5%, at most 10. Only indexable pages that are not canonicalized elsewhere and have a checked status take part in duplicate groups, so noindex, canonicalized and error pages do not cause false duplicates. Pages marked noindex get title, description and heading findings reduced to info, because those are not shown in search results.

Rules version 2026.10.1. The thresholds are guidance, not search engine rules.

All rules

CodeSeverityWhat it flagsNeeds in the dataset
S001errorHTTP error statusHTTP status
S002warningRedirecting URL in crawlHTTP status
I001infoNoindex pagerobots / noindex
I002warningNoindex with canonical elsewhererobots / noindex
I003infoNofollow pagerobots / noindex
T001errorTitle missingtitle
T002errorTitle emptytitle
T003warningTitle shorttitle
T004warningTitle longtitle
T005warningDuplicate titletitle
T006warningGeneric titletitle
T007warningKeyword stuffing in titletitle
T008infoTitle equals H1headings or H1
T009infoDuplicate title apart from brand suffixtitle
D001errorMeta description missingmeta description
D002errorMeta description emptymeta description
D003warningMeta description shortmeta description
D004warningMeta description longmeta description
D005warningDuplicate meta descriptionmeta description
D006warningDescription equals titlemeta description
D007infoBoilerplate descriptionmeta description
H001errorNo H1headings or H1
H002warningMultiple H1headings or H1
H003errorEmpty H1headings or H1
H004warningHeading level skippedheadings or H1
H005infoH1 shared with other pagesheadings or H1
H006warningH1 too longheadings or H1
H007infoFirst heading is not an H1headings or H1
C001warningCanonical missingcanonical
C002errorCanonical invalidcanonical
C003infoCanonical points elsewherecanonical
C004errorCanonical target not indexablecanonical
C005errorCanonical chain or loopcanonical
C006warningCross-host canonicalcanonical
C007warningCanonical scheme mismatchcanonical
C008infoCanonical target not in crawlcanonical
C009warningCanonical differs by slash, case or wwwcanonical
W001warningThin contentpage text
W002errorVery thin contentpage text
W003warningDuplicate contentpage text
U001warningURL too longURL
U002warningUppercase in URL pathURL
U003infoUnderscores in URL pathURL
U004warningQuery parametersURL
U005warningUnsafe characters in URLURL
U006infoInconsistent trailing slashURL
U007warningPage served over httpURL
U008warningURL variants of the same pageURL
U009warningPage too deepcrawl depth
U010infoUnparseable URLURL
O001warningOg:title missingOpen Graph tags
O002infoOg:description missingOpen Graph tags
O003warningOg:image missingOpen Graph tags
O004infoOg:url differs from canonicalOpen Graph tags
O005infoTwitter:card missingOpen Graph tags
L001warningLang missinglanguage
L002warningLang malformedlanguage
L003infoLang differs from site languagelanguage
M001warningImages without alt textimages

Pricing

Pay per event, no subscription:

  • $1.50 per 1,000 pages ($0.0015 per audited page) (page-audited). You are charged for pages returned in the dataset. Pages excluded by URL patterns and pages beyond your maxPages are free.
  • A tiny start fee charged by Apify on every run (about $0.00005).
JobPagesCost
A small site200about $0.30
A mid-size site1,200about $1.80
A large crawl50,000about $75

You can set a maximum cost per run in the run options. The Actor then stops cleanly, tells you how many audited pages were not returned, and charges only for the pages it returned. The site summary and Markdown report still describe every page that was audited. See Apify pricing for plan details.

Limits

  • Up to 100,000 pages per run (5,000 inline). Pages whose extracted data is larger than 20,000 characters are skipped and counted in the summary as pagesSkippedOversized.
  • The audit itself is fast: auditing 2,000 pages took about a quarter of a second of compute in local runs on a laptop; reading and writing the dataset takes longer than the audit.
  • No fetching: the Actor cannot see broken links, redirect targets, page speed, backlinks or rankings. It reports only what your dataset contains.
  • Heuristics: lengths, thresholds and the estimated pixel width are rules of thumb. Use profile and rulesDisabled to fit them to your site.

FAQ

Does it crawl or fetch my site? No. It reads a dataset and never makes a request to your website, or to any other service.

Does it call any API or store my data? No. Your pages are processed during the run and written to your own run storage. Nothing is sent anywhere and nothing is kept elsewhere.

Why was a check skipped? The dataset did not contain the data the check needs, for example headings, HTTP status or Open Graph tags. The summary lists the skipped rules and the reason. Turn on Save Markdown in Website Content Crawler to get H1 and word-count checks.

Why are there no Core Web Vitals, backlinks or keyword rankings? Those need live data and third-party sources. This Actor stays within what a crawl dataset can prove.

Can I switch a rule off? Yes, list its code in rulesDisabled. It stops being checked and stops affecting the score.

Why does a noindex page still get issues? Only the title, description and heading findings are reduced to info. Status, canonical, content and URL findings still apply.

Can I audit a CSV? Upload it as a dataset in Apify, or convert it to JSON and paste it into pages.

Support

Questions, bugs or missing checks: use the Issues tab on this Actor. I answer within 48 hours. If this saved you time, a rating on the Actor page helps others find it.