# Site Health Audit: SEO, TLS, llms.txt & Broken Links (`minhluann291/site-health-audit`) Actor

Audit a list of URLs: status, redirect chain, TLS expiry, SEO tags, hreflang, Open Graph, JSON-LD, robots.txt, sitemap, llms.txt and broken links, with a 0-100 score and clear reasons. No crawl, no browser.

- **URL**: https://apify.com/minhluann291/site-health-audit.md
- **Developed by:** [Luận Phạm Minh](https://apify.com/minhluann291) (community)
- **Categories:** SEO tools, Developer tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $8.00 / 1,000 url 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

## Site Health Audit: SEO, TLS, llms.txt and Broken Links with a 0-100 Score

**Paste a list of URLs. Get a score from 0 to 100 for each, with every lost point explained.**

For each URL, this Actor checks the things that silently break: expiring certificates, redirect chains, missing tags, invalid structured data, a blocked robots.txt, dead links. It reports each one in plain English, ready for a client report or an automated alert.

No login, no browser, no proxies. It uses plain HTTP, so it is fast and cheap.

### Who it is for

- **SEO consultants and agencies** who need a quick technical check across many client pages
- **Developers and DevOps** who want a scheduled check for certificate expiry and broken pages
- **Site owners** preparing for AI search, who want to see `llms.txt`, robots rules and structured data in one place

### What it checks on every URL

| Area | Details |
|---|---|
| **Availability** | Final status, full redirect chain with each hop's status, loop detection, response time |
| **TLS certificate** | Valid or not, the reason if not, expiry date, days left, issuer, protocol |
| **SEO tags** | Title, meta description, canonical, meta robots, hreflang, H1 count, language, viewport |
| **Sharing** | Open Graph tags |
| **Structured data** | JSON-LD types found, and any blocks with invalid JSON |
| **Crawl setup** | robots.txt rules, sitemap presence, whether robots.txt blocks all bots |
| **AI readiness** | `llms.txt` presence, ignoring sites that answer every path with an HTML page |
| **Links** | Broken links on that page, up to your cap, with anchor text |
| **Score** | 0-100 with a grade and a list of reasons, each with its penalty |

### How to use it

1. Paste your URLs.
2. Choose how many links per page to test. Set it to 0 for a much faster run.
3. Run it, or put it on a [schedule](https://docs.apify.com/platform/schedules) to catch problems such as an expiring certificate early.

```json
{
  "urls": [{ "url": "https://www.example.com/" }, { "url": "https://www.example.com/pricing" }],
  "maxLinksPerPage": 50,
  "maxPages": 100
}
```

### Output

One row per URL. Shortened sample from a real run:

```json
{
  "inputUrl": "https://www.iana.org/",
  "finalUrl": "https://www.iana.org/",
  "finalStatus": 200,
  "redirectChain": [],
  "responseTimeMs": 133,
  "tls": { "checked": true, "valid": true, "protocol": "TLSv1.3", "daysUntilExpiry": 79 },
  "page": {
    "title": "Internet Assigned Numbers Authority",
    "metaDescription": null,
    "canonical": "https://iana.org/",
    "hreflang": [],
    "openGraph": {},
    "jsonLd": { "types": [], "blocks": 0, "invalidBlocks": 0 }
  },
  "robotsTxt": { "status": 200, "present": true, "sitemaps": [], "blocksAllBots": false },
  "sitemap": { "present": false },
  "llmsTxt": { "present": false, "status": 404 },
  "links": { "found": 23, "checked": 23, "ok": 20, "redirected": 0, "brokenCount": 0, "broken": [], "unverified": [{ "url": "https://www.icann.org/", "anchor": "ICANN", "status": null, "error": "The operation was aborted" }] },
  "score": 82,
  "grade": "B",
  "reasons": [
    { "check": "meta", "penalty": 6, "severity": "warning", "message": "The meta description is missing." },
    { "check": "open-graph", "penalty": 3, "severity": "info", "message": "Open Graph title or image is missing, so shared links look bare." },
    { "check": "sitemap", "penalty": 3, "severity": "warning", "message": "No sitemap found in robots.txt or at /sitemap.xml." },
    { "check": "canonical", "penalty": 2, "severity": "info", "message": "The canonical points elsewhere: https://iana.org/" },
    { "check": "structured-data", "penalty": 2, "severity": "info", "message": "No JSON-LD structured data." },
    { "check": "language", "penalty": 1, "severity": "info", "message": "The html element has no lang attribute." },
    { "check": "llms", "penalty": 1, "severity": "info", "message": "No llms.txt (an optional file that guides AI tools)." }
  ]
}
```

### How the score works

Every page starts at 100 and loses points for specific problems, so you can always see why. Grades: A is 90+, B is 75+, C is 60+, D is 40+, F is below that.

| Problem | Points lost |
|---|---|
| Unreachable site | Score is 0 |
| HTTP 4xx or 5xx | 60 |
| Redirect loop | 30 |
| Invalid TLS certificate | 25 |
| Not served over HTTPS | 20 |
| Certificate expires in under 7 days / under 21 days | 15 / 5 |
| Broken links | 5 for the first, then 2 more each, up to 15 |
| Missing title | 10 |
| Noindex | 10 |
| Slow response (over 3 s / over 1.5 s) | 8 / 3 |
| Missing meta description | 6 |
| Several redirect hops | 5 |
| Missing canonical / invalid JSON-LD | 4 each |
| Missing sitemap, H1, viewport or Open Graph | 3 each |
| Smaller issues, such as title length, no llms.txt, no lang | 1 or 2 |

The score is a checklist of technical hygiene. It is not a prediction of search ranking.

### Honest link results

A link counts as **broken** only on clear evidence: HTTP 404, 410, a 5xx other than 503, a domain that does not exist, or a refused connection. Sites often answer automated checks with 400, 403, 429 or 503 even when the page works for people, so those are listed as **unverified** and do not cost points. Links whose site's robots.txt disallows us are skipped and counted.

### Pricing

Pay per event.

| Event | Price | When |
|---|---|---|
| Run started | $0.002 | Once per run, per GB of memory |
| URL audited | $0.008 | One URL audited and scored, including a dead site, which is a real finding |

Invalid URLs, private addresses and URLs blocked by robots.txt are free.

**Examples:** 10 URLs cost **$0.082**. 100 URLs cost **$0.80**. Link checks are included in the price, up to 500 links per page. Set a **maximum cost per run** and the Actor stops cleanly at that limit.

### Limits

- **Only the URLs you supply are audited.** Hard cap of 1,000 URLs per run.
- **Link checking is one hop deep**, in page order, and stops after 120 seconds per page. The output says how many links were left unchecked.
- **Response time is one measurement** from Apify's servers, so it is a rough signal and can vary between runs. It is not a Core Web Vitals test.
- **HTML is read as served.** Tags or links added later by JavaScript are not seen.
- **Pages over 3 MB** are read up to that point, and `bodyTruncated` is set.

### What it does not do

- It does not crawl your site or discover pages. If you want that, use a crawler.
- It does not run a browser, so there are no Lighthouse or Core Web Vitals scores.
- It does not report backlinks, keywords, rankings or domain authority.
- **It honours robots.txt.** If a site disallows this Actor's fetching (`LuanLabSiteHealthAudit`), or its robots.txt returns a server error, that URL is reported as not audited and is not charged. To audit such a page, allow this user agent on your own site.
- It does not bypass firewalls, bot protection or CAPTCHAs. A page behind one is scored on what it returned.
- It does not collect e-mail addresses or other contact details.
- It refuses private and internal addresses.

### Support

Think a score is wrong? Open an issue on the **Issues** tab with the URL and the reason you disagree.

# Actor input Schema

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

The pages to audit, one per line. Only these pages are audited: this Actor does not crawl your site. Left empty, it audits a small demo set.

## `maxLinksPerPage` (type: `integer`):

How many links on each page to test for breakage, in page order. 0 turns the link check off and makes the run much faster.

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

Hard cap on URLs audited in one run, up to 1000. Extra URLs are ignored and not charged.

## Actor input object example

```json
{
  "urls": [
    {
      "url": "https://apify.com/"
    },
    {
      "url": "https://www.iana.org/"
    }
  ],
  "maxLinksPerPage": 25,
  "maxPages": 100
}
```

# Actor output Schema

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

One audit with a 0-100 score and reasons per URL.

## `summary` (type: `string`):

Counts for this run as JSON.

# 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": [
        {
            "url": "https://apify.com/"
        },
        {
            "url": "https://www.iana.org/"
        }
    ],
    "maxLinksPerPage": 25
};

// Run the Actor and wait for it to finish
const run = await client.actor("minhluann291/site-health-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": [
        { "url": "https://apify.com/" },
        { "url": "https://www.iana.org/" },
    ],
    "maxLinksPerPage": 25,
}

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

```

## MCP server setup

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