# SEO On-Page Audit for Any URL List (`jadeite_geometry/seo-onpage-audit`) Actor

An on-page SEO report for every URL you list: title, meta description, headings, canonical, hreflang, Open Graph, schema types, image alt text, links, response time and a 0 to 100 score.

- **URL**: https://apify.com/jadeite_geometry/seo-onpage-audit.md
- **Developed by:** [Niklesh Kumar](https://apify.com/jadeite_geometry) (community)
- **Categories:** SEO tools, Marketing
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 results

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

## SEO On-Page Audit for Any URL List

An on-page SEO report for every web page you list, as a clean table: title, meta description, headings, canonical, social tags, schema types, alt text, links, speed and a 0 to 100 score.

### What does SEO On-Page Audit for Any URL List do?

You give it a list of page addresses. For each page it downloads the HTML and reads the parts that matter for on-page SEO. You get one row per page: the title and its length, the meta description, the headings, the canonical address, the Open Graph and Twitter tags, the schema.org types, how many images have no alt text, how many links stay on the site and how many leave it, the word count and how fast the server answered. The row also lists the problems it found, such as a missing title or a noindex tag, and turns them into a score from 0 to 100. Sort a few hundred pages by score and you know where to start. It works as a bulk status code, redirect and noindex checker too.

It is made for SEO agencies, in-house marketers, web developers and site owners who want a quick technical check of their own pages or their clients' pages without setting up a crawler. It audits the pages you list and nothing else: it doesn't follow links to find more pages. It reads the HTML the server sends and doesn't run JavaScript, so a page that builds its content in the browser can show few headings and words.

### What data does it return?

One row for every page that was read. The same fields are always there. A field that doesn't apply to a page is empty (`null`).

| Field | Example | Meaning |
|---|---|---|
| `url` | `https://www.python.org/` | The page address you listed, tidied: lower-case host name, no `#fragment`, and `https://` added if you left it out. |
| `finalUrl` | `https://www.python.org/` | The address the row describes. The same as `url` unless redirects were followed. |
| `status` | `200` | The HTTP status code the page answered with. |
| `score` | `84` | From 0 to 100: 100 minus the points of every issue found (see the table below). Empty when the page was not audited. |
| `issues` | `["title-too-short", "canonical-missing"]` | The issue codes found, the most costly first. Empty when nothing is wrong. |
| `redirects` | `0` | How many redirects were followed to reach `finalUrl`. |
| `redirectLocation` | `https://www.python.org/` | Where the page redirects to, on a row that describes the redirect itself. Otherwise empty. |
| `responseTimeMs` | `91` | How long the page took to arrive, in milliseconds, measured from the machine that runs the Actor. A rough speed check, not a Core Web Vitals score. |
| `contentLength` | `52966` | Size of the page in bytes, after decompression. |
| `contentType` | `text/html; charset=utf-8` | The Content-Type the server sent. |
| `xRobotsTag` | `noindex` | The X-Robots-Tag header, when the server sent one. |
| `title` | `Welcome to Python.org` | The title tag on one line, cut at 300 characters. |
| `titleLength` | `21` | Length of the whole title in characters. 0 when there is none. |
| `metaDescription` | `The official home of the Python Programming Language` | The meta description, cut at 500 characters. |
| `metaDescriptionLength` | `52` | Length of the whole meta description in characters. 0 when there is none. |
| `canonical` | `https://www.nasa.gov/` | The canonical address the page declares, made absolute. |
| `robotsMeta` | `follow, index, max-snippet:-1` | The content of the robots meta tag. |
| `viewport` | `width=device-width, initial-scale=1.0` | The content of the viewport meta tag. |
| `lang` | `en` | The `lang` attribute of the `html` tag. |
| `hreflang` | `[{"lang": "de", "url": "https://example.com/de/"}]` | The language versions the page declares, as `lang` and `url` pairs. At most 50. |
| `h1Count` | `5` | Number of `h1` headings that have text. |
| `h1` | `["Intuitive Interpretation", "Compound Data Types"]` | The text of the first 5 `h1` headings, 200 characters each at most. |
| `h2Count` | `9` | Number of `h2` headings. |
| `h3Count` | `0` | Number of `h3` headings. |
| `wordCount` | `1158` | Words of visible text in the page body. |
| `imageCount` | `1` | Number of images. |
| `imagesMissingAlt` | `0` | Number of images with no `alt` attribute. An empty `alt`, which marks a decorative image, is fine. |
| `internalLinks` | `126` | Links that stay on the same site. |
| `externalLinks` | `78` | Links that go to another site. |
| `openGraphTags` | `["og:type", "og:title", "og:image"]` | The Open Graph tags that have a value. At most 50. |
| `twitterTags` | `["twitter:card", "twitter:site"]` | The Twitter card tags that have a value. At most 50. |
| `structuredDataTypes` | `["WebSite", "SearchAction"]` | The schema.org types found in the page's JSON-LD blocks. Only the type names are kept, not the data. At most 50. |

### How is the score worked out?

Every audited page starts at 100. Each issue found takes off its points, and the score never goes below 0. The `issues` field lists the codes, the most costly first.

| Issue code | Points | It is raised when |
|---|---|---|
| `noindex` | 20 | A robots meta tag or the X-Robots-Tag header says `noindex` or `none`: the page asks search engines to leave it out. |
| `title-missing` | 15 | The page has no title, or an empty one. |
| `description-missing` | 10 | The page has no meta description, or an empty one. |
| `h1-missing` | 10 | The page has no `h1` heading with text. |
| `canonical-missing` | 5 | The page declares no canonical address. |
| `viewport-missing` | 5 | The page has no viewport meta tag. |
| `title-too-short` | 5 | The title is under 30 characters. |
| `title-too-long` | 5 | The title is over 60 characters. |
| `images-missing-alt` | 5 | At least one image has no `alt` attribute. |
| `thin-content` | 5 | The page has fewer than 300 words of visible text. |
| `lang-missing` | 3 | The `html` tag has no `lang` attribute. |
| `description-too-short` | 3 | The meta description is under 70 characters. |
| `description-too-long` | 3 | The meta description is over 160 characters. |
| `h1-multiple` | 3 | The page has more than one `h1` heading. |
| `open-graph-missing` | 3 | The page has none of `og:title`, `og:description` and `og:image`. |
| `slow-response` | 3 | The page took more than 2,000 ms to arrive. |

Three more codes mark a page that was not audited. These rows have no score and no page-content fields.

| Issue code | It is raised when |
|---|---|
| `redirect` | The page answered with a redirect that was not followed: `followRedirects` is off, there were more than 5 redirects, or the next address is not one the Actor may read (see the limits below). |
| `http-error` | The page answered with an error such as 404, 403 or 500, or with any other status that is not a success. |
| `not-html` | The page is not HTML, for example a PDF or an image. |

The score is this Actor's own checklist score, not a Google metric, and it doesn't predict rankings. The length limits are common guidelines, not rules set by a search engine.

### How to use it

1. Open the Input tab and add the pages to audit under Page URLs, one address per line. The prefilled list works as it is.
2. Click Start.
3. Download the results as JSON, CSV or Excel, or read them through the Apify API.

| Input | Meaning |
|---|---|
| `urls` | The pages to audit. Each page becomes one row, in the order you list them. An address without `https://` is read as `https://`, so `example.com` works. A page listed twice is audited once. |
| `followRedirects` | On by default. When a page redirects, the Actor follows up to 5 redirects and audits the page it ends on: `url` is the address you listed and `finalUrl` is where it ended. Turn it off to get the redirect itself: a row with the status code, `redirectLocation` and the issue `redirect`. |
| `maxItems` | Stop after this many results. The default is 1000. There is one result per page, so this is also the most pages a run reports. |

The Actor reads about one page a second, so a list of several thousand pages needs a longer run timeout in the run options.

### How much does it cost?

You pay **$5.00 per 1,000 results**, with no monthly fee. A page that produces a row is a result, including one that answers with an error or a redirect. A page the Actor skips (see the limits below) produces no row and costs nothing. New Apify accounts get free monthly credit, so you can try it at no cost.

### Example output

A page that was audited:

```json
{
  "url": "https://www.python.org/",
  "finalUrl": "https://www.python.org/",
  "status": 200,
  "score": 84,
  "issues": ["title-too-short", "canonical-missing", "description-too-short", "h1-multiple"],
  "redirects": 0,
  "redirectLocation": null,
  "responseTimeMs": 91,
  "contentLength": 52966,
  "contentType": "text/html; charset=utf-8",
  "xRobotsTag": null,
  "title": "Welcome to Python.org",
  "titleLength": 21,
  "metaDescription": "The official home of the Python Programming Language",
  "metaDescriptionLength": 52,
  "canonical": null,
  "robotsMeta": null,
  "viewport": "width=device-width, initial-scale=1.0",
  "lang": "en",
  "hreflang": [],
  "h1Count": 5,
  "h1": ["Intuitive Interpretation", "Compound Data Types", "All the Flow You’d Expect", "Functions Defined", "Quick & Easy to Learn"],
  "h2Count": 9,
  "h3Count": 0,
  "wordCount": 1158,
  "imageCount": 1,
  "imagesMissingAlt": 0,
  "internalLinks": 126,
  "externalLinks": 78,
  "openGraphTags": ["og:type", "og:site_name", "og:title", "og:description", "og:image", "og:image:secure_url", "og:url"],
  "twitterTags": [],
  "structuredDataTypes": ["WebSite", "SearchAction"]
}
```

A page that answered with an error. The page-content fields stay empty and there is no score:

```json
{
  "url": "https://www.python.org/no-such-page-2026/",
  "finalUrl": "https://www.python.org/no-such-page-2026/",
  "status": 404,
  "score": null,
  "issues": ["http-error"],
  "redirects": 0,
  "redirectLocation": null,
  "responseTimeMs": 437,
  "contentLength": 33122,
  "contentType": "text/html; charset=utf-8",
  "xRobotsTag": null,
  "title": null,
  "titleLength": null,
  "metaDescription": null,
  "metaDescriptionLength": null,
  "canonical": null,
  "robotsMeta": null,
  "viewport": null,
  "lang": null,
  "hreflang": null,
  "h1Count": null,
  "h1": null,
  "h2Count": null,
  "h3Count": null,
  "wordCount": null,
  "imageCount": null,
  "imagesMissingAlt": null,
  "internalLinks": null,
  "externalLinks": null,
  "openGraphTags": null,
  "twitterTags": null,
  "structuredDataTypes": null
}
```

### Data source and limits

- **Where the data comes from:** the pages you list, and nothing else. The Actor sends one request for each page, one for each redirect it follows and one for the `robots.txt` file of each site. It never calls a third-party API and never follows links to other pages.
- **Only where the site allows it:** before it reads a page, the Actor reads that site's `robots.txt` and follows the [Robots Exclusion Protocol](https://www.rfc-editor.org/rfc/rfc9309). A page the file doesn't allow for this Actor is skipped. A site with no `robots.txt` (a 404 or 410 answer) allows everything. If the file can't be read for any other reason (a 401, 403 or server error, or no answer), the site is treated as closed and its pages are skipped. Every redirect is checked the same way.
- **Skipped pages have no row and are not charged.** The run log says why each one was skipped. The reasons are: the site's `robots.txt` doesn't allow the page or can't be read; it asks for more than 30 seconds between requests; the address is not a valid public web address (for example it points at a private network, has a port other than 80 or 443, or contains an email address or phone number); the site keeps answering that there are too many requests (HTTP 429); the address is a file that announces more than 50 MB, such as a video or an installer; or the request fails.
- **Polite pace:** about one request a second in all, and a site's own Crawl-delay is kept between requests to that site. The Actor sends a User-Agent that names it.
- **How fresh it is:** every page is read live when the run reaches it.
- **What is read:** the first 5 MB of the page's HTML, as the server sends it. Content that a script adds after the page loads isn't seen.
- **What is kept:** the facts in the table above, not the page text. Titles, descriptions and headings are cut at the lengths shown. Email addresses and phone numbers are removed from every text the Actor keeps.
- **Speed is a rough check.** `responseTimeMs` is one download of the HTML from a data center, which can differ from what your visitors see. It leaves out images, scripts and rendering.
- **Word counts are approximate.** Words are counted by splitting on spaces, so pages in languages written without spaces, such as Chinese or Japanese, count low.
- **Some sites turn data-center traffic away.** A site's firewall can answer the Actor with an error such as 403 although a browser gets the page. You get a row with `http-error`. The Actor doesn't try to get around it.
- **Not covered:** crawling a whole site, checking that links work, page speed in a browser, content built by JavaScript, backlinks and search rankings.
- **Independent tool:** it isn't affiliated with, endorsed by or sponsored by any site you audit or by any search engine.

The data is public. This Actor doesn't collect personal data, and it removes email addresses and phone numbers from any text it keeps.

### FAQ

**Can I run it on a schedule?** Yes. Use Apify Schedules to run it daily or weekly, for example to watch your key pages for a missing title or a stray `noindex`, and send the results to Google Sheets, Slack, a webhook or your own API.

**Can I audit a whole site?** Not on its own: it audits the addresses you list and doesn't follow links. Paste in the addresses from your sitemap or from a crawl you already have.

**Why is a page I listed missing from the results?** It was skipped, and the run log says why. The usual reasons are that the site's `robots.txt` doesn't allow it or can't be read, or that the site keeps answering "too many requests". The same address listed twice is audited once, and the run stops at `maxItems`.

**Is it legal?** It reads only public pages that you choose, only where the site's `robots.txt` allows it, at a polite rate, and keeps facts about each page rather than its text, with no personal data. Check that your own use fits each site's terms.

# Actor input Schema

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

The pages to audit, one address per entry. Each page becomes one result row, in the order you list them. An address without https:// is read as https://, so example.com works. Duplicates are audited once. A page whose site does not allow it in robots.txt is skipped, and a skipped page is not charged.

## `followRedirects` (type: `boolean`):

On: when a listed address redirects, the result describes the page it leads to, after up to 5 redirects. Off: the result describes the redirect itself, with its status code and where it points.

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

Stop after saving this many page reports. You pay per saved report. Raise it if your list is longer than 1,000 pages.

## Actor input object example

```json
{
  "urls": [
    "https://www.python.org/",
    "https://www.djangoproject.com/",
    "https://www.nasa.gov/"
  ],
  "followRedirects": true,
  "maxItems": 1000
}
```

# Actor output Schema

## `results` (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://www.python.org/",
        "https://www.djangoproject.com/",
        "https://www.nasa.gov/"
    ],
    "followRedirects": true,
    "maxItems": 1000
};

// Run the Actor and wait for it to finish
const run = await client.actor("jadeite_geometry/seo-onpage-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://www.python.org/",
        "https://www.djangoproject.com/",
        "https://www.nasa.gov/",
    ],
    "followRedirects": True,
    "maxItems": 1000,
}

# Run the Actor and wait for it to finish
run = client.actor("jadeite_geometry/seo-onpage-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://www.python.org/",
    "https://www.djangoproject.com/",
    "https://www.nasa.gov/"
  ],
  "followRedirects": true,
  "maxItems": 1000
}' |
apify call jadeite_geometry/seo-onpage-audit --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,jadeite_geometry/seo-onpage-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/ueSrIgM40raaCfyx8/builds/bhu2JrQEZuFiQHPzr/openapi.json
