# Own-Site SEO Audit: Catch Broken Links, Hreflang & Canonicals (`licensedpipe/seo-audit-own-site`) Actor

Crawl your own site and get one JSON row per page: titles, canonicals, hreflang, alt text, broken links, with issue codes. Pay per audited page.

- **URL**: https://apify.com/licensedpipe/seo-audit-own-site.md
- **Developed by:** [licensed pipe](https://apify.com/licensedpipe) (community)
- **Categories:** SEO tools, Developer tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$10.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.

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

## Own-Site SEO Audit: catch broken links, hreflang and canonical bugs before they ship

![Own-Site SEO Audit - find it, fix it, ship it](https://api.apify.com/v2/key-value-stores/8P2PJIF2EPbIhhY6W/records/seo-audit-banner.png?signature=SmY9dkSbAPmT4qEGI0K1)

**Crawl your own website and get one clean JSON row per page, with a machine-readable list of what is wrong.** Run it before a release, after a migration, or on a schedule, and see missing titles, broken internal links, wrong canonicals, invalid hreflang and more in one dataset.

Publisher: **Licensed Pipe** - contact through the Apify actor page / issues tab.

> **Not an official SEO score and not a complete audit.** Static HTML checks only: no JavaScript rendering, no Core Web Vitals, no rankings, no backlinks, no content-quality judgement. Rules such as "title over 60 characters" are common rules of thumb, not search-engine requirements. Pages that build their content only in the browser may look emptier than they are.

### Who it is for

- **Developers and technical teams** - check every release before it goes live. Point the actor at a publicly reachable staging or production host you are allowed to audit and get a list of issues per URL.
- **Agencies and freelancers** - audit **your clients' sites with their written permission** and hand over structured data instead of screenshots.
- **E-commerce and content sites** - find duplicate titles, duplicate meta descriptions, missing canonicals, `noindex` mistakes and broken internal links across hundreds of pages.
- **Multilingual sites** - validate hreflang codes, self-references, `x-default` and return links between language versions (return links are checked only among the pages crawled in the same run).

### What you get

One flat dataset row per URL, plus a `RUN_SUMMARY` record with the limits applied and the reason if a run stopped early. Every row has an `issues[]` list with a code and a severity (`error` / `warning` / `info`) that scripts and dashboards can filter on.

**Checks per page:** HTTP status and redirect chain, title and meta-description length and duplicates, H1 and heading structure, canonical, `noindex` (meta tag and `X-Robots-Tag`), hreflang validity and return links (among pages crawled in the same run), `lang`, viewport, Open Graph / Twitter card presence, JSON-LD syntax (blocks and `@type` names only), image alt coverage, mixed content, thin content (word count only) and **broken internal links** (verified against the pages crawled in the same run).

**Example issue codes:** `title_missing`, `title_duplicate`, `meta_description_missing`, `h1_missing`, `canonical_missing`, `canonical_other_host`, `noindex`, `hreflang_invalid_code`, `hreflang_missing_self_reference`, `hreflang_missing_return_link`, `images_missing_alt_some`, `mixed_content`, `json_ld_invalid_syntax`, `redirect_chain_3_hops`, `broken_internal_links`, `thin_content_under_150_words`.

**Illustrative example row** (shortened, made-up values for `example.com` to show the shape - not output of a real run):

```json
{
  "url": "https://www.example.com/pricing",
  "status": 200,
  "title": { "length": 72 },
  "metaDescription": { "length": 0 },
  "h1Count": 1,
  "canonical": "https://www.example.com/pricing",
  "noindex": false,
  "brokenInternalLinks": { "count": 1 },
  "issues": [
    { "code": "title_long_over_60_chars", "severity": "info" },
    { "code": "meta_description_missing", "severity": "warning" },
    { "code": "broken_internal_links", "severity": "error" }
  ]
}
```

### Quick start (3 steps)

1. **Enter your start URLs** - e.g. `https://www.example.com/`. Use the exact host (`www` and non-`www` are different hosts).
2. **Tick the ownership confirmation** - you state that you own the site or have written permission to audit it. The actor will not run without it.
3. **Set the page limit and press Start** - default 50 pages, maximum 500 per run. Open the dataset table view or export JSON, CSV or Excel.

### Run it from code, on a schedule, or from your pipeline

The actor runs on the Apify platform, so the platform's standard features apply (see the Apify documentation; availability depends on your Apify plan):

- **API** - start runs and fetch dataset items over the Apify REST API: https://docs.apify.com/api/v2 (JavaScript client library: https://docs.apify.com/api/client/js).
- **Schedules** - run the audit at times you choose with cron expressions: https://docs.apify.com/platform/schedules
- **Webhooks and integrations** - react when a run succeeds or fails: https://docs.apify.com/platform/integrations
  Use these to check a site after each deployment or every week, and fail your own pipeline if the dataset contains `error` severity issues. Wiring this into a CI system is up to you; the actor itself only crawls and reports.

### Pricing

**Pay per event: one `page-audited` event per returned row (per audited page).** The current price is always shown on the Pricing tab of this page. Runs that stop before returning any row (blocked, WAF, robots.txt unreachable) are not charged. `maxPages` caps what a run can return, so you control the maximum.
Apify platform usage (compute) is billed according to your Apify plan; this actor is slow rather than heavy because it waits at least 1 s between requests.

### FAQ

**Can I audit a site that is not mine, such as a client's?** Only sites you own or have written permission to audit. You confirm this in the input and remain responsible for it.
**Does it render JavaScript or measure speed?** No. It reads the static HTML the server returns. It does not measure Core Web Vitals, rankings or backlinks.
**Is the result an SEO score or a complete audit?** No. It lists specific technical findings from a fixed set of checks. Some rules are rules of thumb, not requirements.
**Why is it slow on large sites?** It sends one request at a time with at least 1 s between request starts per host, obeys robots.txt and Crawl-delay, and stops on repeated blocks. At 1 request per second, 500 pages take about 9 minutes or longer.
**Will it stop if my server blocks it?** Yes: 3 consecutive 403/429 responses, WAF or bot challenges, repeated 5xx/network failures or an excessive Retry-After stop the run. It does not try to get around them.
**Does it store my page text or personal data?** No page text is stored by default; see "Personal data" below.
**Can it audit pages behind a login, or localhost?** No. It never logs in, and private, loopback, link-local and cloud-metadata addresses are refused. A staging site must be publicly reachable.
**Where do I report a problem?** In the Issues tab of the actor page.

### Responsible use - your responsibility

- **Only audit sites you own or have written permission to audit.** By ticking the confirmation box in the input you state that this applies. The actor sends real HTTP requests to the web servers you name; you are responsible for any terms of service, contract or law that applies to that.
- The actor fetches only the start URLs you supply and the same-host pages they link to (up to the page limit). It never follows links to other hosts, never logs in, never submits forms, never uses cookies, proxies or stealth techniques.
- Private, loopback, link-local and cloud-metadata addresses (for example `localhost`, `10.x`, `192.168.x`, `169.254.169.254`) are refused.

### Politeness and hard limits (enforced in code, not configurable above these values)

| rule | value |
|---|---|
| robots.txt | fetched once per host and obeyed (group for this actor's name, else `*`); `Crawl-delay` is honoured (delays above 30 s skip the host); robots.txt unreachable (5xx / network) or access denied (401/403) = treated as "disallow all" and reported; URLs disallowed by robots.txt are **not requested** and are reported as such |
| request rate | requests run one at a time, **>= 1 s between request starts per host** (more if robots.txt asks) |
| size | 50 pages by default, hard cap 500 pages per run; 20 start URLs, 3 hosts; response bodies capped at 2 MB |
| retries | max **3** per URL (network errors, 5xx, 408; 429/503 only after waiting), exponential backoff with jitter |
| Retry-After | honoured; if a server asks for more than 60 s the run stops instead of waiting |
| 403 / 429 | **3 consecutive 403/429 responses stop the run**; a 403 is never retried |
| WAF / bot challenge | Cloudflare/Akamai/etc. challenge pages, `cf-mitigated: challenge`, empty HTTP 202 and CAPTCHA pages stop the run immediately - the actor does not try to get around them |
| total requests | hard ceiling per run (about 3x the page limit plus 40); max run time 15 min by default (1 h max) |
| identification | User-Agent contains the actor name, publisher, a contact pointer and the run id: `<actor>/0.1 (Apify actor by Licensed Pipe; ...; contact ...; run <id>)` |
| adaptive back-off / stop | after any 429/503 the gap grows (2 s, 4 s ... up to 10 s) for the rest of the run; 6 consecutive 5xx/network failures stop the run |
Please do not start many runs against the same site at the same time.

### Personal data

- Page bodies are parsed in memory and **discarded**; no page text, e-mail addresses, phone numbers, names, cookies or form values are stored. `mailto:` and `tel:` links are ignored.
- The only free text kept are fields documented below, shortened, with anything that looks like an e-mail address or phone number masked. URLs are stored without their query string (replaced by a salted marker like `?q~1a2b3c4d`; the real URL is still requested). Set `keepQueryStrings` to keep them - only if they contain no personal data.
- If your own pages contain personal data in titles or URLs, the dataset you create contains it too; you are the data controller of that dataset.

### Input (see .actor/input_schema.json)

| field | meaning |
|---|---|
| startUrls | your pages, e.g. `https://www.example.com/` (use the exact host - `www` and non-`www` are different hosts) |
| confirmOwnership | must be ticked |
| crawlInternalLinks / maxDepth | follow same-host links breadth-first (default on, depth 3) |
| maxPages | default 50, max 500 (each returned row is billed) |
| includeTitleAndDescriptionText | return title/description text (masked) - **off by default**; then only lengths and duplicate flags |
| keepQueryStrings | keep query strings in output URLs (off by default) |
| maxRuntimeSeconds | default 900, max 3600 |

### Output

One dataset row per URL (`url`, `finalUrl`, `status`, `redirects[]`, `responseMs`, `title{length,text}`, `metaDescription{length,text}`, `h1Count`, `noindex`, `canonical`, `lang`, `hreflang[]`, `openGraph`, `jsonLd`, `images`, `links`, `wordCount`, `brokenInternalLinks{count,examples,notChecked,redirecting}`, `issues[]`, `auditedAt`). URLs blocked by robots.txt appear as a row with the issue `blocked_by_robots_not_fetched` and no request was made. The key-value record `RUN_SUMMARY` lists the limits applied, robots.txt status per host, request/retry counts and, if the run stopped early, the reason.

Links to URLs that were not fetched because the page limit was reached are counted as `notChecked`, not as broken.

### Legal notes, disclaimers and trademarks

- **Ownership is your declaration, not something the actor can check.** The actor cannot verify that you own or may audit a site. The confirmation box is your binding statement; you alone are responsible for having the right to send these requests (site terms, contract, applicable law such as the EU/Finnish rules on unauthorised access to information systems) and for complying with the Apify Terms of Service / Acceptable Use Policy. Do not use this actor against sites you do not control and have no permission for. The publisher may block abuse.
- **Load protection is built in, but it does not replace permission.** Sequential requests, at least 1 s apart (slower after any 429/503), robots.txt and Crawl-delay obeyed (401/403 on robots.txt counts as "disallow"), hard page and request ceilings, run-time limit, retries capped at 3, and the run stops on repeated 403/429, WAF challenges, repeated 5xx/network failures or an excessive Retry-After. Do not start several runs against the same site at the same time.
- **Network safety.** Only http(s) URLs on the hosts you supplied; private, loopback, link-local, metadata and other reserved addresses are refused at connection time (also against DNS rebinding); redirects leaving your hosts are reported, never followed; no cookies, logins or forms.
- **Personal data (GDPR).** By default the actor stores no page text. Query strings are replaced by a salted marker in all stored URLs (`keepQueryStrings` is off by default). E-mail addresses, phone numbers, IBAN-like and Finnish personal-identity-code patterns are masked in any text it keeps. Names cannot be detected automatically, so URL paths and (if you enable title/description text) title and description may still contain personal data; if they do, you are the controller of the dataset you create and are responsible for its lawful use, retention and deletion. Requests sent to your server appear in its access logs (IP address of the Apify run, the User-Agent below).
- **Identification.** The User-Agent names the actor, the publisher and the run id, and points to the Apify actor page for contact, so site operators can recognise and report the traffic.
- **Not affiliated.** This is an independent, unofficial tool. "Google", "WCAG" and "W3C" and other names are trademarks or marks of their respective owners; they are used only to describe what is checked and do not imply endorsement or affiliation. Findings are informational, not legal, compliance or professional advice and no guarantee of search ranking or conformance.

### Limitations / disclaimer

Results are a snapshot of what the server returned to this actor's User-Agent at that time. The actor makes no guarantee about search-engine behaviour. Not legal or professional advice.

# Actor input Schema

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

Pages to audit and start crawling from. Only these hosts are fetched (max 20 URLs, max 3 hosts). Redirects to other hosts are reported, not followed. Use the exact host you want audited (www and non-www are different hosts).

## `confirmOwnership` (type: `boolean`):

Required. The actor sends real HTTP requests to the site (<= 1 request per second, robots.txt respected). You are responsible for having the right to audit it.

## `crawlInternalLinks` (type: `boolean`):

Follow same-host links found on the pages (breadth-first) until the page limit. If off, only the start URLs are audited.

## `maxPages` (type: `integer`):

Maximum number of URLs fetched and returned (each result is billed). Default 50, hard cap 500. At 1 request/s, 500 pages take about 9 minutes or longer if robots.txt asks for a Crawl-delay.

## `maxDepth` (type: `integer`):

How many link levels from the start URLs to follow.

## `includeTitleAndDescriptionText` (type: `boolean`):

Return the text of <title> and meta description (e-mail addresses, phone numbers and ID-like numbers are masked, but names cannot be detected). Off by default (data minimisation): you then get only lengths and duplicate flags.

## `maxRuntimeSeconds` (type: `integer`):

The run stops cleanly after this time and returns what it has.

## `keepQueryStrings` (type: `boolean`):

By default everything after "?" in URLs written to the dataset and run summary is replaced by a short salted marker (?q~xxxxxxxx), because query strings can carry tokens, e-mail addresses or user IDs. Requests are still sent with the full URL. Turn on only if you need the original query strings and know they contain no personal data.

## Actor input object example

```json
{
  "startUrls": [
    "https://example.com/"
  ],
  "confirmOwnership": false,
  "crawlInternalLinks": true,
  "maxPages": 50,
  "maxDepth": 3,
  "includeTitleAndDescriptionText": false,
  "maxRuntimeSeconds": 900,
  "keepQueryStrings": false
}
```

# Actor output Schema

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

No description

## `summary` (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 = {
    "startUrls": [
        "https://example.com/"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("licensedpipe/seo-audit-own-site").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://example.com/"] }

# Run the Actor and wait for it to finish
run = client.actor("licensedpipe/seo-audit-own-site").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://example.com/"
  ]
}' |
apify call licensedpipe/seo-audit-own-site --silent --output-dataset

```

## MCP server setup

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

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/6nevmJRooIHJ2dKOL/builds/FjMyPcebtuz07yAQI/openapi.json
