# On-Page SEO & Metadata Audit (Bulk URLs) (`plainwren/seo-metadata-audit`) Actor

Audit titles, meta descriptions, Open Graph, Twitter cards, canonical, hreflang, robots and JSON-LD on up to 5,000 URLs you list. Get a score and a plain fix list per page. Only your listed URLs are fetched; failed pages are free. $0.005 per URL audited.

- **URL**: https://apify.com/plainwren/seo-metadata-audit.md
- **Developed by:** [Plainwren Studio](https://apify.com/plainwren) (community)
- **Categories:** Marketing, Developer tools, AI
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 url auditeds

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

### What is On-Page SEO & Metadata Audit?

**On-Page SEO & Metadata Audit** checks the SEO and social metadata on **URLs you list** and returns a **score plus a plain fix list** for each page. It is for your own or authorised sites.

It fetches **only the URLs you give it**. It does not crawl, follow links or read sitemaps. It obeys robots.txt, never logs in, never sends cookies, and never gets past captchas or bot checks. A page that refuses access comes back as a failed row, and **failed rows are free**.

Try it: leave the URL list empty and click **Start** for a free demo on a built-in sample page.

Bot info (user agent and behaviour): [tools.plainwrenstudio.com/bots/seo-audit/](https://tools.plainwrenstudio.com/bots/seo-audit/). Support: <support@plainwrenstudio.com>.

### What it checks, per listed URL

- **Title:** present, single, length in characters (default 30–60, editable).
- **Meta description:** present, single, length (default 70–160, editable).
- **Headings:** h1 text and count, h1–h3 counts, skipped levels.
- **Canonical:** from `<link rel=canonical>` and the `Link` header (`self`, `other`, `missing`, `multiple`, `cross-domain` or `invalid`; relative URLs resolved, including `<base href>`).
- **Robots:** meta robots / googlebot, the `X-Robots-Tag` header, and `indexable` from both.
- **hreflang:** valid codes, self link, no duplicates; return links checked **between the URLs you listed** (partner pages you did not list are reported, never fetched).
- **Social:** Open Graph (title, description, image, url, type, site_name) and Twitter card. `og:image` is checked with one HEAD (cached, same rate limit and robots rules).
- **JSON-LD:** each block is valid JSON, its `@type`, and missing recommended properties for Article, Product, Organization, LocalBusiness, BreadcrumbList, FAQPage and WebSite (`@graph` and arrays handled).
- **Images:** `<img>` without an alt attribute (`alt=""` counts as fine).
- **Basics:** `html lang`, viewport, declared charset, and a word **count** (a number only).
- A plain **fix list** (`issues`: code, severity, message, fix) and a **score**: 100, minus 10 per error, minus 3 per warning, floor 0.

### Who is it for?

- 🧑‍💻 SEO and marketing teams auditing their own site's titles, meta tags and structured data in bulk.
- 🔗 Agencies and freelancers who need a CSV or HTML report for clients.
- 🤖 Developers and agents calling it through the Apify API, integrations or MCP.

### How to run an audit

1. Open the **Input** tab.
2. Add the pages under **URLs** (one per line), or leave empty for the free demo.
3. Optional: turn check groups on or off, change title / description length ranges, or adjust the polite rate.
4. Click **Start**.
5. Read the dataset rows, or download `REPORT.csv` / `REPORT.html` from the key-value store.

#### Input example

```json
{
  "urls": [
    "https://tools.plainwrenstudio.com/",
    "https://tools.plainwrenstudio.com/bots/",
    "https://tools.plainwrenstudio.com/json-formatter/"
  ],
  "checks": {
    "title": true,
    "meta": true,
    "headings": true,
    "canonical": true,
    "robots": true,
    "hreflang": true,
    "social": true,
    "jsonLd": true,
    "images": true,
    "basics": true
  },
  "titleLength": { "min": 30, "max": 60 },
  "descriptionLength": { "min": 70, "max": 160 },
  "checkOgImage": true,
  "requestsPerSecond": 1,
  "maxConcurrency": 4,
  "timeoutSecs": 20
}
```

| Field | Type | What it does |
|---|---|---|
| `urls` | array of strings | Pages to audit, max 5,000 per run. Only these exact URLs are fetched. A URL without a scheme gets `https://`. Leave empty for the free demo. |
| `checks` | object | Turn check groups on or off (all on by default). |
| `titleLength` | object | Warn when the title is outside `min`/`max` characters (default 30–60). |
| `descriptionLength` | object | Warn when the meta description is outside `min`/`max` (default 70–160). |
| `checkOgImage` | boolean (default `true`) | One HEAD per distinct `og:image` URL. |
| `requestsPerSecond` | number (default `1`, 0.2–4) | Max request rate per host; one request at a time per host. |
| `maxConcurrency` | integer (default `4`, 1–10) | How many different hosts at once. |
| `timeoutSecs` | integer (default `20`, 5–60) | Per-request timeout. Timeouts are free. |

#### Output

- **Dataset:** one row per URL (same fields every time). Rows arrive in completion order; `index` is the position in your input.
- **Key-value store:**
  - `SUMMARY`: URLs audited, average score, failures by code, top issues with page counts, hreflang return-link totals.
  - `HREFLANG`: return-link results for each page.
  - `REPORT.csv`: UTF-8 with a BOM; formula-looking cells are neutralised.
  - `REPORT.html`: static, escaped, no scripts, with a strict CSP.
  - `OUTPUT`: run counts.
- **Failure codes (never charged):** `BAD_URL`, `GUARD_BLOCKED`, `DNS_FAILED`, `ROBOTS_BLOCKED`, `AUTH_REQUIRED`, `BOT_CHALLENGE`, `RATE_LIMITED`, `HTTP_ERROR`, `NOT_HTML`, `TOO_MANY_REDIRECTS`, `TIMEOUT`, `FETCH_FAILED`, `DUPLICATE`, `PARSE_FAILED`.
- **Note:** `HREFLANG_NO_RETURN` needs every listed page, so it is reported in `HREFLANG`, `SUMMARY` and the reports at the end of the run, not in each row's `issues`.

### How much does it cost?

Pay-per-event pricing:

- **$0.005 per URL audited** (`url-audited`): **$5.00 per 1,000** successfully audited HTML pages.
- **Failed URLs are free** (robots-blocked, login/challenge, rate-limited, guard-blocked, non-HTML, duplicates, timeouts, and the demo run).
- The run stops cleanly before it would go over your maximum charge.

Apify's free plan includes monthly platform credit you can use to try it.

### How polite it is

- One request at a time per host; default 1 request per second (0.2–4).
- Up to 4 hosts at once by default (max 10).
- robots.txt read once per host and obeyed. `Crawl-delay` honoured up to 10 s.
- On 429/503: waits `Retry-After` (max 60 s, 2 retries), then `RATE_LIMITED` (free).
- Private, internal and reserved addresses are refused on the first request, on every redirect hop, and again at DNS lookup. Max 5 redirect hops.
- Honest User-Agent: `PlainwrenSEOAudit/1.0 (+https://tools.plainwrenstudio.com/bots/seo-audit/)`.
- Plain HTTP only: no JavaScript, no browser.

### Limits

- Up to **5,000 URLs** per run; extras are skipped, reported and not charged.
- No login, cookies, auth headers or proxies. Pages behind a login or bot check fail as free rows.
- It does not execute JavaScript, so client-rendered-only pages may look empty of tags.
- It returns SEO and metadata fields only: no body text, emails or phone numbers.

### FAQ

#### Does it crawl my site?

No. Only the URLs you list are fetched (plus robots.txt per host, and an optional HEAD for each distinct `og:image`).

#### Am I charged for pages that fail?

No. Failures are free, including the demo run.

#### Can I use it from Make, Zapier, n8n or an AI agent?

Yes, through Apify's integrations, API and MCP.

#### Something looks wrong. What should I do?

Open an issue on the Actor's **Issues** tab (remove anything private) or email <support@plainwrenstudio.com>.

# Actor input Schema

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

The pages to audit, one per line (max 5,000 per run). Only these exact URLs are fetched: links and sitemaps are not followed. Use it for your own or authorised sites. robots.txt is obeyed. Pages behind a login or a bot check fail and are not charged. Leave empty to run a free demo on a built-in sample page.

## `checks` (type: `object`):

Turn check groups on or off (all on by default). Keys: title, meta, headings, canonical, robots, hreflang, social, jsonLd, images, basics. A group that is off returns null fields and no issues.

## `titleLength` (type: `object`):

Warn when the title is shorter than min or longer than max characters. Default {"min": 30, "max": 60}.

## `descriptionLength` (type: `object`):

Warn when the meta description is shorter than min or longer than max characters. Default {"min": 70, "max": 160}.

## `checkOgImage` (type: `boolean`):

Send one HEAD request per distinct og:image URL (same rate limit and robots.txt rules) to confirm it returns an image.

## `requestsPerSecond` (type: `number`):

Maximum request rate on each host (0.2-4, default 1). One request at a time per host. A robots.txt Crawl-delay or 429 answers slow it down further.

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

How many different hosts are audited at the same time (1-10, default 4). Each host still gets one request at a time.

## `timeoutSecs` (type: `integer`):

Per request (5-60, default 20). A page that doesn't answer in time fails as TIMEOUT and is not charged.

## Actor input object example

```json
{
  "urls": [
    "https://tools.plainwrenstudio.com/",
    "https://tools.plainwrenstudio.com/bots/",
    "https://tools.plainwrenstudio.com/json-formatter/"
  ],
  "checks": {
    "title": true,
    "meta": true,
    "headings": true,
    "canonical": true,
    "robots": true,
    "hreflang": true,
    "social": true,
    "jsonLd": true,
    "images": true,
    "basics": true
  },
  "titleLength": {
    "min": 30,
    "max": 60
  },
  "descriptionLength": {
    "min": 70,
    "max": 160
  },
  "checkOgImage": true,
  "requestsPerSecond": 1,
  "maxConcurrency": 4,
  "timeoutSecs": 20
}
```

# Actor output Schema

## `dataset` (type: `string`):

Dataset of audited (or failed) URLs.

## `OUTPUT` (type: `string`):

Compact run totals for the Console.

## `SUMMARY` (type: `string`):

Full audit summary JSON.

## `REPORT_html` (type: `string`):

Human-readable HTML report.

## `REPORT_csv` (type: `string`):

CSV export of slim audit rows.

## `HREFLANG` (type: `string`):

Cross-page hreflang analysis when enabled.

# 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://tools.plainwrenstudio.com/",
        "https://tools.plainwrenstudio.com/bots/",
        "https://tools.plainwrenstudio.com/json-formatter/"
    ],
    "checks": {
        "title": true,
        "meta": true,
        "headings": true,
        "canonical": true,
        "robots": true,
        "hreflang": true,
        "social": true,
        "jsonLd": true,
        "images": true,
        "basics": true
    },
    "titleLength": {
        "min": 30,
        "max": 60
    },
    "descriptionLength": {
        "min": 70,
        "max": 160
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("plainwren/seo-metadata-audit").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://tools.plainwrenstudio.com/",
        "https://tools.plainwrenstudio.com/bots/",
        "https://tools.plainwrenstudio.com/json-formatter/",
    ],
    "checks": {
        "title": True,
        "meta": True,
        "headings": True,
        "canonical": True,
        "robots": True,
        "hreflang": True,
        "social": True,
        "jsonLd": True,
        "images": True,
        "basics": True,
    },
    "titleLength": {
        "min": 30,
        "max": 60,
    },
    "descriptionLength": {
        "min": 70,
        "max": 160,
    },
}

# Run the Actor and wait for it to finish
run = client.actor("plainwren/seo-metadata-audit").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://tools.plainwrenstudio.com/",
    "https://tools.plainwrenstudio.com/bots/",
    "https://tools.plainwrenstudio.com/json-formatter/"
  ],
  "checks": {
    "title": true,
    "meta": true,
    "headings": true,
    "canonical": true,
    "robots": true,
    "hreflang": true,
    "social": true,
    "jsonLd": true,
    "images": true,
    "basics": true
  },
  "titleLength": {
    "min": 30,
    "max": 60
  },
  "descriptionLength": {
    "min": 70,
    "max": 160
  }
}' |
apify call plainwren/seo-metadata-audit --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,plainwren/seo-metadata-audit"
        }
    }
}
```

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/x024D8WUoccy3Offt/builds/yzYgL1UgAs3KyD1Yz/openapi.json
