The Problem: Not All Dead Links Return 404
When a webpage goes down, most developers expect a 404 Not Found response. But many services don't play by those rules.
Soft 404s return 200 OK with error content:
Request: GET https://example.com/deleted-page
Response: 200 OK
Body: <html>...Page not found...</html>
Standard uptime monitors see 200 OK and report everything as healthy. The link is actually dead.
This happens everywhere:
- Any website: Custom error pages, removed content, expired pages
- Documentation sites: Outdated links, reorganized docs, deprecated APIs
- E-commerce: Out-of-stock products, discontinued items
- File hosts: K2S, Nitroflare, Rapidgator, MEGA, MediaFire
- Video platforms: YouTube ("Video unavailable"), Vimeo, Dailymotion
- Social media: Deleted posts, suspended accounts
- Cloud storage: Expired Google Drive shares, revoked permissions
- APIs & webhooks: Deprecated endpoints, changed URLs
Anyone managing external links—whether it's a documentation site, resource page, or link directory—has likely experienced users reporting dead links that monitoring tools missed.
The Solution: Universal Link Monitoring
DeadLinkRadar monitors any URL with smart dead link detection. It works in two ways:
- Universal monitoring: Any URL gets checked with smart soft-404 detection
- Deep integrations: 30+ platforms (YouTube, file hosts, social media) get specialized detection
This tutorial covers:
- Building a JavaScript API client
- Bulk importing links (any URL type)
- Setting up Discord and Telegram alerts
- Automating daily link audits
Building the API Client
First, create an API key from the DeadLinkRadar dashboard. Then build a client class:
// deadlinkradar-client.js
const API_BASE = 'https://deadlinkradar.com/api/v1'
class DeadLinkRadarClient {
constructor(apiKey) {
this.apiKey = apiKey
}
async request(endpoint, options = {}) {
const response = await fetch(`${API_BASE}${endpoint}`, {
...options,
headers: {
'X-API-Key': this.apiKey,
'Content-Type': 'application/json',
...options.headers,
},
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error?.message || 'API request failed')
}
return response.json()
}
// Account & Usage
getAccount = () => this.request('/account')
getUsage = () => this.request('/usage')
// Links
getLinks = (params = {}) => {
const query = new URLSearchParams(params).toString()
return this.request(`/links${query ? `?${query}` : ''}`)
}
createLink = (url, groupId = null) =>
this.request('/links', {
method: 'POST',
body: JSON.stringify({ url, group_id: groupId }),
})
deleteLink = (id) => this.request(`/links/${id}`, { method: 'DELETE' })
checkLinkNow = (id) => this.request(`/links/${id}/check`, { method: 'POST' })
getLinkHistory = (id) => this.request(`/links/${id}/history`)
// Batch operations
batchCreateLinks = (links) =>
this.request('/links/batch', {
method: 'POST',
body: JSON.stringify({ action: 'create', links }),
})
// Groups
getGroups = () => this.request('/groups')
createGroup = (name, description = '') =>
this.request('/groups', {
method: 'POST',
body: JSON.stringify({ name, description }),
})
// Webhooks
getWebhooks = () => this.request('/webhooks')
createWebhook = (url, events = ['link.dead']) =>
this.request('/webhooks', {
method: 'POST',
body: JSON.stringify({ url, events }),
})
testWebhook = (id) => this.request(`/webhooks/${id}/test`, { method: 'POST' })
}
export default DeadLinkRadarClient
Importing Links
With the client ready, bulk import links from any source—websites, APIs, file hosts, anything:
import DeadLinkRadarClient from './deadlinkradar-client.js'
const client = new DeadLinkRadarClient(process.env.DEADLINKRADAR_API_KEY)
// Mix of link types - all supported
const linksToMonitor = [
// Regular websites
'https://docs.example.com/api/v2/authentication',
'https://blog.company.com/2024/product-launch',
'https://partner-site.com/integration-guide',
// APIs and webhooks
'https://api.stripe.com/v1/status',
'https://hooks.slack.com/services/T00/B00/XXX',
// File hosting (deep integration)
'https://k2s.cc/file/abc123/archive.zip',
'https://mega.nz/file/ABC123',
'https://drive.google.com/file/d/1234/view',
// Video platforms (deep integration)
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
'https://vimeo.com/123456789',
// Social media (deep integration)
'https://bsky.app/profile/user.bsky.social/post/abc123',
'https://github.com/owner/repo',
]
async function importLinks() {
// Create groups to organize different link types
const { data: docsGroup } = await client.createGroup(
'Documentation',
'External documentation and API references',
)
const { data: downloadsGroup } = await client.createGroup(
'Downloads',
'File hosting and download links',
)
console.log(`Created groups: ${docsGroup.name}, ${downloadsGroup.name}`)
// Batch import (up to 1000 URLs per request)
// Format: array of objects with url (required), group_id (optional), check_frequency (optional)
const linksPayload = linksToMonitor.map((url) => ({ url }))
const { data: result } = await client.batchCreateLinks(linksPayload)
console.log(`Imported: ${result.success}`)
console.log(`Failed: ${result.failed}`)
if (result.errors?.length) {
console.log('Errors:', result.errors)
}
if (result.created?.length) {
console.log(
`Created links:`,
result.created.map((l) => l.url),
)
}
}
importLinks()
How Detection Works
DeadLinkRadar automatically determines the best detection strategy for each URL:
| URL Type | Detection Method |
|---|---|
| Known platforms (YouTube, K2S, etc.) | Specialized platform detection |
| Any other URL | Smart soft-404 detection |
This catches most dead links even on sites without proper 404 responses.
Setting Up Discord Alerts
Discord webhooks provide instant notifications when any link dies. Create a webhook in Discord (Server Settings → Integrations → Webhooks), then register it:
import DeadLinkRadarClient from './deadlinkradar-client.js'
const client = new DeadLinkRadarClient(process.env.DEADLINKRADAR_API_KEY)
async function setupDiscordAlerts() {
const discordWebhookUrl = process.env.DISCORD_WEBHOOK_URL
// Register webhook for dead link and recovery events
const { data: webhook } = await client.createWebhook(discordWebhookUrl, [
'link.dead',
'link.alive',
])
console.log('Discord webhook registered')
console.log(`Webhook ID: ${webhook.id}`)
console.log(`Signing secret: ${webhook.signing_secret}`) // Store securely
// Send a test notification
await client.testWebhook(webhook.id)
console.log('Test notification sent')
}
setupDiscordAlerts()
When a link dies, Discord receives the webhook payload:
{
"event": "link.dead",
"timestamp": "2024-01-15T10:30:00Z",
"webhook_id": "550e8400-e29b-41d4-a716-446655440000",
"data": {
"linkId": "550e8400-e29b-41d4-a716-446655440001",
"url": "https://docs.example.com/api/v2/authentication",
"previousStatus": "active",
"currentStatus": "dead",
"checkedAt": "2024-01-15T10:30:00Z"
}
}
Setting Up Telegram Alerts
For Telegram notifications, create a webhook receiver that forwards to the Telegram Bot API. This can run on Vercel, Cloudflare Workers, or any serverless platform:
// api/telegram-forwarder.js
const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN
const TELEGRAM_CHAT_ID = process.env.TELEGRAM_CHAT_ID
export default async function handler(req) {
if (req.method !== 'POST') {
return new Response('Method not allowed', { status: 405 })
}
const payload = await req.json()
// Format the message
const emoji = payload.event === 'link.dead' ? '🔴' : '🟢'
const status = payload.event === 'link.dead' ? 'Dead Link' : 'Link Alive'
const message = `
${emoji} *${status}*
*URL:* \`${payload.data.url}\`
*Previous:* ${payload.data.previousStatus}
*Checked:* ${new Date(payload.data.checkedAt).toLocaleString()}
`.trim()
// Forward to Telegram
await fetch(`https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: TELEGRAM_CHAT_ID,
text: message,
parse_mode: 'Markdown',
}),
})
return new Response('OK', { status: 200 })
}
Register the forwarder endpoint as a webhook:
await client.createWebhook('https://your-domain.com/api/telegram-forwarder', [
'link.dead',
'link.alive',
])
To get the Telegram bot token and chat ID:
- Create a bot with @BotFather
- Start a chat with the bot
- Get the chat ID from
https://api.telegram.org/bot<TOKEN>/getUpdates
Querying Link Status
Check the current state of monitored links:
import DeadLinkRadarClient from './deadlinkradar-client.js'
const client = new DeadLinkRadarClient(process.env.DEADLINKRADAR_API_KEY)
async function getDeadLinks() {
// Fetch all dead links with pagination
const { data: links, pagination } = await client.getLinks({
status: 'dead',
per_page: 100,
})
console.log(`Found ${pagination.total} dead links:\n`)
for (const link of links) {
console.log(`URL: ${link.url}`)
console.log(`Service: ${link.service}`) // "generic" for regular URLs
console.log(`Last checked: ${link.last_checked_at}`)
console.log('---')
}
}
getDeadLinks()
Filter by service, group, or search:
// Get all dead links from regular websites (generic detection)
const genericLinks = await client.getLinks({
status: 'dead',
service: 'generic',
})
// Get all dead YouTube links (specialized detection)
const youtubeLinks = await client.getLinks({
status: 'dead',
service: 'youtube',
})
// Get links in a specific group
const groupLinks = await client.getLinks({
group_id: '550e8400-e29b-41d4-a716-446655440000',
})
// Search by URL substring
const searchResults = await client.getLinks({
search: 'docs.example.com',
})
Investigating Link History
When a link dies, check its history to understand when and why:
async function investigateLink(linkId) {
const { data: history } = await client.getLinkHistory(linkId)
console.log('Check history:\n')
for (const check of history) {
const emoji = check.status === 'active' ? '🟢' : '🔴'
console.log(`${emoji} ${check.checked_at}`)
console.log(` Status: ${check.status}`)
console.log(` Response time: ${check.response_time_ms}ms`)
if (check.error_message) {
console.log(` Error: ${check.error_message}`)
}
console.log()
}
}
investigateLink('550e8400-e29b-41d4-a716-446655440000')
Automated Daily Audit
Combine everything into a daily audit script that posts a summary to Discord:
import DeadLinkRadarClient from './deadlinkradar-client.js'
const client = new DeadLinkRadarClient(process.env.DEADLINKRADAR_API_KEY)
const DISCORD_WEBHOOK = process.env.DISCORD_SUMMARY_WEBHOOK
async function dailyAudit() {
// Fetch usage and dead links
const { data: usage } = await client.getUsage()
const { data: deadLinks, pagination } = await client.getLinks({
status: 'dead',
per_page: 100,
})
// Group dead links by service
const byService = deadLinks.reduce((acc, link) => {
acc[link.service] = acc[link.service] || []
acc[link.service].push(link)
return acc
}, {})
const summary = {
monitored: usage.links.used,
dead: pagination.total,
byService: Object.entries(byService).map(([service, links]) => ({
service,
count: links.length,
})),
}
console.log('Daily Audit Summary')
console.log(`Monitored: ${summary.monitored}`)
console.log(`Dead: ${summary.dead}`)
// Post to Discord
if (DISCORD_WEBHOOK) {
const embed = {
title: '📊 Daily Link Audit',
color: summary.dead > 0 ? 0xff0000 : 0x00ff00,
fields: [
{ name: 'Monitored', value: summary.monitored.toString(), inline: true },
{ name: 'Dead', value: summary.dead.toString(), inline: true },
],
timestamp: new Date().toISOString(),
}
if (summary.dead > 0) {
embed.fields.push({
name: 'By Type',
value: summary.byService.map((s) => `**${s.service}**: ${s.count}`).join('\n'),
})
}
await fetch(DISCORD_WEBHOOK, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ embeds: [embed] }),
})
console.log('Summary posted to Discord')
}
return summary
}
dailyAudit()
Run this with a cron job or scheduled task:
# crontab - run daily at 9am
0 9 * * * node /path/to/daily-audit.js
Webhook Signature Verification
For production use, verify webhook signatures to ensure requests come from DeadLinkRadar:
import crypto from 'crypto'
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex')
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))
}
// In the webhook handler
export default async function handler(req) {
const signatureHeader = req.headers.get('x-deadlinkradar-signature')
// Format: "sha256=<hex>"
const signature = signatureHeader?.replace('sha256=', '')
const payload = await req.json()
if (!signature || !verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) {
return new Response('Invalid signature', { status: 401 })
}
// Process the webhook...
}
Real-World Example: Docusaurus Documentation Monitor
Documentation sites often link to external APIs, libraries, and resources that can go offline without notice. Here's a complete example for monitoring all external links in a Docusaurus documentation site.
Step 1: Extract Links from Markdown Files
First, create a utility to recursively scan documentation files and extract external URLs:
// extract-docs-links.js
import fs from 'fs'
import path from 'path'
/**
* Recursively find all markdown files in a directory
*/
function findMarkdownFiles(dir, files = []) {
const entries = fs.readdirSync(dir, { withFileTypes: true })
for (const entry of entries) {
const fullPath = path.join(dir, entry.name)
if (entry.isDirectory()) {
findMarkdownFiles(fullPath, files)
} else if (entry.name.match(/\.(md|mdx)$/)) {
files.push(fullPath)
}
}
return files
}
/**
* Extract external links from markdown content
*/
function extractLinks(content, filePath) {
const links = []
// Match markdown links: [text](url)
const linkRegex = /\[([^\]]*)\]\(([^)]+)\)/g
let match
while ((match = linkRegex.exec(content)) !== null) {
const url = match[2]
// Only include external https:// links
if (url.startsWith('https://') || url.startsWith('http://')) {
links.push({
url,
text: match[1],
file: filePath,
section: path.dirname(filePath).split(path.sep).pop() || 'root',
})
}
}
return links
}
/**
* Extract all external links from Docusaurus docs directory
*/
export function extractLinksFromDocs(docsDir = './docs') {
const markdownFiles = findMarkdownFiles(docsDir)
const allLinks = []
for (const file of markdownFiles) {
const content = fs.readFileSync(file, 'utf-8')
const links = extractLinks(content, file)
allLinks.push(...links)
}
// Deduplicate by URL
const uniqueUrls = new Map()
for (const link of allLinks) {
if (!uniqueUrls.has(link.url)) {
uniqueUrls.set(link.url, link)
}
}
return Array.from(uniqueUrls.values())
}
Step 2: Setup Monitoring with Groups
Now import the links into DeadLinkRadar, organized by documentation section:
// setup-docs-monitoring.js
import DeadLinkRadarClient from './deadlinkradar-client.js'
import { extractLinksFromDocs } from './extract-docs-links.js'
const client = new DeadLinkRadarClient(process.env.DEADLINKRADAR_API_KEY)
async function setupDocsMonitoring() {
console.log('Extracting links from documentation...')
const links = extractLinksFromDocs('./docs')
console.log(`Found ${links.length} unique external links`)
// Get unique sections
const sections = [...new Set(links.map((l) => l.section))]
// Create a group for each documentation section
const groupIds = {}
for (const section of sections) {
const { data: group } = await client.createGroup(
`docs-${section}`,
`External links from ${section} documentation`,
)
groupIds[section] = group.id
console.log(`Created group: ${group.name}`)
}
// Prepare links with group assignments
const linksPayload = links.map((link) => ({
url: link.url,
group_id: groupIds[link.section] || null,
}))
// Batch import (API supports up to 1000 per request)
const { data: result } = await client.batchCreateLinks(linksPayload)
console.log(`\nImport complete:`)
console.log(` Imported: ${result.success}`)
console.log(` Failed: ${result.failed}`)
if (result.errors?.length) {
console.log(` Errors:`, result.errors.slice(0, 5))
}
// Set up webhook for dead link alerts
const webhookUrl = process.env.DOCS_ALERT_WEBHOOK_URL
if (webhookUrl) {
const { data: webhook } = await client.createWebhook(webhookUrl, ['link.dead', 'link.alive'])
console.log(`\nWebhook configured: ${webhook.id}`)
console.log(`Signing secret: ${webhook.signing_secret}`)
}
return result
}
setupDocsMonitoring().catch(console.error)
Running the Monitor
# Set environment variables
export DEADLINKRADAR_API_KEY="your-api-key"
export DOCS_ALERT_WEBHOOK_URL="https://your-server.com/api/docs-webhook"
# Run the setup script
node setup-docs-monitoring.js
Example output:
Extracting links from documentation...
Found 47 unique external links
Created group: docs-getting-started
Created group: docs-api-reference
Created group: docs-guides
Import complete:
Imported: 47
Failed: 0
Webhook configured: 550e8400-e29b-41d4-a716-446655440000
Signing secret: whsec_abc123...
Once configured, DeadLinkRadar checks each link daily. When a documentation link dies, the webhook fires with the full context—allowing automated Slack alerts, GitHub issues, or custom notification flows.
The free tier includes 25 links with daily checks—enough to validate the solution before scaling up.
Summary
Traditional uptime monitors fail because they only check HTTP status codes. DeadLinkRadar provides:
- Universal monitoring for any URL with smart soft-404 detection
- Deep integrations for 30+ platforms (YouTube, file hosts, social media) with specialized detection
- REST API for programmatic link management
- Webhooks for Discord, Slack, Telegram, and custom integrations
- Batch operations for bulk imports
- Link groups for organization
- Check history for debugging
Whether monitoring documentation links, API endpoints, download pages, or social media posts—the JavaScript client above covers the full API surface. Adapt it to any workflow.
Resources:
Top comments (0)