# Complete Site Audit — SEO, Security & Social Grade (`eliai/complete-site-audit`) Actor

Full website health audit in one call: SEO, security headers, social tags, crawlability, and tech stack, scored into an A–F grade with one prioritized issue list. $0.05 per site audited. No API key.

- **URL**: https://apify.com/eliai/complete-site-audit.md
- **Developed by:** [Broke to Built](https://apify.com/eliai) (community)
- **Categories:** Developer tools, Automation
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Complete Site Audit — crawl a site, get every problem on every page

**The problem:** finding out what is wrong with a website means running eight different tools. An SEO
checker, a security-header scanner, an SSL expiry monitor, a DNS/SPF/DMARC lookup, an accessibility
pass, a cookie scanner, a robots/sitemap check, a tech-stack detector. Eight tabs, eight output
formats, and nothing tells you what to fix first — and you still have to repeat all of it per page.

**The solution:** send one URL. The Actor finds your pages from `sitemap.xml` (falling back to
on-page links), audits **every page across all ten areas**, and returns one record per page plus a
site summary. Every finding is tagged `high` / `medium` / `low` with a `fixOrder` array already
sorted into a work queue.

```json
{ "url": "https://example.com/", "maxPages": 25 }
```

```json
{
  "score": 87, "grade": "B", "issueCount": 4,
  "fixOrder": [
    "[high] accessibility: 12/40 images have no alt attribute",
    "[high] email: no DMARC record — spoofed mail is not rejected or reported",
    "[medium] security: missing header: content-security-policy",
    "[low] seo: title is 69 chars (10-60 renders fully in results)"
  ]
}
```

### Schedule it: the second run tells you what changed

An audit you run once is a snapshot. Schedule this Actor and every run diffs against the last one, so
you get the thing that actually matters — **what broke since last week**:

```json
"changes": {
  "isBaseline": false,
  "newIssues": [ { "area": "security", "severity": "low", "msg": "missing header: referrer-policy" } ],
  "fixedIssues": [ "seo|missing meta description" ],
  "stillOpen": 3,
  "previousScore": 91, "scoreDelta": -4,
  "summary": "1 NEW · 1 fixed — 3 still open."
}
```

The first run records a baseline and says so. Counts inside a finding are normalised, so
"12/40 images missing alt" becoming "13/41" is the *same* ongoing issue rather than a fix plus a new
problem — the change list stays worth reading. Alert on `changes.newIssues`, on `scoreDelta`, or on
`certificate.daysUntilExpiry`.

### What it checks — ten areas in a single pass

| Area | What comes back |
|---|---|
| **SEO** | title, meta description, H1 count, canonical, JSON-LD blocks, images missing alt, word count, `noindex` detection |
| **Security headers** | HTTPS, all 6 key headers (HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy), mixed content |
| **TLS certificate** | issuer, subject, validity dates, **days until expiry**, trust status, protocol version |
| **Email / DNS** | A, MX, NS, CAA record counts · **SPF** record · **DMARC** record and its enforcement policy |
| **Accessibility** | `lang` attribute, viewport meta, images missing alt, form inputs with no label association, skipped heading levels |
| **Privacy** | cookies set, third-party script hosts, known tracker hosts by name |
| **Performance** | server response time, HTML document size, script/stylesheet counts |
| **Crawlability** | robots.txt, sitemap.xml, ads.txt, `/.well-known/security.txt` |
| **Social** | Open Graph tags, og:image, Twitter Card |
| **Tech stack** | WordPress / Next.js / Shopify / Astro / Nuxt / React fingerprints, server and X-Powered-By |

### Input

| Field | Type | Required | Notes |
|---|---|---|---|
| `url` | string | one of the two | A site to crawl and audit |
| `urls` | string\[] | one of the two | Audit exactly these pages instead of crawling |
| `maxPages` | integer | no | Pages to audit, and the run's billing cap (default 10, max 200) |
| `stateStoreName` | string | no | Named store holding the last run's findings for change detection (default `site-audit-state`) |

Send `url` to crawl, or `urls` to audit an exact list. Each page becomes one dataset record and one
charge. A page that fails to fetch returns an error record and is **not charged**. Set `maxPages: 1`
to audit only the URL you gave.

### Pricing

**$0.03 per page analyzed** (pay per event, `page-analyzed`). You are charged once per page that
produced a real audit. Pages that could not be fetched produce a record explaining what happened and
cost nothing.

`maxPages` is your cost ceiling: a 10-page run costs at most $0.30, a 50-page run at most $1.50.
Domain-level checks — TLS certificate, DNS/SPF/DMARC/CAA, robots/sitemap/security.txt — run **once
per run** and are not charged per page. There is no subscription and no API key to obtain.

### Calling it as an agent tool

This Actor is callable over Apify MCP, so an agent can hand it a URL and read the `fixOrder` array
directly as its task list. It takes one string and returns one flat JSON record per page, plus a
`SITE_SUMMARY` record — no pagination, no session, no auth handshake.

### FAQ

**Does it render JavaScript?**
No. Every reading comes from the server-rendered HTML — which is exactly what a search crawler sees.
On a client-rendered SPA the SEO and accessibility numbers describe the crawler's view, not the
human's. If you need the rendered DOM, this is the wrong tool and it says so rather than guessing.

**Is this a full WCAG audit?**
No, and no automated tool is. It covers the machine-checkable subset — roughly 30% of WCAG success
criteria. It cannot judge colour contrast against a background image, keyboard focus order, or whether
alt text is meaningful rather than merely present. It finds the failures that are unambiguous.

**How is the score calculated?**
Every issue carries a severity, and the score is `100 − Σ(high 10, medium 4, low 1)`, floored at 0.
Grades: A ≥ 90, B ≥ 75, C ≥ 60, D ≥ 40, else F. The weighting is a judgement call, not a standard —
the `issues` array is the real output and you can re-weight it yourself.

**Why does it check DNS and mail records on a website audit?**
Because a missing SPF or DMARC record is how a company's domain gets used to spoof its own customers,
and nobody looks until it happens. It costs one DNS query and it is on the same domain you already
asked about.

**How does it decide which pages to audit?**
`sitemap.xml` first — that is the site's own statement of what matters. If there is no usable
sitemap it falls back to same-origin links on the page you gave. It never leaves your origin, skips
assets (PDF, images, CSS, JS), and stops at `maxPages`.

**Can I monitor a site over time?**
That is what it is built for. Each run diffs against the last and fills in `changes` per page plus a
roll-up on the site summary. Give each site its own `stateStoreName` if you monitor several. If the
named store cannot be opened in your account the run still succeeds — it says so in the log and
treats the run as a baseline rather than silently reporting "no changes" forever.

**What if a check fails?**
Each sensor is independent and fails soft. If the TLS handshake times out, the certificate section
reports the error and the other nine areas still return. A partial audit is still an audit.

### Limits

- Server-rendered HTML only; no JavaScript execution.
- Accessibility covers the machine-checkable subset of WCAG, not contrast-on-image or focus order.
- Tech detection is fingerprint-based — it reports what leaves a trace in the markup.
- DNS is resolved over DoH; a domain behind a split-horizon resolver may read differently from inside
  its own network.
- Default cap of 10 pages per run, raise with `maxPages` (max 200).

***

Built by [Broke to Built](https://broke2builtai.com). Run it against your own site first — it takes
one click and you can check every finding by hand.

# Actor input Schema

## `url` (type: `string`):

A site to crawl and audit. Pages are found from sitemap.xml, falling back to on-page links. Every page gets the full 10-area audit; DNS, TLS and well-known checks run once for the domain.

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

Audit exactly these pages instead of crawling. Each page is one dataset record and one charge.

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

How many pages to audit in this run. This is also the billing cap: you are charged once per page analyzed, so this is the most the run can cost. Set to 1 to audit only the URL you gave.

## `stateStoreName` (type: `string`):

Named key-value store that remembers the last run's findings so this run can report what changed. Keep the default to track one site over time; give each site its own name if you monitor several.

## Actor input object example

```json
{
  "url": "https://www.apify.com/",
  "urls": [],
  "maxPages": 10,
  "stateStoreName": "site-audit-state"
}
```

# Actor output Schema

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

Every item this run produced, as JSON.

## `resultsCsv` (type: `string`):

The same items as a spreadsheet-ready CSV.

# 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 = {
    "url": "https://www.apify.com/",
    "urls": [],
    "stateStoreName": "site-audit-state"
};

// Run the Actor and wait for it to finish
const run = await client.actor("eliai/complete-site-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 = {
    "url": "https://www.apify.com/",
    "urls": [],
    "stateStoreName": "site-audit-state",
}

# Run the Actor and wait for it to finish
run = client.actor("eliai/complete-site-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 '{
  "url": "https://www.apify.com/",
  "urls": [],
  "stateStoreName": "site-audit-state"
}' |
apify call eliai/complete-site-audit --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,eliai/complete-site-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/Yahq01Vyajdrssdfw/builds/CuNWY3FJG18AZTWCK/openapi.json
