Check meta tags, Open Graph and schema.org on many pages at once
Browser extensions and online checkers show one page's meta tags. An SEO audit, a migration or a pre-launch check needs the same answers for every page in a spreadsheet: which titles are too long, which pages have no description, a canonical pointing elsewhere, a stray noindex, no og:image, or Product markup without offers. This guide shows a short Python script, tested live, and SEO Meta Tags, Open Graph & Schema.org Extractor, an Apify actor that checks thousands of URLs or crawls a site and gives one row per page with an issue list.
Run it on Apify Opens the actor's page on apify.com. A free Apify account is needed to run it.
Only need one page right now? Try the free meta tag checker: it runs on this site with no sign-up.
What to check on every page
| Tag | Why it matters | Rule of thumb |
|---|---|---|
<title> | The blue link in search results | One per page, 30–60 characters, unique |
meta description | Often used as the snippet | One per page, 70–160 characters |
link rel=canonical | Which URL should be indexed | Points to the page itself unless it is a duplicate |
meta robots, X-Robots-Tag | noindex removes the page from search | Only on pages you do not want found |
og:*, twitter:* | Title, text and image of social shares | og:title, og:description, an absolute og:image, twitter:card |
hreflang | Language versions | Valid codes, and each set includes the page itself |
| JSON-LD | Rich results (products, articles, FAQs, breadcrumbs) | Valid JSON with the required properties for its type |
For a single page, our free meta tag checker runs these checks with no sign-up.
A Python script for a quick audit
Standard library only. It prints the main tags and the JSON-LD types of one URL as JSON; loop over a list to build a CSV:
import json, sys, urllib.request
from html.parser import HTMLParser
class Meta(HTMLParser):
def __init__(self):
super().__init__()
self.tags, self.title, self.h1, self.jsonld, self._in = {}, "", 0, [], None
def handle_starttag(self, tag, attrs):
a = dict(attrs)
if tag == "meta" and (a.get("name") or a.get("property")):
self.tags[(a.get("name") or a.get("property")).lower()] = a.get("content", "")
elif tag == "link" and a.get("rel") == "canonical":
self.tags["canonical"] = a.get("href")
elif tag == "h1":
self.h1 += 1
if tag == "title" or (tag == "script" and a.get("type") == "application/ld+json"):
self._in = tag
def handle_endtag(self, tag):
if tag == self._in: self._in = None
def handle_data(self, data):
if self._in == "title": self.title += data
elif self._in == "script": self.jsonld.append(data)
req = urllib.request.Request(sys.argv[1], headers={"User-Agent": "meta-check/1.0"})
p = Meta()
p.feed(urllib.request.urlopen(req, timeout=20).read().decode("utf-8", "replace"))
types = []
for block in p.jsonld:
try:
data = json.loads(block)
types += [i.get("@type") for i in (data.get("@graph", [data]) if isinstance(data, dict) else data)]
except ValueError:
types.append("INVALID JSON-LD")
print(json.dumps({"title": p.title.strip(), "titleLength": len(p.title.strip()),
"description": p.tags.get("description"), "canonical": p.tags.get("canonical"),
"robots": p.tags.get("robots"), "h1Count": p.h1, "ogImage": p.tags.get("og:image"),
"twitterCard": p.tags.get("twitter:card"), "jsonLdTypes": types}, indent=2))
On python.org (2026-10-12) it reported a 21-character title, a 52-character description, 5 H1 headings, no canonical, no Twitter card and one WebSite JSON-LD block. Where a script like this falls short:
- It does not check required properties (that
WebSiteblock has noname, which Google requires). - No robots.txt, no redirect chain, no
X-Robots-Tagheader, no hreflang validation. - One timeout, 429 or bot challenge stops the loop, and it cannot crawl a site to find the pages.
Check many pages or a whole site without code
- Open SEO Meta Tags, Open Graph & Schema.org Extractor on Apify.
- Paste up to 10,000 URLs, or enter a home page and set Pages per site (for example 500) to crawl internal links breadth-first.
- Click Start and export the Overview, Social tags or Structured data view as CSV or Excel.
Each page gets lengths, counts, an indexable flag and issue codes instead of raw HTML. The python.org row from a real run on 2026-10-12, shortened:
{
"url": "https://www.python.org/",
"status": "ok",
"indexable": true,
"issueCount": 6,
"issues": ["TITLE_TOO_SHORT", "META_DESCRIPTION_TOO_SHORT", "MULTIPLE_H1", "CANONICAL_MISSING",
"TWITTER_CARD_MISSING", "SCHEMA_MISSING_REQUIRED_PROPERTIES"],
"title": "Welcome to Python.org",
"titleLength": 21,
"metaDescriptionLength": 52,
"h1Count": 5,
"openGraph": {"type": "website", "site_name": "Python.org", "image": "https://www.python.org/static/opengraph-icon-200x200.png"},
"schemaTypes": ["WebSite"],
"schemaIssues": ["WebSite: missing required name"],
"wordCount": 1012
}Worked example: a 12-page crawl with failures
On 2026-10-12 we crawled python.org to 12 pages and added five problem URLs and two invalid inputs. In 15 seconds:
| Issue across the 12 python.org pages | Pages |
|---|---|
META_DESCRIPTION_TOO_SHORT | 12 |
CANONICAL_MISSING | 12 |
TWITTER_CARD_MISSING | 12 |
SCHEMA_MISSING_REQUIRED_PROPERTIES | 12 |
MULTIPLE_H1 | 8 |
TITLE_TOO_SHORT | 7 |
The failures came back as free rows with a reason: facebook.com disallowed_by_robots_txt, g2.com blocked_by_site (DataDome, HTTP 403), an apify.com 404 as http_error, a PDF as not_html, and a non-existent domain. A site-wide pattern like "every page lacks a canonical" is usually one template fix.
What each row contains
| Field | What it holds |
|---|---|
url, finalUrl, status, httpStatus, redirectChain | Request and outcome |
indexable, issues, issueCount | HTTP 200, no noindex, canonical self or missing; 26 issue codes |
title, titleLength, metaDescription, metaDescriptionLength | Text and character counts |
h1, h1Count, h2Count | Headings |
canonicalUrl, canonicalIsSelf, metaRobots, xRobotsTag, hreflang | Indexing signals |
openGraph, twitterCard | All og:* and twitter:* tags, image URLs made absolute |
schemaTypes, microdataTypes, rdfaTypes, schemaIssues | Structured data and missing properties for 18 rich-result types |
wordCount, imagesMissingAlt, internalLinksCount, externalLinksCount | Content and links |
depth, foundOn | In crawl mode: depth and the page that linked to it |
Honest limits
- HTML as served. Titles or JSON-LD injected by JavaScript are not seen.
- Not a full validator. Schema checks cover the main properties of common Google rich-result types; Google's Rich Results Test is the final word on one page.
- No bypassing. Bot-protected sites come back as
blocked_by_site, robots.txt-disallowed pages asdisallowed_by_robots_txt. - Rules of thumb, not penalties. A 65-character title is not an error to Google; the thresholds flag pages worth a look.
All input fields
Generated from the actor's live input schema (build 1.0.3).
| Field | Type | Default | What it does |
|---|---|---|---|
Page URLs requiredurls | array | Pages to analyse, one per line. 'example.com/page' works too. Up to 10,000 per run. | |
Pages per site (crawl)maxPagesPerSite | integer | 1 | 1 analyses only the URLs you entered. A higher number also follows each start page's internal links (same site, robots.txt respected, nofollow links skipped) until that many pages per start URL are analysed. |
Maximum pages in totalmaxPages | integer | 1000 | Stops the run after this many pages (start URLs plus crawled pages). |
Include full JSON-LDincludeJsonLd | boolean | false | Adds each page's parsed JSON-LD objects (up to 50) to the row. Off: only the schema types and the missing-property checks. |
| Advanced | |||
Parallel requestsmaxConcurrency | integer | 10 | Pages fetched at the same time. At most 2 run against the same host. |
Request timeout (seconds)requestTimeoutSecs | integer | 30 | How long to wait for each page. |
RetriesmaxRetries | integer | 2 | Retries for network errors and HTTP 429/5xx answers, with backoff. Retry-After is honoured up to 30 s. |
Call it from code
Start a run and get the results in one HTTP request with the Apify API (your token is under Console, Settings, API & Integrations):
curl -X POST "https://api.apify.com/v2/acts/adamhaleworks~seo-meta-tags-schema-extractor/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"urls":["https://apify.com/","https://www.python.org/"]}'The run's Output tab offers 3 ready-made views; every view exports to CSV, Excel, JSON or XML:
- Overview:
url,status,httpStatus,indexable,issueCount,issues,title,titleLength,metaDescription,metaDescriptionLength,h1Count,canonicalUrl,schemaTypes,error - Social tags:
url,openGraph.title,openGraph.description,openGraph.image,openGraph.type,twitterCard.card,twitterCard.title,twitterCard.image - Structured data:
url,schemaTypes,microdataTypes,rdfaTypes,schemaIssues,jsonLdBlocksInvalid,hreflang
What it costs
Free to run right now. The actor charges no fee of its own today. Apify bills only the compute a run uses against your account's platform credit, and the free Apify plan includes $5 of usage every month. For scale: building all five of our actors and test-running each once on its default input used $0.03 of credit in total (2026-10-12).
Planned: pay-per-event pricing of $0.002 per page analysed ($2 per 1,000) once the account's payout setup is finished. This page and the Apify page will show the change, and Apify notifies existing users before a price change takes effect.
At the planned price, the 12-page crawl above would cost $0.024: the seven failed or invalid inputs would be free. Only HTML pages answering HTTP 2xx would be charged.
FAQ
How can I check meta tags for multiple URLs at once?
Paste the URLs into the SEO Meta Tags & Schema.org Extractor on Apify (up to 10,000 per run) and export the Overview view to a spreadsheet. For a handful, the Python script on this page works; for one page, use the free meta tag checker.
Can it crawl my whole site?
Yes. Enter the home page and set Pages per site. Internal links are followed breadth-first; nofollow links and files such as PDFs are skipped.
Does it validate structured data?
It parses every JSON-LD block (including @graph), counts invalid ones, lists microdata and RDFa types and checks 18 common rich-result types for required and recommended properties. It is not a replacement for Google's Rich Results Test.
Why is a page marked not indexable?
It did not answer HTTP 200, has noindex in its robots meta tag or X-Robots-Tag header, or its canonical points to another URL.
How do I keep it off my site?
It identifies itself as seo-meta-tags-schema-extractor and follows robots.txt; a group for that name with Disallow: / keeps it out.
Try it on your own data
Bulk on-page SEO check: title, meta description, canonical, robots, hreflang, Open Graph, Twitter card, JSON-LD and microdata types with missing-property checks, headings, alt text and an issues list per page. Paste URLs or crawl a site. Free while in launch; planned price $2 per 1,000 pages.
Open SEO Meta Tags, Open Graph & Schema.org Extractor Opens the actor's page on apify.com. A free Apify account is needed to run it.