# Structured Data Extractor (`openrows/structured-data-extractor`) Actor

JSON-LD, microdata, RDFa Lite, Open Graph, Twitter Card, meta, hreflang and canonical for a list of URLs, with SEO validation flags, in one call.

- **URL**: https://apify.com/openrows/structured-data-extractor.md
- **Developed by:** [openrows](https://apify.com/openrows) (community)
- **Categories:** SEO tools, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$3.00 / 1,000 analysed pages

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## 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

## Structured Data Extractor

Reads every machine-readable signal a page publishes about itself — JSON-LD, microdata, RDFa Lite,
Open Graph, Twitter Card, the basic `<head>` meta tags, hreflang alternates and the canonical URL —
and flags what's missing against a simple set of SEO/rich-result rules. Built for SEO audits,
AI-visibility ("GEO") audits and content-ops QA, where the usual tools give you one or two of these
formats and make you stitch the rest together yourself.

Give it a list of URLs. It fetches exactly those pages — nothing is discovered or crawled beyond
them — and returns one result per page. HTTP-only, no browser, so runs are fast and cheap.

### What data you get

For every page the Actor returns:

| Field        | Description                                                                                                                           |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
| `url`        | Page the result was extracted from                                                                                                    |
| `scrapedAt`  | ISO 8601 timestamp of the extraction                                                                                                  |
| `jsonLd`     | Every JSON-LD entity found, flattened out of any `@graph` wrapper, each keeping its `@type`                                           |
| `microdata`  | Top-level `itemscope` items (HTML microdata), each with its `itemtype`(s), `itemid` and properties                                    |
| `rdfa`       | Top-level `[typeof]` items (RDFa Lite), each with its type(s), `resource` and properties                                              |
| `openGraph`  | `og:*` meta properties, keyed without the `og:` prefix                                                                                |
| `twitter`    | `twitter:*` meta tags, keyed without the `twitter:` prefix (`creator`/`site` and every `label*`/`data*` pair are dropped — see below) |
| `meta`       | `title`, `description`, `robots`, `viewport`, `charset` from `<head>`                                                                 |
| `hreflang`   | `<link rel="alternate" hreflang>` entries, with absolute URLs                                                                         |
| `canonical`  | `<link rel="canonical">` target, resolved to an absolute URL, or `null`                                                               |
| `validation` | Missing required properties for recognised `@type`s — see "Validation rules" below                                                    |
| `raw`        | Only when `includeRaw` is true: each JSON-LD block in its original, unflattened shape                                                 |

### Input

| Option               | Type    | Default          | Description                                                                  |
| -------------------- | ------- | ---------------- | ---------------------------------------------------------------------------- |
| `urls`               | array   | -                | Pages to extract structured data from. Required.                             |
| `maxItems`           | integer | `100`            | Stop after this many results (max 10,000). Also your cost cap.               |
| `includeRaw`         | boolean | `false`          | Also return each page's JSON-LD blocks in their original, unflattened shape. |
| `proxyConfiguration` | object  | Apify datacenter | Proxy settings                                                               |

Example input:

```json
{
    "urls": ["https://docs.apify.com/", "https://schema.org/Product"],
    "maxItems": 50
}
```

### Output example

```json
{
    "url": "https://schema.org/Product",
    "scrapedAt": "2026-09-22T10:15:30.000Z",
    "jsonLd": [{ "@type": "WebPage", "name": "Product - Schema.org Type" }],
    "microdata": [],
    "rdfa": [],
    "openGraph": {},
    "twitter": {},
    "meta": {
        "title": "Product - Schema.org Type",
        "description": null,
        "robots": null,
        "viewport": null,
        "charset": "UTF-8"
    },
    "hreflang": [],
    "canonical": null,
    "validation": []
}
```

A run summary (items pushed, items skipped by category, pages fetched, failed requests by category,
stop reason) is stored in the run's key-value store under the key `OUTPUT`.

### Validation rules

A simple, documented rule set — not a full schema.org or Google Rich Results validator. For entities
whose type matches one of the rows below (checked on both `jsonLd` and `microdata`; `rdfa` is not
checked), the Actor flags which of the listed properties are missing. A type not in this list is not
checked at all, and a matching entity with nothing missing produces no entry in `validation[]`.

| Type (any of)                           | Required properties checked              |
| --------------------------------------- | ---------------------------------------- |
| `Product`                               | `name`, `image`, `description`, `offers` |
| `Article`, `NewsArticle`, `BlogPosting` | `headline`, `image`, `datePublished`     |
| `Organization`                          | `name`, `url`                            |
| `BreadcrumbList`                        | `itemListElement`                        |

`author`/`creator` are intentionally absent from every row: this Actor drops those properties for
privacy before validation ever runs (see "Data and compliance"), so requiring them would flag a page
as broken for a field it never had a chance to keep.

### Pricing

**Pay per result.** You are charged per result and nothing else: no platform usage, no compute units.
Listing pages, retries and failed requests are free. The current rate is on the *Pricing* tab of this
Actor — that is the only place it is set, so no figure is repeated here.

- `maxItems` caps the number of results, and therefore the cost, of a run.
- The run also stops when it reaches the *maximum total charge* you set for the run in Apify Console
  or through the API.

### Tips

- Start with `maxItems: 20` to check the fields before a large run.
- If you only need the source's own `@graph` grouping rather than this Actor's flattened `jsonLd[]`
  list, set `includeRaw: true` and read `raw.jsonLdBlocks`.

### Limitations

- Pages that require a login, or that build their structured data with client-side JavaScript after
  load, are not accessible to an HTTP-only fetch and are not scraped — you'll get an empty result,
  not an error.
- `microdata` and `rdfa` do not support `itemref`/indirect property references (properties pulled in
  from elsewhere in the document by id) — only directly nested properties are read. Uncommon in
  practice; every page seen while building this Actor used direct nesting.
- `openGraph` covers the `og:` namespace only, not Facebook's wider `article:`/`profile:` namespaces
  (which can themselves carry personal fields like `article:author` or `profile:first_name`).
- `rdfa` picks up any `[typeof]` attribute, including non-schema.org uses of the same mechanism (for
  example, MediaWiki sites annotate their own templates with a `mw:` namespace via `typeof`). These
  are harmless but not useful signal — filter by type prefix if your source does this.
- `validation` covers four common types with a fixed property list each; it is not a general
  schema.org or Google Rich Results validator.

### Data and compliance

This Actor is **site-agnostic**: it fetches only the URLs you give it, and does not discover, crawl
or follow links beyond them. You choose the sources and are responsible for them — for your right to
use the pages you point this Actor at, and for how you use the results.

This Actor collects **publicly available, non-personal data only**:

- It fetches pages that are reachable without logging in and does not bypass access controls or solve
  CAPTCHAs.
- It identifies itself with a plain `openrows/structured-data-extractor` User-Agent and sends no
  headers that imitate a browser.
- It removes fields that identify a person before a result is stored:
  - any JSON-LD, microdata or RDFa entity whose type is (or includes) `Person` — matched whether the
    type is given as a bare term, a prefixed name or a full IRI, with or without a trailing slash;
  - the properties `author`, `creator`, `byline`, `contributor`, `dc:creator`, `email`, `telephone`,
    `tel`, wherever they appear in JSON-LD, microdata or RDFa;
  - the Twitter Card `twitter:creator` and `twitter:site` handles, and every `twitter:label*` /
    `twitter:data*` pair — including the bare, unnumbered `twitter:label` / `twitter:data` form
    (free-form slots some sites use for a byline or reading time; the label text is
    caller-controlled, so these are dropped by key pattern regardless of what they say).
    `creator`/`site` are user handles, and this Actor does not try to guess which handles belong to
    an organisation rather than a person, so both are dropped rather than heuristically kept.
- **Credit fields are kept as public professional attribution.** A `developer`, `publisher`, `editor`
  or `holder` name is kept exactly as the source page shows it — the same way a book cover credits its
  author, or software credits its publisher. This does not weaken the removal above: an entity that is
  itself `Person`-typed is still dropped even when it fills a credit field, and `author`/`creator`/
  `byline`/`contributor`/`email`/`telephone` are still dropped wherever they appear.
- Free text a page displays in its own right — a product name, an article headline, a description —
  is not personal data merely for being prose, and passes through unless its property name matches the
  list above. This also means a byline can reach the output through free text the property-name filter
  does not scan: for example, a page's own `text`-style free-text property (as in some of schema.org's
  microdata examples) can contain prose like `"...by John Doe"`. This Actor does not scan free-text
  values for embedded names — doing so would mean guessing at what is a name inside arbitrary prose,
  which this Actor does not attempt (see the credit-fields point above). That text passes through as
  the buyer's content, same as any other free text on the page.

