# SEO Audit Tool: Titles, Meta Tags, Headings and Links API (`rowcraft/seo-audit-tool`) Actor

Audit the on-page SEO of any list of pages or websites: titles, meta descriptions, headings, canonicals, indexing, links, images and structured data, with a score and the issues found per page. robots.txt obeyed, no personal data, pay only per audited page.

- **URL**: https://apify.com/rowcraft/seo-audit-tool.md
- **Developed by:** [Rowcraft](https://apify.com/rowcraft) (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 $1.50 / 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.
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

## SEO Audit Tool: Titles, Meta Tags, Headings and Links API

Run an on-page **SEO audit** on a list of web pages, or on a whole domain. Paste page addresses (or just domains) and get one row per page with its title, meta description, headings, canonical address, indexing directives, language, social tags, structured data types, word count, links and images, plus a list of issue codes and a score from 0 to 100, as JSON, CSV or Excel. Turn on **Follow internal links** and a domain becomes a small site audit, limited to the number of pages you choose.

Every page is read politely and once: robots.txt first, then one request for the page. Nothing is collected about people. The issue codes and the score follow rules that are written down in this README, so you can check any result against the fields of the row.

![Input form of SEO Audit Tool: Titles, Meta Tags, Headings and Links API](https://api.apify.com/v2/key-value-stores/7hbQFQdGq61v2OZ2h/records/seo-audit-tool-input.png)

### What you get

One row per page that was read and audited:

- **Identity and fetch:** the address you gave (`url`), the final address after redirects (`finalUrl`), HTTP status, number of redirects, response time in milliseconds, content type and page size
- **Title and meta description:** the text and its length in characters (an e-mail address or phone number inside is masked)
- **Headings:** the first H1, the number of H1 and H2 headings
- **Indexing:** canonical address and whether it points to the page itself, the robots and googlebot meta directives, the `X-Robots-Tag` header, and one `indexable` flag with the reason when it is false
- **Language and alternates:** the `lang` of the page and the language codes of the `hreflang` alternates as text (for example `de, fr, x-default`) and their number in `hreflangCount`
- **Social tags:** Open Graph title, description and image, and the Twitter card type
- **Structured data:** the Schema.org types found in JSON-LD and microdata, as text (for example `Organization, WebSite`; the type names only, never the data inside)
- **Content and links:** word count, number of internal links, external links and links marked nofollow, number of images and images without an alt attribute
- **Result:** `issues` (the short codes as text, for example `missing_h1, title_too_long`), `issueCount`, a `score` and a `seoHash` that changes when the SEO of the page changes

The run also writes a `COVERAGE` record with one line for every page you asked for (ok, blocked, robots_disallowed, not_found, not_html, skipped, error or not_checked, with the reason) and a `RUN_SUMMARY` with counts, the most frequent issues and the average score.

### Why this actor

- **Every issue is explained.** The code list, the thresholds and the score weights are printed below. There is no hidden calculation: a page that scores 83 lost 17 points for issues you can see in its row.
- **A domain or a list, your choice.** Audit the exact pages you care about, or follow internal links from a start page up to a page limit per site. Links with a query string, files such as PDFs and images, and links marked nofollow are not followed.
- **Pay only for audited pages.** Pages behind an anti-bot page, disallowed by robots.txt, missing, not HTML, invalid or repeated in your list are never charged.
- **Polite by design.** robots.txt is read and obeyed on every host before any other request, a Crawl-delay is respected, the default is one request per second, and the actor identifies itself with a clear user agent.
- **Change tracking built in.** Turn on **Only new rows since the last run** and a scheduled run delivers a page again only when its SEO fingerprint changed: a new title, a removed canonical, a noindex that appeared.
- **Flat rows, stable columns.** Empty values are `null`, never missing, and lists such as the issue codes are text separated by commas, not arrays, so a table shows them as they are and the CSV and Excel exports keep the same columns from run to run.

### How to use it

1. Open the Actor and paste page addresses or domains into **Pages or websites to audit**, one per line. A column copied from a spreadsheet works, and so do entries separated by commas.
2. For a first try keep the list short, or set **Maximum pages delivered** to a small number.
3. Optional: turn on **Follow internal links** and set **Maximum pages per site** to audit a domain beyond the pages you listed.
4. Click **Start**. The status line shows progress; the run stops cleanly at your maximum or at the charge limit you set in the run options.
5. Open the **Dataset** tab. The **Overview**, **Issues**, **Indexing and speed** and **Content and links** views show the main columns. Sort by `score` to see the weakest pages first. Download JSON, CSV, Excel, XML or HTML, or send the rows to Google Sheets, Slack, Make, n8n or Zapier with the Integrations tab.
6. Open the `COVERAGE` record in the key-value store to see which pages could not be audited and why.
7. To watch pages over time, run the actor on a schedule with **Only new rows since the last run** turned on.

### Input

| Field | What it does |
|---|---|
| `startUrls` | Page addresses (`https://example.com/blog/post`) or domains (`example.com`), one per line. Each entry is audited as the page it points to; a domain means its home page. The `#fragment` is ignored. Up to 100,000 per run. |
| `crawl` | Off by default: only the pages you list are audited. On: internal links of each audited page are followed too (same site, `www.` ignored, robots.txt obeyed). |
| `maxPagesPerSite` | Used only with `crawl`. The most pages read per site, counting the pages you listed. Default 20, up to 1,000. |
| `maxItems` | Stop after this many delivered rows. 0 = no limit. |
| `onlyNew`, `stateName` | Deliver a page again only when its SEO fingerprint changed since the last run with the same state name |
| `notifyUrl`, `notifyWhenEmpty` | A webhook that receives one message when the run ends; see **Automation** below |
| `requestIntervalMs` | Pause between requests, default 1000 ms, minimum 250 ms. A longer Crawl-delay in a site's robots.txt wins. |

Example: three pages, one row each.

```json
{
  "startUrls": ["https://www.python.org/", "https://developer.mozilla.org/en-US/", "https://www.gnu.org/"]
}
```

Example: audit up to 30 pages of one site, starting from its home page.

```json
{
  "startUrls": ["https://example.com/"],
  "crawl": true,
  "maxPagesPerSite": 30
}
```

Example: a weekly check of key pages that reports only the pages whose SEO changed, with a Slack message.

```json
{
  "startUrls": ["https://example.com/", "https://example.com/pricing", "https://example.com/blog"],
  "onlyNew": true,
  "stateName": "key-pages",
  "notifyUrl": "https://hooks.slack.com/services/..."
}
```

### Output

42 fields per row. Empty values are `null`, never missing, so CSV and Excel columns are stable.

| Field | Type | Example |
|---|---|---|
| `id` | string | `https://www.python.org/about` |
| `site` | string | python.org |
| `depth` | integer | 0 |
| `url` | string | `https://www.python.org/about/` |
| `finalUrl` | string | `https://www.python.org/about/` |
| `statusCode` | integer | 200 |
| `redirectCount` | integer | 0 |
| `responseTimeMs` | integer | 412 |
| `contentType` | string | text/html; charset=utf-8 |
| `pageSizeBytes` | integer | 48210 |
| `title` | string | About Python | Python.org |
| `titleLength` | integer | 25 |
| `metaDescription` | string | The official home of the Python Programming Language |
| `metaDescriptionLength` | integer | 52 |
| `h1` | string | About Python |
| `h1Count` | integer | 1 |
| `h2Count` | integer | 4 |
| `canonical` | string | `https://www.python.org/about/` |
| `canonicalIsSelf` | boolean | True |
| `metaRobots` | string | noindex, nofollow |
| `xRobotsTag` | string | noindex |
| `indexable` | boolean | True |
| `indexabilityReason` | string | canonical_to_other_url |
| `lang` | string | en |
| `hreflang` | string | de, fr, x-default |
| `hreflangCount` | integer | 3 |
| `ogTitle` | string | About Python |
| `ogDescription` | string | The official home of the Python Programming Language |
| `ogImage` | string | `https://www.python.org/static/opengraph-icon-200x200.png` |
| `twitterCard` | string | summary |
| `structuredDataTypes` | string | Organization, WebSite |
| `wordCount` | integer | 842 |
| `internalLinks` | integer | 62 |
| `externalLinks` | integer | 11 |
| `nofollowLinks` | integer | 2 |
| `images` | integer | 14 |
| `imagesWithoutAlt` | integer | 3 |
| `issues` | string | missing_meta_description, images_without_alt |
| `issueCount` | integer | 2 |
| `score` | integer | 83 |
| `seoHash` | string | a1b2c3d4e5f6 |
| `checkedAt` | string | 2026-10-14T20:00:00.000Z |

#### Example rows

Two real rows from a run (long texts shortened here, complete in the dataset):

```json
[
  {
    "id": "https://www.python.org/",
    "site": "python.org",
    "depth": 0,
    "url": "https://www.python.org/",
    "finalUrl": "https://www.python.org/",
    "statusCode": 200,
    "redirectCount": 0,
    "responseTimeMs": 35,
    "contentType": "text/html; charset=utf-8",
    "pageSizeBytes": 51934,
    "title": "Welcome to Python.org",
    "titleLength": 21,
    "metaDescription": "The official home of the Python Programming Language",
    "metaDescriptionLength": 52,
    "h1": "Functions Defined",
    "h1Count": 5,
    "h2Count": 9,
    "canonical": null,
    "canonicalIsSelf": null,
    "metaRobots": null,
    "xRobotsTag": null,
    "indexable": true,
    "indexabilityReason": null,
    "lang": "en",
    "hreflang": null,
    "hreflangCount": 0,
    "ogTitle": "Welcome to Python.org",
    "ogDescription": "The official home of the Python Programming Language",
    "ogImage": "https://www.python.org/static/opengraph-icon-200x200.png",
    "twitterCard": null,
    "structuredDataTypes": "SearchAction, WebSite",
    "wordCount": 942,
    "internalLinks": 64,
    "externalLinks": 63,
    "nofollowLinks": 0,
    "images": 1,
    "imagesWithoutAlt": 0,
    "issues": "title_too_short, meta_description_too_short, multiple_h1, missing_canonical",
    "issueCount": 4,
    "score": 84,
    "seoHash": "58754deac877",
    "checkedAt": "2026-10-14T17:40:17.659Z"
  },
  {
    "id": "https://nodejs.org/en",
    "site": "nodejs.org",
    "depth": 0,
    "url": "https://nodejs.org/en",
    "finalUrl": "https://nodejs.org/en",
    "statusCode": 200,
    "redirectCount": 0,
    "responseTimeMs": 79,
    "contentType": "text/html; charset=utf-8",
    "pageSizeBytes": 531732,
    "title": "Node.js — Run JavaScript Everywhere",
    "titleLength": 35,
    "metaDescription": "Node.js® is a free, open-source, cross-platform JavaScript runtime environment that lets developers create servers, web apps, command lin…",
    "metaDescriptionLength": 157,
    "h1": "Run JavaScript Everywhere",
    "h1Count": 1,
    "h2Count": 0,
    "canonical": "https://nodejs.org/en",
    "canonicalIsSelf": true,
    "metaRobots": "index, follow",
    "xRobotsTag": null,
    "indexable": true,
    "indexabilityReason": null,
    "lang": "en-GB",
    "hreflang": "x-default, en, es, fr, id, ja, ko, pt-br, pt, ro, ta, tr, uk, zh-cn, zh-tw",
    "hreflangCount": 15,
    "ogTitle": "Node.js — Run JavaScript Everywhere",
    "ogDescription": "Node.js® is a free, open-source, cross-platform JavaScript runtime environment that lets developers create servers, web apps, command lin…",
    "ogImage": "https://nodejs.org/en/next-data/og/announcement/Node.js%20%E2%80%94%20Run%20JavaScript%20Everywhere",
    "twitterCard": "summary",
    "structuredDataTypes": null,
    "wordCount": 409,
    "internalLinks": 11,
    "externalLinks": 27,
    "nofollowLinks": 0,
    "images": 0,
    "imagesWithoutAlt": 0,
    "issues": null,
    "issueCount": 0,
    "score": 100,
    "seoHash": "9e55922649a9",
    "checkedAt": "2026-10-14T17:40:19.840Z"
  }
]
```

Results can be downloaded as JSON, CSV, Excel, XML or HTML from the Dataset tab, or read through the Apify API.

![Results of SEO Audit Tool: Titles, Meta Tags, Headings and Links API in the Apify dataset view](https://api.apify.com/v2/key-value-stores/7hbQFQdGq61v2OZ2h/records/seo-audit-tool-results.png)

#### The issue codes and the score

The score starts at 100 and loses the points below for every issue found, never going under 0. Each code can be read off the fields of the row (for example `title_too_long` means `titleLength` is above 60). The thresholds are common rules of thumb used by SEO tools, not rules or ranking factors published by Google or any other search engine; treat them as a checklist, not as a verdict.

| Code | Reported when | Points |
|---|---|---|
| `missing_title` | there is no title element, or it is empty | 20 |
| `noindex` | a robots or googlebot meta tag, or the `X-Robots-Tag` header, says `noindex` or `none` | 20 |
| `missing_h1` | the page body has no H1, or no H1 with text | 15 |
| `missing_meta_description` | there is no meta description, or it is empty | 12 |
| `canonical_to_other_url` | the canonical address differs from the final address (a trailing slash and a `#fragment` are ignored; `www.` and `http` against `https` are not) | 10 |
| `thin_content` | the body has fewer than 300 words | 8 |
| `not_https` | the final address starts with `http://` | 8 |
| `title_too_long` | the title is longer than 60 characters | 5 |
| `title_too_short` | the title is shorter than 30 characters | 5 |
| `multiple_h1` | the body has more than one H1 | 5 |
| `missing_lang` | the `html` tag has no `lang` attribute | 5 |
| `missing_og_tags` | none of `og:title`, `og:description` and `og:image` is present | 5 |
| `images_without_alt` | at least one image has no `alt` attribute (an empty `alt=""` marks a decorative image and is fine) | 5 |
| `slow_response` | the response headers took more than 1,800 ms to arrive | 5 |
| `meta_description_too_long` | the meta description is longer than 160 characters | 4 |
| `redirect_chain` | two or more redirects before the final page | 4 |
| `meta_description_too_short` | the meta description is shorter than 70 characters | 3 |
| `missing_canonical` | no canonical in the page or in the `Link` header | 3 |
| `invalid_json_ld` | a JSON-LD block is not valid JSON | 3 |
| `incomplete_og_tags` | some, but not all, of `og:title`, `og:description` and `og:image` are present | 2 |

How the main fields are read:

- **Title:** the first title element of the HTML, as a browser reads it. Entities are decoded and white space is collapsed. A title inside an inline SVG does not count.
- **Indexable:** `false` when a `noindex` or `none` directive applies to us (a robots or googlebot meta tag, or an `X-Robots-Tag` header that names no bot, googlebot or rowcraft), or when the canonical points to another address. `indexabilityReason` lists the causes: `noindex_meta`, `noindex_header`, `canonical_to_other_url`. Pages that robots.txt blocks are not read at all, so they are never in the dataset.
- **Words:** the words of the HTML body without scripts, styles, templates and noscript fallbacks. Menus and footers are included, and text hidden with CSS cannot be told apart. Languages written without spaces (Chinese, Japanese, Thai) are not counted meaningfully.
- **Links:** different addresses, not tags. A link to the same address twice counts once, a link to the page itself counts as internal, and links with only a `#fragment` are skipped. Internal means the same host with `www.` ignored; another subdomain is external. `mailto:`, `tel:`, `sms:` and every other non-web link is skipped completely and not counted.
- **Response time:** from sending the request to receiving the response headers of the final page, in milliseconds. It includes setting up the connection to a new host, and it depends on where the actor runs.
- **SEO fingerprint (`seoHash`):** a hash of the final address, status, title, meta description, H1 and the heading counts, canonical, robots directives, language, hreflang, social tags, structured data types and the issue codes except `slow_response`. Word and link counts, sizes and times are left out, so a changing comment count or a slow server does not look like an SEO change.

### Pricing

**Pay per result.** You pay for each audited page delivered to your dataset, nothing else: no subscription, no minimum, no charge for pages that could not be read (anti-bot page, robots.txt, not found, not HTML, errors), for invalid or repeated entries, or for pages skipped by **Only new rows** because nothing changed.

| Your Apify plan | Price per 1,000 rows | Price per row |
|---|---|---|
| Free plan | **$2.00** | $0.002 |
| Starter plan | **$1.80** | $0.0018 |
| Scale plan | **$1.70** | $0.0017 |
| Business plan and above | **$1.50** | $0.0015 |

Example: 1,000 pages cost $2.00 on the Free plan and $1.50 on the Business plan. The Apify platform usage of a run (compute and storage) is billed by Apify separately, as for every Actor; the actor runs in 256 MB and spends most of its time waiting between requests.
A start event of $0.00005 is charged once per run (Apify default).
**Cap your spend:** set *Maximum total charge* in the run options. The actor stops cleanly when the cap is reached and you keep everything delivered until then. The run summary (`RUN_SUMMARY` in the key-value store) says why a run stopped.

### Automation: only new rows, notifications, schedules

**Only new rows since the last run.** Turn on `onlyNew` and the actor remembers, for every page it delivered to you, the SEO fingerprint it had (in a key-value store named `rowcraft-seo-audit-tool-state` in your Apify account, holding a short hash of the address and the fingerprint, not the page). The next run with the same `stateName` delivers a page again only when its fingerprint changed; pages with the same fingerprint are skipped and not charged. A change in the word count, the number of links or the response time does not count as a change. The first run delivers every page it can read. Use a different `stateName` for each list so they keep separate memories. With **Follow internal links** on, unchanged pages are still read, so that new links are found.

**Notification when the run ends.** Put a webhook address in `notifyUrl` and the actor sends one JSON POST with the number of delivered rows, links to the dataset (JSON and CSV) and the run. It works with Slack and Discord incoming webhooks (the message is in `text` and `content`), with Make, n8n and Zapier catch hooks, and with your own endpoint. By default it is sent only when at least one row was delivered; `notifyWhenEmpty` sends it every time.

**Schedules.** Create a schedule in Apify Console (daily, weekly, monthly or cron) with `onlyNew` on, and you get a feed of the pages of your list whose SEO changed, for the price of those rows only.

**Integrations.** Send the results to Make, n8n, Zapier, Google Sheets, Slack, Google Drive, Airtable or any webhook from the Integrations tab of the Actor, with no code.

**API, SDKs and AI agents.** Every run is available through the Apify API and the official JavaScript and Python clients; the Actor can also be called by AI agents through the Apify MCP server. The example input in the API tab is a working call.

### Use cases

- **Quick audit of a new page or a whole section:** paste the addresses before a launch and fix the pages with the lowest score first. Missing H1, a noindex left from staging and a canonical pointing to the wrong address show up at once.
- **Agency reports:** audit the key pages of each client every month and export the Overview view to a spreadsheet. The issue codes are the same on every run, so months can be compared.
- **Migration checks:** list the old addresses and compare the `finalUrl`, `statusCode`, `redirectCount` and `canonical` fields before and after a move to a new domain or a new CMS. Redirect chains and canonicals that still point to the old host are listed.
- **Regression alerts:** schedule the actor on your money pages with **Only new rows** and a webhook. You hear about a changed title, a noindex or a lost canonical the day it happens, not at the next monthly report.
- **Competitor pages:** read the titles, meta descriptions, headings, structured data types and word counts of the pages that rank for your keywords, in one table.
- **Content inventory:** follow internal links from a start page and sort by `wordCount` to find thin pages, or by `imagesWithoutAlt` to find pages that need alt text.
- **Lead research for SEO services:** run a list of prospect domains and open with concrete findings, such as pages without a meta description or without HTTPS.

Ready-made examples, each with the input filled in (open one and press Start, or save it as your own task):

- [SEO audit of a list of pages: titles, meta tags, issues](https://apify.com/rowcraft/seo-audit-tool/examples/seo-audit-of-a-page-list)
- [Audit a small website by following its internal links](https://apify.com/rowcraft/seo-audit-tool/examples/audit-a-small-website-by-following-links)
- [Indexing and canonical check of key pages, with change alerts](https://apify.com/rowcraft/seo-audit-tool/examples/indexing-and-canonical-check-of-key-pages)

### What this actor does not do

- Run JavaScript. It reads the HTML the server sends, as a basic crawler does. Pages that build their content in the browser (single page apps without server rendering) show only what is in that first HTML, so their word count and headings can be low.
- Render or measure page speed. `responseTimeMs` is the time until the headers arrived, not a Core Web Vitals measurement.
- Compare pages with each other. Duplicate titles and duplicate meta descriptions are not flagged inside a run, because each row is delivered as soon as it is audited; sort or group the dataset by `title` to find them.
- Check every link on a page. It counts internal, external and nofollow links; checking whether each link works is a different job.
- Look at keyword rankings, backlinks, search volume or traffic. No search engine or third-party database is queried.
- Collect contacts: no e-mail addresses, phone numbers, names of authors or social profiles, and no page text apart from the title, the meta description, the first H1 and the Open Graph title and description.
- Use proxies, solve CAPTCHAs, log in, fill forms or accept cookie banners. A page behind an anti-bot page is reported as `blocked` and not charged.

### Limits and fair use

- **One request per second** by default (minimum 250 ms), across all sites of the run, and never faster than a site's own Crawl-delay (up to 10 seconds is applied). A page takes one request, plus one robots.txt request per site and one per redirect to a new host. In our test runs, eight different sites took about 20 seconds and a 12-page crawl of one site about 19 seconds, which is between 1,000 and 2,000 pages an hour at the default.
- **Page limit per site.** With **Follow internal links** the actor stops at **Maximum pages per site** (default 20, up to 1,000) and says so in the run summary and the warnings. Pages you listed yourself are always read, and count towards the limit.
- **What a crawl follows.** Same-site links in document order, breadth first. Not followed: links marked nofollow, links with a query string, links to files that are not pages (PDF, images, archives, scripts, feeds) and links that redirect to another site. Add such addresses to the list yourself if you want them audited.
- **Anti-bot pages.** A site that answers with an anti-bot or access-denied page (Cloudflare, Akamai, DataDome and similar, or HTTP 401, 403, 429) is reported as `blocked` and not charged. After three such pages in a row on one site the rest of that site is not requested, and after ten in a row anywhere the run stops and says why in the run summary.
- **robots.txt is obeyed** for every address the visit touches, including redirect targets and followed links. If robots.txt disallows a page it is `robots_disallowed`; if robots.txt answers with a server error the site is not read, as the robots.txt standard ([RFC 9309](https://www.rfc-editor.org/rfc/rfc9309)) requires.
- **Page size:** the first 5 MB of a page are read; larger pages are cut there and counted in the run summary. Answers that are not HTML (PDF, images, JSON) are not read at all and are listed as `not_html`.
- **Up to 100,000 entries per run.** The run stops 90 seconds before its timeout with the summary, the coverage and the **Only new rows** memory saved; for long lists raise the timeout or split the list.

### Data source and licence

The data is what each page sends to every visitor: the response status and headers, and the HTML of the page. The actor reads the page once, as a browser does on the first visit, after checking robots.txt, and keeps only the audited values: tag texts, counts, flags and issue codes. The page body, images, cookies and the data inside structured data blocks are not stored or delivered.

You choose the pages, so you are responsible for auditing only pages you may audit. The terms of each site cannot be checked one by one by the actor; audit your own sites, your clients' sites with their agreement, and public pages for the purposes the site allows. The thresholds and the score are Rowcraft's own rules of thumb and are described above.

Rowcraft is independent and not affiliated with or endorsed by Google, Bing or any other search engine, or by the websites you audit. The names of standards and products appear only to say what a field contains, for example Open Graph ([ogp.me](https://ogp.me/)) and Schema.org ([schema.org](https://schema.org/)).

### Privacy: no personal data

Rowcraft actors are built not to collect personal data:

- Only the audited values of a page are delivered. There are no author names, e-mail addresses, phone numbers or social profiles in the output.
- `mailto:`, `tel:` and `sms:` links are skipped completely: they are not read, not counted and not delivered.
- An e-mail address or phone number inside a title, a meta description, a heading or an Open Graph text is replaced by a mask before the row is saved. Addresses in the URL fields are cleaned in the same way.
- A title or a heading is page text, so on a personal site it can still contain a name that the owner chose to publish; the actor does not look for names and cannot remove them from free text.
- The actor sends no cookies, fills no forms and never logs in.

You are responsible for using the data in line with the law that applies to you.

### Other Rowcraft actors

All Rowcraft actors read official or public sources, deliver flat rows without personal data and support only-new runs and notifications:

- [Ashby Jobs Scraper – Job Postings with Salary Ranges](https://apify.com/rowcraft/ashby-jobs-scraper) — Job postings from Ashby job boards (jobs.ashbyhq.com) by company: title, department, team, locations with country, employment and workplace type, published salary range, full text.
- [Broken Link Checker – Find 404s and Dead Links API](https://apify.com/rowcraft/broken-link-checker) — Pay per crawled page, not per link: crawl a site within robots.txt and check every link on each page for 404s, 5xx errors, DNS and TLS failures and redirects.
- [Career Site Jobs Scraper: Greenhouse, Lever, Ashby, Workday](https://apify.com/rowcraft/career-site-jobs-scraper) — Job listings from the career pages of the companies you choose on Greenhouse, Lever, Ashby, Workday, Workable, Personio, Recruitee, Teamtailor and Gem, in one run and one format: title, locations, department, salary when published, full text.
- [Domain Email and DNS Checker API: MX, SPF, DMARC, Providers](https://apify.com/rowcraft/domain-email-dns-checker) — Find which email provider, sending services and DNS host each domain uses, from public DNS records only.
- [SIRENE France Company Scraper: SIREN, NAF, Revenue, Status](https://apify.com/rowcraft/france-company-register) — French companies from the official Recherche d'entreprises API (INSEE SIRENE + INPI): SIREN and SIRET, legal form, NAF activity, head-office address, employee band, revenue and net income, labels, creation date and VAT number.
- [Gem Jobs Scraper – Job Board Listings from jobs.gem.com](https://apify.com/rowcraft/gem-jobs-scraper) — Job listings from Gem job boards (jobs.gem.com) by company: title, department, offices with country, employment type, remote, hybrid or on-site, requisition ID, publication date and full ad text, from the public Gem Job Board API.
- [Google Play Apps Scraper: App Details, Installs, Ratings](https://apify.com/rowcraft/google-play-apps-scraper) — Public Google Play app details as flat rows: installs, rating, number of ratings, price, version, last update, minimum Android, ads and in-app purchases.
- [Greenhouse Jobs Scraper – Job Listings by Company Board](https://apify.com/rowcraft/greenhouse-jobs-scraper) — Job listings from Greenhouse job boards by company: title, departments, offices, publication date, full ad text, published pay ranges.
- [Himalayas Jobs Scraper: Remote Jobs API with Salaries](https://apify.com/rowcraft/himalayas-jobs-scraper) — Remote jobs from Himalayas through its public jobs API: title, company, salary range, seniority, categories, the countries and time zones open to candidates, full description and job link.
- [KRS Poland Companies Scraper: Register Data, PKD, Status](https://apify.com/rowcraft/krs-poland-companies) — Polish companies from the official KRS register API of the Ministry of Justice: name, KRS, NIP and REGON numbers, legal form, status, address, PKD activity codes, share capital, branches and registration dates.
- [LEI Records Scraper: Legal Entity Identifier Lookup API](https://apify.com/rowcraft/lei-records-scraper) — Legal Entity Identifier (LEI) records from the Global LEI Index, read through the public GLEIF API: legal name, legal form, register number, addresses, status, renewal date and parent companies.
- [Lever Jobs Scraper – Job Postings from jobs.lever.co](https://apify.com/rowcraft/lever-jobs-scraper) — Job postings from Lever career pages (jobs.lever.co) by company: title, team, department, location, commitment, workplace type, salary range when published, description, requirements and benefits.
- [Norway Company Register Scraper: Brønnøysund Business Data](https://apify.com/rowcraft/norway-company-register-scraper) — Norwegian companies from the Central Coordinating Register for Legal Entities (Brønnøysund, NLOD open data): organisation number, legal form, industry, employees, share capital, addresses, website, VAT and bankruptcy status.
- [Personio Jobs Scraper: Job Listings, Salaries, Departments](https://apify.com/rowcraft/personio-jobs-scraper) — Job ads from any company's Personio careers feed: title, department, office, employment type, schedule, seniority, salary when published, keywords and the full description in the language you choose.
- [Platsbanken Scraper: Sweden Jobs, Employers, Org Numbers](https://apify.com/rowcraft/platsbanken-jobs-scraper) — Every job ad on Platsbanken (Arbetsförmedlingen) from the official JobTech API: headline, employer and organisation number, occupation codes, municipality, coordinates, salary type, deadline and full text.
- [Recruitee Jobs Scraper – Job Listings by Company Career Site](https://apify.com/rowcraft/recruitee-jobs-scraper) — Job listings from Recruitee career sites by company: title, department, locations with country, employment type, remote flag, published salary and full text, one request per company.
- [Shopify Products Scraper: Store Catalog Export to JSON, CSV](https://apify.com/rowcraft/shopify-products-scraper) — Products from any Shopify store's public catalog: title, vendor, type, tags, description, variants with price, compare-at price, SKU and stock, images, options, store currency.
- [Sitemap URL Extractor: Every Page with Last-Modified Dates](https://apify.com/rowcraft/sitemap-url-extractor) — Get every page a site lists in its sitemaps, with last-modified dates, from robots.txt through sitemap indexes and .gz files.
- [Store Products Scraper – Shopify, WooCommerce, VTEX](https://apify.com/rowcraft/store-products-scraper) — Export the product catalogue of any Shopify, WooCommerce or VTEX store from its public catalogue endpoint: title, brand, prices, discounts, stock, categories, images and links, one row per product, the same columns on every platform.
- [Teamtailor Jobs Scraper – Career Site Jobs by Company](https://apify.com/rowcraft/teamtailor-jobs-scraper) — Job listings from Teamtailor career sites by company: title, department, role, locations with city and country, remote or hybrid status, publication date and full ad text, from the official job feed.
- [TED Tenders Scraper: EU Public Procurement Notices API](https://apify.com/rowcraft/ted-tenders-scraper) — EU public procurement notices from TED (Tenders Electronic Daily), the official EU tenders portal, through its public Search API: title, buyer, country, CPV codes, values, deadlines, lots and document links.
- [UK Companies House Scraper: Company Search & Profiles API](https://apify.com/rowcraft/uk-companies-house-scraper) — UK companies from the official Companies House API with your free key: search by name, status, type, SIC code, date or location, or look up company numbers.
- [UK Tenders Scraper: Find a Tender + Contracts Finder API](https://apify.com/rowcraft/uk-tenders-scraper) — UK public procurement notices from Find a Tender and Contracts Finder, the official UK government portals, through their open OCDS APIs: title, buyer, region, CPV codes, values, deadlines, notice type and links.
- [Bulk URL Status and Redirect Chain Checker API](https://apify.com/rowcraft/url-status-redirect-checker) — Check the status code, redirect chain, canonical and indexing signals of every URL in a list, with the final destination and a loop flag.
- [VTEX Products Scraper – Public Catalog Export](https://apify.com/rowcraft/vtex-products-scraper) — Export the catalogue of any VTEX store (Auchan, Chedraui and thousands of retailers in Europe and Latin America) from its public search endpoint: title, brand, EAN, prices, list prices, stock, categories, specifications and images, one row per product.
- [Website Tech Stack Detector – Technology Lookup API](https://apify.com/rowcraft/website-tech-stack-detector) — Find the technologies behind a list of websites: CMS, e-commerce platform, analytics, tag managers, ad pixels, consent tools, chat, CDN and payments, from one polite home-page request per domain.
- [WooCommerce Products Scraper – Store API Export](https://apify.com/rowcraft/woocommerce-products-scraper) — Export every product of a WooCommerce store through its public Store API: title, SKU, prices and sale prices, stock, categories, tags, attributes, ratings, images and links, one row per product.
- [Workable Jobs Scraper: Job Listings from Career Pages](https://apify.com/rowcraft/workable-jobs-scraper) — Job listings from any company's Workable career page: title, department, location, remote or on-site, employment type, description, requirements, benefits and salary when published.
- [Workday Jobs Scraper – Jobs from myworkdayjobs.com Sites](https://apify.com/rowcraft/workday-jobs-scraper) — Job listings from Workday career sites (myworkdayjobs.com) by company: title, locations, country, posting date, time type, requisition ID and the full description.

### FAQ

**What does the SEO audit check?** The title and meta description (presence and length), the H1 and H2 headings, the canonical address, indexing directives from meta tags and the `X-Robots-Tag` header, the page language and hreflang alternates, Open Graph and Twitter card tags, the types of structured data, the word count, the number of internal, external and nofollow links, images without alt text, HTTPS, redirects and the response time. The 20 issue codes are listed in the Output section with their thresholds and points.

**Can I audit a whole website?** Yes, within a limit: turn on **Follow internal links**, give the home page, and set **Maximum pages per site** (up to 1,000). The actor follows links in document order, breadth first, and stops at the limit. For a full inventory of every address of a large site, give a list of addresses instead.

**Are the thresholds Google's rules?** No. A title of 30 to 60 characters, a meta description of 70 to 160, 300 words and 1,800 ms are common rules of thumb. Search engines do not publish a pass mark for them, and a page outside the range is not penalised by that fact alone. Use the codes as a checklist and decide for each page.

**Why is the result empty?** Every page of the list could not be read or was skipped. The `COVERAGE` record gives the reason per page: an anti-bot page (`blocked`), robots.txt (`robots_disallowed`), a missing page or domain (`not_found`), an address that is not HTML (`not_html`), an invalid entry or a connection problem (`error`). With **Only new rows** on, pages whose SEO fingerprint did not change are skipped too. Nothing is charged for those, apart from the start event.

**Is it legal?** The actor reads public pages, one request each, obeys robots.txt and keeps only SEO values, not personal data or page content. Whether and how you may audit a given site, and what you do with the results, depends on the site's terms and on the law that applies to you.

**Does it collect personal data?** No personal data: no e-mail addresses, phone numbers, author names or social profiles. See the Privacy section.

**Do I pay for pages that block the actor?** No. Blocked, robots-disallowed, missing, non-HTML and invalid entries are never charged, and neither are duplicates in your list or pages that redirect to an address already audited in the run.

**Why does a page show few words or no H1 although I see them in the browser?** The actor reads the HTML as the server sends it and does not run JavaScript. If the content is added in the browser, it is not in that HTML. Check the page source (not the inspector): what is not there is not seen by a basic crawler either.

**Why is a `www` page flagged `canonical_to_other_url`?** Because its canonical names another address, for example the same page without `www.`, or with `http`. That is a real difference in the address, even when the redirect rules of the site make both work. Check which address you want search engines to use.

**What does `indexable: false` mean?** A `noindex` directive (meta tag or header) applies, or the canonical points elsewhere. `indexabilityReason` says which. The flag describes the page, not the site, and robots.txt rules are not part of it: pages that robots.txt blocks are never read.

**How does Only new rows work here?** The actor remembers the SEO fingerprint of every page delivered to you. The next run with the same state name delivers a page only when the fingerprint changed, so a weekly schedule costs only the pages that changed.

**Can I follow links to other websites?** No. A crawl stays on the site of the page it started from; links that redirect to another site are not followed. To audit another site, add it to the list.

**How fast is it?** At the default, between 1,000 and 2,000 pages an hour (a list of different sites is slower than the pages of one site, because every site also needs its robots.txt). Sites with a Crawl-delay in robots.txt are read more slowly, as the site asks. Lower `requestIntervalMs` only for sites you own.

### Support and reviews

**Problems, missing fields, questions:** open an issue on the **Issues** tab of this Actor. We answer within one working day and fix confirmed bugs in the next release; the changelog lists every change.

If this Actor saved you time, a short review on its page helps other users find it and helps us keep improving it.

# Changelog

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

# Actor input Schema

## `startUrls` (type: `array`):

Page addresses (https://example.com/blog/post) or domains (example.com), one per line. Each one is audited as the page it points to; a domain means its home page. Duplicates and invalid entries are skipped and never charged.

## `crawl` (type: `boolean`):

Off (default): only the pages you list are audited. On: the internal links of each audited page are followed too (same site, robots.txt obeyed, up to the page limit below), so a domain becomes a small site audit.

## `maxPagesPerSite` (type: `integer`):

Only used when Follow internal links is on: the most pages read per site, counting the pages you listed. Links with a query string (?...) and files such as PDFs and images are not followed.

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

Stop after this many audited pages are delivered. 0 = no limit (the list and the page limit decide).

## `onlyNew` (type: `boolean`):

Remember the rows already delivered to you (in a key-value store in your Apify account) and deliver only rows that appeared since the last run with the same state key. The first run delivers everything that matches; later runs cost only the new rows. Ideal for schedules.

## `stateName` (type: `string`):

Name of the memory used by "Only new rows". Use a different name for each schedule or search (for example `stockholm-python`) so they do not share one memory.

## `notifyUrl` (type: `string`):

A webhook address that receives one JSON POST with the number of new rows and links to the dataset: a Slack or Discord incoming webhook, a Make, n8n or Zapier catch hook, or your own endpoint. The message is also in the `text` (Slack) and `content` (Discord) fields.

## `notifyWhenEmpty` (type: `boolean`):

By default the notification is sent only when at least one row was delivered.

## `requestIntervalMs` (type: `integer`):

Default 1000 ms (one request per second). Each page takes two requests: robots.txt (once per site) and the page, plus redirects. A Crawl-delay in a site's robots.txt is obeyed when it is longer. Minimum 250.

## Actor input object example

```json
{
  "startUrls": [
    "https://www.python.org/",
    "https://developer.mozilla.org/en-US/",
    "https://www.gnu.org/"
  ],
  "crawl": false,
  "maxPagesPerSite": 20,
  "maxItems": 0,
  "onlyNew": false,
  "stateName": "default",
  "notifyWhenEmpty": false,
  "requestIntervalMs": 1000
}
```

# Actor output Schema

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

One row per audited page with its on-page SEO fields, issues and score, in the default dataset (JSON, CSV, Excel and other formats via the dataset API).

## `runSummary` (type: `string`):

Counts, the most frequent issues, the average score, warnings and why the run stopped (RUN_SUMMARY record in the default key-value store).

## `coverage` (type: `string`):

One line per page read or requested: ok, blocked, robots_disallowed, not_found, not_html, skipped, error or not_checked, with the reason (COVERAGE record).

# 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 = {
    "startUrls": [
        "https://www.python.org/",
        "https://developer.mozilla.org/en-US/",
        "https://www.gnu.org/"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("rowcraft/seo-audit-tool").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 = { "startUrls": [
        "https://www.python.org/",
        "https://developer.mozilla.org/en-US/",
        "https://www.gnu.org/",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("rowcraft/seo-audit-tool").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 '{
  "startUrls": [
    "https://www.python.org/",
    "https://developer.mozilla.org/en-US/",
    "https://www.gnu.org/"
  ]
}' |
apify call rowcraft/seo-audit-tool --silent --output-dataset

```

## MCP server setup

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

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/hKd0Fd1ttEK55RLby/builds/JWWQiyVx77ZykAYvn/openapi.json
