# Broken Link Resurrector — Find Dead Links & Their Replacements (`alaudinburki/link-resurrector`) Actor

Finds broken links on your pages and, for each one, suggests the fix: the last working capture from the Internet Archive, whether the site itself still exists, and the exact next action. A report you can act on, not a list of 404s.

- **URL**: https://apify.com/alaudinburki/link-resurrector.md
- **Developed by:** [alaudin burki](https://apify.com/alaudinburki) (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 $3.00 / 1,000 results

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

## Broken Link Resurrector — Find Dead Links **and Their Replacements**

Every link checker hands you a list of 404s and walks away. That's the easy half — and it's why the fix
sits in a backlog for a year.

This one does the **hard half**: for each dead link it finds the **last working capture in the Internet
Archive**, checks whether the site itself still exists, and tells you exactly what to do about that
specific link.

**A report becomes a fix list.** Free sources. **No API key, no login, no anti-bot.**

### Why this works differently

A normal link checker outputs:

> `https://oldsite.com/guide` — 404

Which tells you nothing you can act on. Was the page moved, or did the company die? Is there anything
left of it? What did it even say?

This outputs:

> `https://oldsite.com/guide` — **404** · site is still alive · **archived copy from 2019-04-12**
> → *"Site is alive but this page is gone. Search the site for "guide" and point the link at the current
> page; use the archived copy (6.4y old) to confirm what it said."* · confidence **medium**

The difference is `siteStillAlive`. A dead page on a **living** site almost always moved — go find it.
A dead page on a **dead** site is never coming back — swap in the archive link or cut it. Those are two
completely different jobs, and no checker tells you which one you have.

### The four verdicts

| `fixType` | What happened | What you do |
|---|---|---|
| **`archive_replacement`** | An archived capture exists | Paste the archive URL, or use it to find where the page moved |
| **`search_site`** | Page gone, site up, nothing archived | Search that site for the slug — it probably just moved |
| **`remove_or_replace`** | Domain no longer resolves, no archive | Cut the link or find a new source |
| **`remove`** | Nothing left anywhere | Delete it |

Confidence is deliberately **conservative**. An archived copy is evidence of what a page *was*, never
proof of where it *moved to* — so a live site with a dead page is capped at `medium` and asks for a
human to confirm. This actor will not lie to you to look smarter.

### What it does **not** call broken

Most cheap checkers report these as dead and waste your afternoon:

- **`403` / `401`** → `blocked`, not dead. The page is fine; it just refuses bots.
- **`429`** → `rate_limited`, not dead. You checked too fast.
- **`301` / `302`** → `redirect`, not dead — but `redirectedTo` tells you where it now lands, so you can
  update the link anyway and save a hop.

Only `404`, `410`, `4xx`, `5xx` and DNS failures count as dead.

### What you get

| Field | Description |
|---|---|
| `url` · `anchorText` · `foundOn` | The dead link, its link text, and which of your pages it's on |
| `httpStatus` · `state` · `reason` | `ok` · `not_found` · `domain_gone` · `blocked` · `redirect` · `server_error` … |
| **`siteStillAlive`** | The field that decides everything — did the page move, or did the site die? |
| **`fixType`** | One of the four verdicts above |
| **`suggestedUrl`** | The archived replacement, ready to paste |
| **`confidence`** | `high` · `medium` · `low` — honest, not optimistic |
| **`action`** | Plain-language next step, written for *that* link |
| `archivedAt` · `redirectedTo` | When the archive captured it; where a redirect now lands |

### Input

```json
{
  "pages": ["https://yourblog.com/2019/some-old-post"],
  "findReplacements": true,
  "brokenOnly": true
}
```

Or feed URLs straight in (from a sitemap, a crawler, a CSV):

```json
{ "urls": ["https://a.com/gone", "https://b.com/moved"] }
```

Set `brokenOnly: false` to get a **full audit** of every link with a link-rot percentage in the
`QUALITY_REPORT`.

### Sample output

```json
[
  {
    "url": "https://oldstartup.com/pricing",
    "anchorText": "their pricing page",
    "foundOn": "https://yourblog.com/2019/some-old-post",
    "httpStatus": null,
    "state": "domain_gone",
    "dead": true,
    "reason": "domain_not_found",
    "siteStillAlive": false,
    "fixType": "archive_replacement",
    "suggestedUrl": "https://web.archive.org/web/20200114/https://oldstartup.com/pricing",
    "confidence": "high",
    "action": "Whole site is gone. Replace the link with the archived copy, or remove it.",
    "archivedAt": "2020-01-14T09:22:41.000Z",
    "status": "ok"
  }
]
```

### Typical uses

- **Content / SEO audit** — broken outbound links hurt UX and waste crawl budget. This gives your writer
  a paste-ready fix list instead of a spreadsheet of 404s.
- **Documentation upkeep** — docs rot faster than code. Run it over your docs site monthly.
- **Wikipedia / research citations** — dead citations are the classic Wayback use case; this automates it.
- **Site migration cleanup** — after a redesign, find every reference that didn't survive.
- **Agency deliverable** — run it across a client's top 50 pages and sell the fix list. This is a
  genuinely sellable report, which a plain 404 list is not.

### Pricing

**$3.00 / 1,000 links checked** (`$0.003` per result), plus a near-zero start fee. Auditing a 300-link
page costs under a dollar. Never charged beyond `maxItems`.

### ⚠️ Read before you act

- **An archive is not a redirect.** The archived page proves what the URL *used to* contain. Where the
  content lives *now* is a judgment call — that's why `confidence` exists, and why `medium`/`low` rows
  ask for a human.
- **Archived pages can themselves be incomplete** — the Internet Archive often captures HTML without
  images, CSS, or JS. Check the capture before you publish it as a citation.
- **`blocked` (403) is not a pass either.** The link works for humans but a bot can't verify it. If it
  matters, open it yourself.
- A link that is fine today can break tomorrow. This is a **scheduled** job, not a one-off.

### FAQ & limitations

- **Do I need an API key?** No. It uses the public Internet Archive availability API and plain HTTP.
- **Does it crawl my whole site?** No — it checks the pages you give it. Pair it with the **Sitemap
  Extractor** actor to feed in every URL, or pass them in `urls`.
- **Does it find links rendered by JavaScript?** No, it reads server HTML. For JS-rendered pages, pass
  the link URLs directly in `urls`.
- **Why is one link `blocked` instead of dead?** Some sites refuse automated requests (`403`). That's a
  bot policy, not a broken link — so it is reported honestly rather than counted as rot.
- **Can it fix the links for me?** No, and it shouldn't — it gives you the replacement and the reasoning;
  publishing the change stays a human decision.

### Related actors

- **Sitemap Extractor** — get every URL on a site, then feed them in here.
- **Website Screenshot** — capture what a page looks like now versus its archive.
- **HTTP Header Analyzer** · **DNS Lookup** — dig into a specific offender.

# Actor input Schema

## `pages` (type: `array`):

Pages whose outbound links should be checked. Every <a href> on each page is extracted and tested.

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

A list of specific links to check. Use this to feed in output from another crawler, or when the page renders links with JavaScript.

## `findReplacements` (type: `boolean`):

For every dead link, look up the last working capture in the Internet Archive and suggest it as the fix. This is the whole point of the actor — leave it on.

## `brokenOnly` (type: `boolean`):

Return only dead links. Turn this OFF to get a full audit of every link with its status (and a link-rot percentage).

## `skipInternal` (type: `boolean`):

Ignore links pointing at the same domain as the page they were found on, and check only outbound links.

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

Cap on how many links to take from each scanned page.

## `concurrency` (type: `integer`):

How many links to check at once. Lower this if you are checking many links on one host.

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

Hard cap on results. You are never charged beyond this.

## `proxyConfiguration` (type: `object`):

Optional proxy. Not needed for most sites.

## Actor input object example

```json
{
  "pages": [
    "https://en.wikipedia.org/wiki/Link_rot"
  ],
  "findReplacements": true,
  "brokenOnly": true,
  "skipInternal": false,
  "maxLinksPerPage": 300,
  "concurrency": 8,
  "maxItems": 2000
}
```

# Actor output Schema

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

Broken links with their suggested archived replacements.

## `qualityReport` (type: `string`):

Counts by fix type, link-rot percentage, and any lookup problems.

# 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 = {
    "pages": [
        "https://en.wikipedia.org/wiki/Link_rot"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("alaudinburki/link-resurrector").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 = { "pages": ["https://en.wikipedia.org/wiki/Link_rot"] }

# Run the Actor and wait for it to finish
run = client.actor("alaudinburki/link-resurrector").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 '{
  "pages": [
    "https://en.wikipedia.org/wiki/Link_rot"
  ]
}' |
apify call alaudinburki/link-resurrector --silent --output-dataset

```

## MCP server setup

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

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/PSBNdOBc6gIpFGTwx/builds/OAg5t63PbAFAkJjtR/openapi.json