#### How robots.txt is handled

Before the first page on a host is fetched, the Actor fetches that host's `robots.txt` once and obeys
it for the rest of the run. Rules are read for the product token `openrows`, falling back to the `*`
group when the file does not name us, with `*` and `$` wildcards and the standard "longest matching
rule wins" precedence. A `Crawl-delay` set for us is honoured, up to 10 seconds. When a page redirects,
the URL the redirect actually leads to is checked again against its own host's rules before anything is
read from it, so a redirect cannot carry the Actor onto a site, or a path, that its owner puts off limits.

- **Disallowed URLs are never requested.** They are reported under `skipped.robotsDisallowed` in the
  run summary, so you can see exactly how many of your input URLs the site puts off limits.
- **No `robots.txt` (404 or 410) means no rules**, and the host is crawled normally. It is counted as
  `robotsAbsent` in the summary.
- **A `robots.txt` we cannot read means the host is skipped, not crawled.** Any other response — 401,
  403, 429, a 5xx, a timeout, a network failure, or a 200 that turns out to be an error or login page
  — leaves us without the site's rules, and this Actor will not guess. Every URL on that host is
  skipped and counted under `skipped.robotsUnknown`, and the host and the status that stopped us are
  listed under `robots.unknownHosts` in the summary. If a host you supplied returns no results, look
  there first.

