# Technical SEO Audit (bulk, with issues) (`steadydata/technical-seo-audit`) Actor

Audit up to 500 pages per run on the technical basics and get a ranked list of issues per page: noindex, title and description length, canonical, headings, images without alt text, structured data, Open Graph, viewport and redirects. Respects robots.txt. Pay per page.

- **URL**: https://apify.com/steadydata/technical-seo-audit.md
- **Developed by:** [Steadydata Team](https://apify.com/steadydata) (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

from $21.00 / 1,000 page auditeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Technical SEO Audit (bulk, with issues per page)

Audit **up to 500 pages per run** on the technical basics and get, per page, **a ranked
list of what is actually wrong**: a forgotten noindex, a missing or overlong title, no
canonical, no H1 or six of them, images without alt text, thin content, no viewport, no
structured data. Plus the measured fields behind every verdict. **Pages that their own
robots.txt forbids are skipped and never charged.**

### Why this scraper

- **It tells you what to fix, not just what it found.** Thirty columns of raw fields are
  a dataset; `issues` is a to-do list. Every failed check comes back with a code, a
  severity (critical, warning, info), a sentence and the measured value, sorted worst
  first. Filter on `severity = critical` and you have your morning.
- **The noindex check alone pays for a run.** A page nobody can find because a staging
  `noindex` shipped to production is the most expensive SEO mistake there is, and it is
  invisible in a browser.
- **It respects robots.txt.** The target's own robots.txt is read first, with the rules
  search engines actually use, and a page it forbids comes back as a free
  `ROBOTS_DISALLOWED` record. An audit that reports on robots meta tags while ignoring
  robots.txt would not be worth much.
- **Your limits, not ours.** Title and description length limits are inputs, so the
  audit matches your own house style instead of a number we picked.
- **Sitemaps found for free.** Every row lists the sitemaps declared in the site's
  robots.txt, which is usually the fastest way to find the rest of the pages.
- **Only delivered pages are charged.** Robots refusals, dead URLs, timeouts and bot
  walls are free error records. No start fee.

### Who this is for

SEO teams and agencies auditing a site or a portfolio of client sites on a schedule.
Developers checking a release for the mistakes that only show up in the HTML. Anyone who
needs the technical basics of hundreds of pages in one table.

### Who this is not for

**Read this before you buy.** This actor fetches the HTML of each URL you give it and
does not run JavaScript, so a page that renders its title and content in the browser
shows up emptier here than it looks to a visitor. It also audits **only the URLs you
supply**: it does not crawl a site to discover pages (the `sitemapsInRobots` field points
you at the sitemap so you can feed those URLs in). Page speed, Core Web Vitals and
Lighthouse scores are not included; those need a browser and Google's own API.

### Input example

```json
{
    "urls": ["https://apify.com", "wordpress.org", "https://example.com/product/x"],
    "language": "en-US,en;q=0.9",
    "titleMaxLength": 60,
    "descriptionMaxLength": 160
}
```

A domain is audited on its home page; a full URL is audited as given.

### Output example

```json
{
    "url": "https://example.com/pagina",
    "finalUrl": "https://example.com/pagina",
    "statusCode": 200,
    "issueCount": 3,
    "issues": [
        {"code": "noindex", "severity": "critical", "message": "The page tells search engines not to index it", "value": "noindex, nofollow"},
        {"code": "canonical-missing", "severity": "warning", "message": "No canonical link", "value": null},
        {"code": "images-without-alt", "severity": "warning", "message": "4 of 9 images have no alt text", "value": 4}
    ],
    "title": "Een nette titel voor deze pagina",
    "titleLength": 32,
    "metaDescription": "Een nette beschrijving van deze pagina.",
    "metaDescriptionLength": 39,
    "canonical": null,
    "robotsMeta": "noindex, nofollow",
    "isIndexable": false,
    "h1": ["Kop een"],
    "h1Count": 1,
    "h2Count": 2,
    "wordCount": 251,
    "imageCount": 9,
    "imagesWithoutAlt": 4,
    "internalLinks": 24,
    "externalLinks": 3,
    "hreflang": ["en", "nl"],
    "structuredDataTypes": ["Organization", "PostalAddress"],
    "openGraph": ["image", "title"],
    "hasTwitterCard": true,
    "hasViewport": true,
    "htmlLang": "nl",
    "charset": "utf-8",
    "isHttps": true,
    "redirected": false,
    "contentEncoding": "br",
    "pageSizeBytes": 81234,
    "responseTimeMs": 412,
    "robotsRule": "allow /",
    "sitemapsInRobots": ["https://example.com/sitemap.xml"],
    "status": "ok"
}
```

A page its own robots.txt forbids produces an error record instead, and is **not** charged:

```json
{
    "input": "https://example.com/private/page",
    "status": "error",
    "errorCode": "ROBOTS_DISALLOWED",
    "error": "https://example.com/private/page is forbidden by its own robots.txt (disallow /private); skipped and not charged"
}
```

Error codes: `INVALID_URL`, `ROBOTS_DISALLOWED`, `FETCH_FAILED`, `BLOCKED`. `INPUT_TRUNCATED` appears once when your input is longer than this actor accepts.

### Related actors from steadydata

- [website-tech-stack](https://apify.com/steadydata/website-tech-stack): what the site is built with
- [domain-dns-ssl-report](https://apify.com/steadydata/domain-dns-ssl-report): domain and certificate health
- [rss-feed-reader](https://apify.com/steadydata/rss-feed-reader): the site's own feed, if it publishes one

### Pricing

Pay per event: one `page-audited` event per delivered page. No charge for robots
refusals, dead URLs, timeouts or bot walls, and no start fee.

**Free Apify plan:** this actor delivers up to 25 rows per run for accounts on the Apify free
plan, and then stops with a message. That limit is set by us, not by Apify. It exists so the
actor keeps paying for itself for the people who do pay. Any paid Apify plan runs it at full
size, billed per delivered row, with failed rows never charged.

**Reviews:** if this actor saves you time, a short review on this page is the one thing that
helps most. Ratings are what other buyers look at first, and we have no other way to ask.

### FAQ

**Which issues can it report?**
`noindex`, `http-error`, `title-missing`, `title-too-long`, `title-too-short`,
`description-missing`, `description-too-long`, `canonical-missing`, `h1-missing`,
`h1-multiple`, `images-without-alt`, `thin-content`, `viewport-missing`, `lang-missing`,
`structured-data-missing`, `open-graph-missing` and `no-https`.

**Why does `imagesWithoutAlt` count images with `alt=""`?**
Because an empty alt is only correct for decorative images, and at scale it is far more
often an oversight. The count and the total are both in the row, so you can judge.

**Why does one page report 117 images without alt text?**
Because that is what the HTML as served contains. Sites that lazy-load images often set
the alt attribute later in JavaScript, which this actor does not run, so treat a very high
count on an image-heavy page as "check this by hand" rather than as 117 separate mistakes.
The total count is in the row so you can see the ratio.

**Are 251 words really thin content?**
The `thin-content` issue is `info`, not a verdict: for a product page it is normal, for
an article it is a signal. Filter it out if it does not apply to your pages.

**Can it crawl my whole site?**
No. It audits the URLs you give it. Use `sitemapsInRobots` from the first run to collect
the rest of your URLs, then feed those in.

**Is personal data collected?**
No. These are properties of a web page.

**What happens when a check is wrong?**
Every issue carries the measured value, so a disagreement is verifiable rather than a
matter of opinion. If a rule is genuinely wrong, it gets fixed.

# Changelog

This Actor's version history is a separate document: https://apify.com/steadydata/technical-seo-audit/changelog.md

# Actor input Schema

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

Page URLs or domains, one per row, up to 500. A domain is audited on its home page. A URL that its own robots.txt forbids is skipped and not charged.

## `language` (type: `string`):

Language header sent with the request.

## `titleMaxLength` (type: `integer`):

A title longer than this is reported as an issue. Google truncates around 60 characters.

## `descriptionMaxLength` (type: `integer`):

A meta description longer than this is reported as an issue. Google truncates around 160.

## Actor input object example

```json
{
  "urls": [
    "https://apify.com",
    "wordpress.org"
  ],
  "language": "en-US,en;q=0.9",
  "titleMaxLength": 60,
  "descriptionMaxLength": 160
}
```

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

// Run the Actor and wait for it to finish
const run = await client.actor("steadydata/technical-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://apify.com",
        "wordpress.org",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("steadydata/technical-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://apify.com",
    "wordpress.org"
  ]
}' |
apify call steadydata/technical-seo-audit --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,steadydata/technical-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/nLIiZESDBFDhMPOMz/builds/6CmndLFFXaOZlOahH/openapi.json
