# On-Page SEO Audit - Metadata, Headings and Explained Scores (`grit-77/on-page-seo-audit`) Actor

On-page SEO audit for supplied public URLs. Export metadata, headings, links, indexability signals and explained rule-based scores as JSON with request errors.

- **URL**: https://apify.com/grit-77/on-page-seo-audit.md
- **Developed by:** [Grit](https://apify.com/grit-77) (community)
- **Categories:** SEO tools, Marketing
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.00 / 1,000 audited 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

## On-Page SEO Audit - Metadata, Headings and Explained Scores

On-page SEO scraper returns page metadata and a rule-based audit for supplied public URLs. On-page SEO API results include titles, descriptions, headings, links, indexability signals, scores, and errors.

### Input example

```json
{
  "urls": [
    "https://example.com/"
  ],
  "maxItems": 3
}
```

The input keys and allowed values follow [input_schema.json](.actor/input_schema.json).

### Output fields

| Field | Type | Example or excerpt from sample |
|---|---|---|
| `url` | string | `"https://example.com/"` |
| `finalUrl` | string | `"https://example.com/"` |
| `statusCode` | integer | `200` |
| `responseTimeMs` | integer | `297` |
| `fetchedAt` | string | `"2026-10-05T13:05:03.966343+00:00"` |
| `title` | string | `"Example Domain"` |
| `titleLength` | integer | `14` |
| `titleCount` | integer | `1` |
| `metaDescription` | string or null | `null` |
| `metaDescriptionLength` | integer | `0` |
| `metaDescriptionCount` | integer | `0` |
| `canonical` | string or null | `"https://schema.org/Product"` |
| `canonicals` | array | `[]` |
| `robotsMeta` | object | `{}` |
| `xRobotsTag` | string or null | `null` |
| `noindex` | boolean | `false` |
| `hreflang` | array | `[]` |
| `headings` | array | `[]` |
| `h1Count` | integer | `0` |
| `imageCount` | integer | `0` |
| `imagesWithAlt` | integer | `0` |
| `imagesWithEmptyAlt` | integer | `0` |
| `imagesMissingAlt` | integer | `0` |
| `imageAltCoverage` | integer or null | `100` |
| `internalLinkCount` | integer | `0` |
| `externalLinkCount` | integer | `0` |
| `structuredDataTypes` | array | `[]` |
| `structuredDataErrors` | array | `[]` |
| `openGraph` | object | `{}` |
| `twitter` | object | `{}` |
| `wordCount` | integer | `25` |
| `error` | string or null | `null` |
| `scoreVersion` | string | `"1.0"` |
| `scoreRules` | array | `[{"id": "title", "maximum": 15, "earned": 0, "passed": false, "reason": "Exactly one …` |
| `score` | integer | `20` |
| `issues` | array | `[]` |
| `attempts` | integer | `1` |
| `redirectChain` | array | `[]` |

### Output example

Trimmed from [sample_output.json](sample_output.json); omitted fields remain available in the dataset.

```json
{
  "url": "https://example.com/",
  "finalUrl": "https://example.com/",
  "statusCode": 200,
  "responseTimeMs": 297,
  "fetchedAt": "2026-10-05T13:05:03.966343+00:00",
  "title": "Example Domain",
  "titleLength": 14,
  "error": null
}
```

### Transparent scoring: version 1.0

| Rule | Maximum | Full-credit condition |
|---|---:|---|
| Title | 15 | Exactly one title, 30–60 characters |
| Description | 15 | Exactly one description, 70–160 characters |
| Canonical | 10 | Exactly one canonical resolving to an absolute HTTP(S) URL |
| Indexability | 10 | No noindex/none detected in the inspected meta tags or X-Robots-Tag |
| H1 | 10 | Exactly one non-empty H1 |
| Heading order | 5 | Starts with H1; adjacent headings do not skip upward by more than one level |
| Image alt | 10 | Non-empty alt on all images; partial points = 10 × non-empty-alt ratio; no images earns 10 |
| Text quantity | 10 | At least 300 text tokens |
| Structured data | 5 | At least one JSON-LD/microdata type, without JSON-LD parse errors |
| Open Graph | 5 | Non-empty og:title, og:description and og:image |
| Twitter card | 5 | twitter:card is summary, summary_large_image, app or player |

The maximum sums to 100. Image partial credit is rounded to two decimals; the other rules award their maximum or zero. `scoreRules` contains the point calculation and reason for each rule.

These thresholds are this Actor's checklist heuristics, not Google's ranking rules or a ranking forecast. Empty alt can be correct for a decorative image; it is reported separately and receives no non-empty-alt credit. Short pages or pages intentionally marked noindex can be correct for their purpose. Inspect the reasons before choosing changes. Response time and hreflang presence do not contribute points.

### Limits and error handling

| Input | Default | Allowed range |
|---|---:|---|
| `maxItems` | 1,000 | 1–10,000 result rows, including errors |
| `concurrency` | 5 | 1–20 workers |
| `timeoutSecs` | 30 | 5–120 seconds per fetch attempt |
| `maxRetries` | 2 | 0–3 retries after the initial attempt |
| `maxResponseBytes` | 2,000,000 | 1,024–10,000,000 decoded bytes per page |

The input list may contain at most 100,000 URL entries before deduplication. The first distinct URLs within maxItems are attempted; dataset order follows completion order. Lower concurrency when requesting many pages from one host.

Network errors and HTTP 408/429/500/502/503/504 receive exponential backoff (1, 2, 4 seconds). Numeric Retry-After is honored up to 30 seconds. There is a maximum of ten redirects per attempt. Redirect destinations are validated before requesting them. Unsupported content types and excessive responses produce error rows.

Only explicitly supplied pages and their HTTP redirects are requested. There is no site-wide crawl, browser rendering, login, CAPTCHA solving, robots.txt fetch, hreflang reciprocity check, link-status check or schema semantic validation. Public host validation rejects private IP literals and private DNS answers; DNS preflight is not connection-level IP pinning, so this is not a security gateway for untrusted hostile inputs. HTML parsing is best-effort for malformed markup, and CSS visibility is not evaluated.

### FAQ

**What are the output limits?** `maxItems` caps result rows, including errors. Fetch failures, non-HTML responses, and oversized pages produce error rows.

**Do I need a proxy or login?** Public URLs need no login or proxy. The actor does not authenticate or render JavaScript.

**How is this priced?** Proposed pay per event: $0.002 per successfully audited `page` row. Error rows are uncharged. These are proposals in [PRICING.md](PRICING.md); the owner must configure Store billing.

**How are rate limits handled?** `concurrency` limits fetches; transient failures are retried with backoff and `Retry-After`.

**How fresh is the data?** Results reflect HTML fetched at run time. JavaScript rendered content may be absent. Scores are this actor’s checklist heuristics, not search ranking predictions; review rule reasons before acting.

### Use with AI agents / MCP

After the actor is published, connect your agent to the [Apify MCP server](https://docs.apify.com/integrations/mcp) with an Apify API token. Use `call-actor` with the published actor identifier and the JSON input shown above. Read the returned dataset with `get-dataset-items`; the run response itself contains status and storage IDs, not the result rows. Alternatively, use the [Apify API](https://docs.apify.com/api/v2) to start a run and read its default dataset. Keep the actor input bounded with `maxItems`, and inspect source links and any error fields before using results in an answer.

# Actor input Schema

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

Absolute HTTP or HTTPS URLs. Fragments and default ports are removed, duplicates are dropped, and invalid URLs produce free error rows. Private hosts and credentials are rejected.

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

Cap on all dataset rows, including free errors. The first distinct URLs up to this cap are attempted.

## `concurrency` (type: `integer`):

Number of parallel workers. Reduce this when auditing one host.

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

Total deadline for one fetch attempt, including DNS, redirects and body transfer. Retries have their own deadline.

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

Retries after network failures or HTTP 408, 429, 500, 502, 503 and 504, with exponential backoff.

## `maxResponseBytes` (type: `integer`):

Decoded body size cap per response; an oversized response produces an unbilled error row.

## Actor input object example

```json
{
  "urls": [
    "https://example.com/",
    "https://apify.com/",
    "https://schema.org/Product"
  ],
  "maxItems": 1000,
  "concurrency": 5,
  "timeoutSecs": 30,
  "maxRetries": 2,
  "maxResponseBytes": 2000000
}
```

# Actor output Schema

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

Dataset with one audit or error row per attempted distinct URL.

# 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://example.com/",
        "https://apify.com/",
        "https://schema.org/Product"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("grit-77/on-page-seo-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://example.com/",
        "https://apify.com/",
        "https://schema.org/Product",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("grit-77/on-page-seo-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://example.com/",
    "https://apify.com/",
    "https://schema.org/Product"
  ]
}' |
apify call grit-77/on-page-seo-audit --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,grit-77/on-page-seo-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/A3vHIA40Z1xh0MlZ9/builds/wmkDVGlXZP43EVwxl/openapi.json
