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

TagWhy it mattersRule of thumb
<title>The blue link in search resultsOne per page, 30–60 characters, unique
meta descriptionOften used as the snippetOne per page, 70–160 characters
link rel=canonicalWhich URL should be indexedPoints to the page itself unless it is a duplicate
meta robots, X-Robots-Tagnoindex removes the page from searchOnly on pages you do not want found
og:*, twitter:*Title, text and image of social sharesog:title, og:description, an absolute og:image, twitter:card
hreflangLanguage versionsValid codes, and each set includes the page itself
JSON-LDRich 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 WebSite block has no name, which Google requires).
  • No robots.txt, no redirect chain, no X-Robots-Tag header, 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

  1. Open SEO Meta Tags, Open Graph & Schema.org Extractor on Apify.
  2. 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.
  3. 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 pagesPages
META_DESCRIPTION_TOO_SHORT12
CANONICAL_MISSING12
TWITTER_CARD_MISSING12
SCHEMA_MISSING_REQUIRED_PROPERTIES12
MULTIPLE_H18
TITLE_TOO_SHORT7

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

FieldWhat it holds
url, finalUrl, status, httpStatus, redirectChainRequest and outcome
indexable, issues, issueCountHTTP 200, no noindex, canonical self or missing; 26 issue codes
title, titleLength, metaDescription, metaDescriptionLengthText and character counts
h1, h1Count, h2CountHeadings
canonicalUrl, canonicalIsSelf, metaRobots, xRobotsTag, hreflangIndexing signals
openGraph, twitterCardAll og:* and twitter:* tags, image URLs made absolute
schemaTypes, microdataTypes, rdfaTypes, schemaIssuesStructured data and missing properties for 18 rich-result types
wordCount, imagesMissingAlt, internalLinksCount, externalLinksCountContent and links
depth, foundOnIn 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 as disallowed_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).

FieldTypeDefaultWhat it does
Page URLs required
urls
arrayPages to analyse, one per line. 'example.com/page' works too. Up to 10,000 per run.
Pages per site (crawl)
maxPagesPerSite
integer11 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 total
maxPages
integer1000Stops the run after this many pages (start URLs plus crawled pages).
Include full JSON-LD
includeJsonLd
booleanfalseAdds 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 requests
maxConcurrency
integer10Pages fetched at the same time. At most 2 run against the same host.
Request timeout (seconds)
requestTimeoutSecs
integer30How long to wait for each page.
Retries
maxRetries
integer2Retries 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.