# SEO Meta Tags, Open Graph & Schema.org Extractor (`adamhaleworks/seo-meta-tags-schema-extractor`) Actor

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.

- **URL**: https://apify.com/adamhaleworks/seo-meta-tags-schema-extractor.md
- **Developed by:** [Adam Hale](https://apify.com/adamhaleworks) (community)
- **Categories:** SEO tools, Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-usage

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## SEO Meta Tags, Open Graph & Schema.org Extractor: bulk on-page SEO check

Paste page URLs (or one start page per site and let it crawl) and get **one row per page** with everything that decides how the page shows up in Google and on social media: title, meta description, canonical, robots directives, hreflang, Open Graph, Twitter card, JSON-LD / microdata / RDFa types with **missing-property checks**, headings, image alt text, word count and a list of **SEO issue codes**.

- **One flat row per page**, ready for a spreadsheet: lengths, counts and issue codes instead of raw HTML.
- **Structured data checks**: JSON-LD (including `@graph`) is parsed, broken blocks are counted, and common types (Product, Offer, Article, Organization, LocalBusiness, BreadcrumbList, FAQPage, Event, Recipe, JobPosting, VideoObject, Review, SoftwareApplication, WebSite…) are checked for the properties Google requires or recommends.
- **Paste URLs or crawl**: set **Pages per site** above 1 to follow internal links from each start page.
- **Free while in launch; planned price $2 per 1,000 pages** (pay per event). Pages that fail, are blocked or are not HTML would not be charged.
- **Fast and polite**: no browser, no proxy, honest User-Agent, robots.txt respected. The default 3-page input takes about 2 seconds.

### What can I use it for?

- **SEO audits**: find missing or too-long titles and descriptions, duplicate H1s, missing canonicals, `noindex` pages and pages without structured data across a whole site.
- **Social previews**: check that every page has `og:title`, `og:description`, `og:image` and a Twitter card before a launch or campaign.
- **Rich results**: list which pages carry Product, Article, FAQ or Breadcrumb markup and which required properties are missing.
- **Migrations and releases**: crawl staging and production and compare titles, canonicals and robots tags.
- **Competitor research**: see how competitors title and describe their pages and which schema types they use.

### What data does it return?

| Field | Description |
|---|---|
| `url`, `finalUrl`, `status`, `httpStatus`, `redirectChain` | What was requested, where it ended and how. `status` is `ok` or why the page was not analysed |
| `indexable` | HTTP 200, no `noindex` in meta robots or `X-Robots-Tag`, and the canonical is missing or points to the page itself |
| `issues`, `issueCount` | Issue codes (list below) |
| `title`, `titleLength`, `metaDescription`, `metaDescriptionLength` | Text and character counts |
| `h1`, `h1Count`, `h2`, `h2Count`, `h3Count` | Headings (first 20 texts) and counts |
| `canonicalUrl`, `canonicalIsSelf`, `metaRobots`, `xRobotsTag` | Indexing signals (`metaRobots` merges `robots` and `googlebot`) |
| `lang`, `charset`, `viewport`, `generator`, `favicon` | Page basics |
| `hreflang` | `[{lang, url}]` alternates |
| `openGraph`, `twitterCard` | All `og:*` and `twitter:*` tags, image URLs made absolute |
| `schemaTypes`, `microdataTypes`, `rdfaTypes` | Structured-data types found |
| `schemaIssues`, `jsonLdBlocksInvalid` | Missing required/recommended properties per type; JSON-LD blocks that are not valid JSON |
| `jsonLd` | Parsed JSON-LD objects, only with **Include full JSON-LD** on |
| `wordCount`, `imagesCount`, `imagesMissingAlt`, `internalLinksCount`, `externalLinksCount`, `nofollowLinksCount` | Content and link counts |
| `depth`, `foundOn` | Crawl depth and the page that linked to it (crawl mode) |
| `responseTimeMs`, `contentType`, `fetchedAt` | Response details |

**Issue codes:** `TITLE_MISSING`, `TITLE_TOO_LONG` (over 60 characters), `TITLE_TOO_SHORT` (under 30), `META_DESCRIPTION_MISSING`, `META_DESCRIPTION_TOO_LONG` (over 160), `META_DESCRIPTION_TOO_SHORT` (under 70), `MULTIPLE_META_DESCRIPTIONS`, `H1_MISSING`, `MULTIPLE_H1`, `CANONICAL_MISSING`, `MULTIPLE_CANONICALS`, `CANONICAL_POINTS_ELSEWHERE`, `NOINDEX`, `LANG_MISSING`, `VIEWPORT_MISSING`, `OG_TITLE_MISSING`, `OG_DESCRIPTION_MISSING`, `OG_IMAGE_MISSING`, `TWITTER_CARD_MISSING`, `HREFLANG_INVALID_CODE`, `HREFLANG_NO_SELF_REFERENCE`, `JSON_LD_INVALID`, `NO_STRUCTURED_DATA`, `SCHEMA_MISSING_REQUIRED_PROPERTIES`, `IMAGES_MISSING_ALT` (no `alt` attribute at all; `alt=""` counts as decorative), `LOW_WORD_COUNT` (under 200 words).

Pages that could not be analysed get a row too, with `status` (`http_error`, `not_html`, `blocked_by_site`, `disallowed_by_robots_txt`, `robots_txt_unreachable`, `unreachable`, `timeout`, `skipped`, `invalid_url`) and a readable `error`. A `SUMMARY` record in the key-value store counts statuses and issues across the run. The Output tab has three views: **Overview**, **Social tags** and **Structured data**.

#### Example output

From a real run on 2026-10-12 (shortened):

```json
{
  "url": "https://www.python.org/",
  "status": "ok",
  "httpStatus": 200,
  "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,
  "metaDescription": "The official home of the Python Programming Language",
  "metaDescriptionLength": 52,
  "h1Count": 5,
  "canonicalUrl": null,
  "lang": "en",
  "openGraph": {"type": "website", "site_name": "Python.org", "title": "Welcome to Python.org", "image": "https://www.python.org/static/opengraph-icon-200x200.png"},
  "twitterCard": {},
  "schemaTypes": ["WebSite"],
  "schemaIssues": ["WebSite: missing required name"],
  "wordCount": 1012,
  "imagesMissingAlt": 0,
  "internalLinksCount": 64,
  "externalLinksCount": 78
}
```

A page that was not analysed (not charged):

```json
{"url": "https://www.g2.com/", "status": "blocked_by_site", "httpStatus": 403, "error": "BLOCKED_BY_SITE (DataDome challenge page, HTTP 403): this actor does not bypass bot protection. Not charged."}
```

### How much does it cost?

**Free while in launch.** The actor charges no fee of its own right now; you pay only the Apify platform usage of your runs (the free Apify plan includes $5 of usage a month). The price below is planned and applies only once it is switched on; Apify notifies existing users before a price change takes effect.

| Event | Planned price |
|---|---|
| Page analysed (HTML page, HTTP 2xx) | $0.002 (= **$2 per 1,000 pages**) |

At the planned price, the default input (3 pages) would cost $0.006, and a 12-page crawl with 5 failed URLs (from our test on 2026-10-12) $0.024. **Not charged:** HTTP errors, redirects to errors, non-HTML files, pages blocked by bot protection or robots.txt, timeouts, invalid URLs. Once pricing is on, **Maximum cost per run** stops the run cleanly.

For comparison (Apify Store, checked 2026-10-12): the most-used SEO audit crawler charges $40 per 1,000 pages, a schema-markup scraper $5 per 1,000 and an on-page audit crawler $15 per 1,000.

### How to use it

1. Paste URLs into **Page URLs** (up to 10,000), one per line.
2. To audit a whole site, enter its home page and set **Pages per site** (for example 500). Internal links are followed breadth-first; `nofollow` links and files such as PDFs and images are skipped.
3. Press Start, then export CSV or Excel from the Output tab, or schedule the actor and connect Google Sheets, Slack or a webhook.

#### Input example

```json
{
  "urls": ["https://www.python.org/"],
  "maxPagesPerSite": 200,
  "maxPages": 1000,
  "includeJsonLd": false
}
```

#### API example

```bash
curl -X POST "https://api.apify.com/v2/acts/adamhaleworks~seo-meta-tags-schema-extractor/run-sync-get-dataset-items?token=<TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"urls": ["https://apify.com/", "https://www.python.org/"]}'
```

### Polite by design

- One honest User-Agent: `seo-meta-tags-schema-extractor/1.0 (+https://apify.com/<this actor>)`. No browser disguise, no proxy, no retry with a different identity.
- robots.txt is read for every site (RFC 9309) and followed for our User-Agent token. A robots.txt that fails with a server error means nothing is fetched from that site.
- At most 2 parallel requests per host. Retries use backoff and honour `Retry-After` up to 30 seconds. After an HTTP 429 or a bot-protection challenge, nothing more is sent to that host in the run.
- Private and internal addresses are refused, including redirects to them.

### Limitations

- The HTML as served is analysed; content added later by JavaScript (client-side rendered titles or JSON-LD) is not seen.
- Schema checks cover Google's most common rich-result types with their main properties; they are not a full validator like the Rich Results Test.
- Sites behind bot protection that challenge non-browser clients are reported as `blocked_by_site`, not bypassed.

### FAQ

**Why is a page `disallowed_by_robots_txt`?** The site's robots.txt does not allow it for crawlers (often `User-agent: *` with `Disallow: /`). Site owners can block or allow this actor by name with `User-agent: seo-meta-tags-schema-extractor`.

**Does it follow redirects?** Yes, up to 10 hops; the chain is in `redirectChain` and the analysed page in `finalUrl`.

**Something is wrong or missing?** Open an issue on the actor's Issues tab and include the URL.

***

Author: Adam Hale

# Actor input Schema

## `urls` (type: `array`):

Pages to analyse, one per line. 'example.com/page' works too. Up to 10,000 per run.

## `maxPagesPerSite` (type: `integer`):

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.

## `maxPages` (type: `integer`):

Stops the run after this many pages (start URLs plus crawled pages).

## `includeJsonLd` (type: `boolean`):

Adds each page's parsed JSON-LD objects (up to 50) to the row. Off: only the schema types and the missing-property checks.

## `maxConcurrency` (type: `integer`):

Pages fetched at the same time. At most 2 run against the same host.

## `requestTimeoutSecs` (type: `integer`):

How long to wait for each page.

## `maxRetries` (type: `integer`):

Retries for network errors and HTTP 429/5xx answers, with backoff. Retry-After is honoured up to 30 s.

## Actor input object example

```json
{
  "urls": [
    "https://apify.com/",
    "https://www.python.org/",
    "https://developer.mozilla.org/en-US/"
  ],
  "maxPagesPerSite": 1,
  "maxPages": 1000,
  "includeJsonLd": false,
  "maxConcurrency": 10,
  "requestTimeoutSecs": 30,
  "maxRetries": 2
}
```

# Actor output Schema

## `results` (type: `string`):

Dataset items, one per page. Views: overview, social tags and structured data.

## `summary` (type: `string`):

SUMMARY record in the default key-value store: pages analysed and charged, status counts, issue counts across all pages, hosts that refused us, stop reason.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "urls": [
        "https://apify.com/",
        "https://www.python.org/",
        "https://developer.mozilla.org/en-US/"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("adamhaleworks/seo-meta-tags-schema-extractor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "urls": [
        "https://apify.com/",
        "https://www.python.org/",
        "https://developer.mozilla.org/en-US/",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("adamhaleworks/seo-meta-tags-schema-extractor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "urls": [
    "https://apify.com/",
    "https://www.python.org/",
    "https://developer.mozilla.org/en-US/"
  ]
}' |
apify call adamhaleworks/seo-meta-tags-schema-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,adamhaleworks/seo-meta-tags-schema-extractor"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/Mr8TPTwVKcM6BqPi6/builds/NVShGVPAh2uLssrfa/openapi.json
