# Broken Link & Image Checker (`moonweil/broken-link-scanner`) Actor

Crawl your website and find every broken link, missing image, and dead script or stylesheet, plus the exact page each one sits on. Fast checks with no browser, redirect detection, external-link checking, and a summary of what failed and why.

- **URL**: https://apify.com/moonweil/broken-link-scanner.md
- **Developed by:** [Aleksandr Jelohhin](https://apify.com/moonweil) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 page checkeds

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?

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

## Broken Link & Image Checker (with source pages)

Crawl your website and find **every broken link and missing image** — plus the
exact page each one is on, so you know precisely what to fix. It also checks
scripts and stylesheets on request, follows and reports redirects, and can verify
external links too.

Fast and inexpensive: it uses lightweight HTTP checks and never opens a browser.
Run it on demand, on a schedule, or from the API.

### What it finds

- **Broken links** — 404s, 410s, server errors, timeouts, DNS failures, connection errors
- **Broken images** — the `<img>` sources that don't load
- **Broken scripts & stylesheets** *(optional)*
- **Redirecting URLs** — links that only resolve after a 301/302, so you can update them
- **Bot-protection blocks** — external URLs that answer with a Cloudflare/DataDome/Akamai-style
  anti-bot *challenge* (a 401/403/429/503 with a challenge header or challenge page). Reported
  separately as `state: "blocked"` and **not** counted as broken, because they normally work fine
  in a browser. A plain 403/503 with no challenge is still reported as broken.
- For every problem: the **source page** it was found on, the **resource type**, the **HTTP status**, and a **reason code**

### Input

```json
{
  "startUrls": [{ "url": "https://example.com" }],
  "crawl": true,
  "maxPages": 1000,
  "checkExternal": true,
  "checkImages": true,
  "checkScripts": false,
  "checkStylesheets": false
}
```

### Output

One dataset row per page that has problems (or every page — your choice):

```json
{
  "pageUrl": "https://example.com/blog",
  "checked": 124,
  "broken": 5,
  "redirecting": 12,
  "blocked": 2,
  "items": [
    { "url": "https://example.com/old-post", "type": "link", "scope": "internal",
      "statusCode": 404, "sourceUrl": "https://example.com/blog", "reason": "not_found", "state": "broken" },
    { "url": "https://crozdesk.com/software/apify", "type": "link", "scope": "external",
      "statusCode": 403, "sourceUrl": "https://example.com/blog", "reason": "blocked",
      "state": "blocked", "blockedBy": "Cloudflare" }
  ]
}
```

Plus a run `SUMMARY`: total URLs checked, broken count, breakdown by reason, and
the worst offenders with the pages that link to them.

### Use it for

- Regular link-rot maintenance on a blog or docs site
- A pre-launch check that nothing 404s
- Finding images that silently stopped loading after a CDN or media move
- Bulk-auditing many sites from the API

### Note on 403 / 429 (bot protection)

Some third-party sites sit behind Cloudflare, Akamai, DataDome and similar
services that challenge automated requests with a 401, 403, 429 or 503. The
scanner sends real browser headers to get past the soft blocks, and when a
challenge is unavoidable — confirmed by a challenge header or challenge-page
body — it labels the URL **`state: "blocked"`** (with `blockedBy` naming the
service) instead of `broken`. These do not inflate the broken count or the run's
`brokenTotal`. A genuine 403/503 with no challenge fingerprint is still counted
as broken.

- To count them as broken anyway, set `treatBlockedAsBroken: true`.
- To skip external checks entirely, set `checkExternal: false`.

### Pricing

Pay per page checked — the link and image checks on that page are included, no
matter how many there are.

# Actor input Schema

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

One or more URLs to analyze. When crawling is on, these are also the crawl seeds.

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

Follow same-domain links from the start URLs. When off, only the start URLs are processed.

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

Hard limit on the number of pages processed. Protects compute cost — the crawl stops once it is reached.

## `maxConcurrency` (type: `integer`):

How many pages to process in parallel.

## `sameDomainOnly` (type: `boolean`):

Restrict the crawl to the hostname of each start URL (www and apex are treated as equal).

## `includeSubdomains` (type: `boolean`):

Also follow links to subdomains of the start URL's registrable domain.

## `useSitemap` (type: `boolean`):

Seed the crawl from the site's /sitemap.xml (and /sitemap\_index.xml) in addition to the start URLs.

## `respectRobotsTxt` (type: `boolean`):

Skip URLs disallowed by the target site's robots.txt.

## `excludePatterns` (type: `array`):

Glob-style deny patterns for URLs to skip, e.g. /logout, /cart\*, \*.zip

## `checkExternal` (type: `boolean`):

Also check links that point to other domains.

## `checkImages` (type: `boolean`):

Also check <img src> resources.

## `checkScripts` (type: `boolean`):

Also check <script src> resources.

## `checkStylesheets` (type: `boolean`):

Also check linked stylesheets.

## `reportRedirects` (type: `boolean`):

Include URLs that resolve only after an HTTP redirect in the results.

## `onlyReportProblems` (type: `boolean`):

Skip dataset rows for pages where every URL is healthy.

## `treatBlockedAsBroken` (type: `boolean`):

By default, external URLs that answer 403/429/503 with a Cloudflare-style anti-bot challenge are reported as "blocked" (state: blocked) and NOT counted as broken — they normally work in a real browser. Turn this on to treat them as broken links.

## `maxUrlsPerPage` (type: `integer`):

Cap on the number of URLs checked per page. Protects compute on link-heavy pages.

## `linkCheckConcurrency` (type: `integer`):

How many link checks to run in parallel per page.

## `linkCheckTimeoutSecs` (type: `integer`):

Timeout in seconds for a single link check.

## `debug` (type: `boolean`):

Verbose logging. Also allows stack traces into the dataset.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://apify.com"
    }
  ],
  "crawl": true,
  "maxPages": 1000,
  "maxConcurrency": 5,
  "sameDomainOnly": true,
  "includeSubdomains": false,
  "useSitemap": false,
  "respectRobotsTxt": true,
  "excludePatterns": [],
  "checkExternal": true,
  "checkImages": true,
  "checkScripts": false,
  "checkStylesheets": false,
  "reportRedirects": true,
  "onlyReportProblems": true,
  "treatBlockedAsBroken": false,
  "maxUrlsPerPage": 250,
  "linkCheckConcurrency": 10,
  "linkCheckTimeoutSecs": 15,
  "debug": false
}
```

# Actor output Schema

## `pages` (type: `string`):

One row per page: how many URLs were checked and how many are broken, redirecting or bot-protection-blocked, plus an item for every problem URL with its resource type, HTTP status, reason code and the source page it was found on.

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

Totals for the whole run: URLs checked, unique broken URLs, breakdown by reason, blocked URLs by protection vendor, and the worst offending URLs with the pages that link to them.

# 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": [
        {
            "url": "https://apify.com"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("moonweil/broken-link-scanner").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": [{ "url": "https://apify.com" }] }

# Run the Actor and wait for it to finish
run = client.actor("moonweil/broken-link-scanner").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": [
    {
      "url": "https://apify.com"
    }
  ]
}' |
apify call moonweil/broken-link-scanner --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,moonweil/broken-link-scanner"
        }
    }
}

```

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/vTC7eXCzHyDybs490/builds/H7ctk7aLv4ORKPfde/openapi.json