**Removal requests.** If you believe this Actor exposes data it should not, open an issue on the
Actor's *Issues* tab and state the URL(s) concerned. Requests are answered there, and the fields or
pages in question are removed.

You are responsible for using the extracted data in line with the terms of the sites you point this
Actor at and the laws that apply to you.

### Support

Report bugs and request fields on the *Issues* tab of this Actor. Include the run ID.

# Actor input Schema

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

Pages to extract structured data from. This Actor fetches exactly the URLs you give it — nothing is discovered or crawled beyond them.

## `maxItems` (type: `integer`):

Stop after this many results. You are charged per result, so this is also your cost cap. Maximum 10,000 per run; split a bigger job across runs.

## `includeRaw` (type: `boolean`):

Also return each page's JSON-LD <script> blocks in their original, unflattened shape (object / array / @graph), alongside the normalised jsonLd\[] list. Personal-data fields are stripped from this raw form exactly as they are from jsonLd\[] — this option does not bypass that filter.

## `proxyConfiguration` (type: `object`):

Proxy settings. Apify datacenter proxies are the default and are enough for most targets.

## Actor input object example

```json
{
  "urls": [
    "https://docs.apify.com/",
    "https://schema.org/Product"
  ],
  "maxItems": 100,
  "includeRaw": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

No description

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

No description

# 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://docs.apify.com/",
        "https://schema.org/Product"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("openrows/structured-data-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://docs.apify.com/",
        "https://schema.org/Product",
    ],
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("openrows/structured-data-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://docs.apify.com/",
    "https://schema.org/Product"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call openrows/structured-data-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,openrows/structured-data-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/ExLFOr5KvhVTzOG8s/builds/7H8FwPByYQictaUVM/openapi.json
