Website Health Audit: Broken Links & SEO Checker
Pricing
from $5.00 / 1,000 page auditeds
Website Health Audit: Broken Links & SEO Checker
Crawl a website and find broken links, redirect chains, missing or duplicate titles and meta descriptions, missing H1, slow pages and sitemap gaps. A 0-100 score and a fix for every issue. $5 per 1,000 pages.
Pricing
from $5.00 / 1,000 page auditeds
Rating
0.0
(0)
Developer
Helvetic Labs
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
17 days ago
Last modified
Categories
Features
Share
Crawl a website and get a page-by-page report of what is wrong with it: broken links, redirect chains, slow pages, missing or duplicate titles and meta descriptions, missing H1 headings, images without alt text and pages that are missing from the sitemap. Every page gets a score from 0 to 100 and a short fix for every issue. For SEO specialists, agencies, web developers and site owners who need a repeatable website SEO audit and broken link checker.
What is Website Health Audit?
It is a site crawler, broken link checker and on-page SEO audit in one Actor. You give it the address of a site. It starts there, follows the links that stay on the same host, breadth first, and audits each page it reaches, up to a page limit you set. For every page it records the HTTP status, the redirect chain, the response time and the on-page facts that search engines care about. It also checks the links on the page, including links to other sites and to linked files such as PDFs, and compares the crawl with the site's sitemaps.
The result is one dataset item per page, with issues, fixes and a score, plus a site summary per site in the key-value store. Sort by score to see the worst pages first, or open the Broken links view for the list of dead links with the page they sit on.
The audit works on the HTML the server sends. It parses each page in memory and drops it right away: the output has lengths, counts, flags and URLs, never page text.
What does it check?
Per page:
- Status and speed: HTTP status, number of redirects and the full redirect chain, response time in milliseconds, content type, size in bytes and whether the final URL uses HTTPS.
- Title and meta description: present or missing, length in characters, too short or too long, and whether another crawled page has the same one (compared by a hash, the text is never returned).
- Headings: number of
<h1>elements (none, one or several). - Mobile and language basics:
<meta name="viewport">and thelangattribute of<html>. - Canonical: the canonical URL and whether it points to the page itself.
- Robots directives:
noindexandnofollowfrom the robots meta tag and theX-Robots-Tagheader. - Images: how many
<img>elements the page has and how many have noaltattribute. - Links: how many internal and external links the page has, and which of them are broken.
- Sitemap: whether the page is listed in the site's sitemap.
Per site (SUMMARY record): pages audited and average score, pages by status class, broken
internal and external links, how many external links and files were checked, issue counts by
type, average response time, robots.txt status, whether the crawl was complete, and how the
crawl compares with the sitemap.
Issues and score
Each page starts at 100. Each issue takes points off, and the score never goes below 0. The issue list on every page contains the code, the severity and a one-sentence fix.
| Issue code | Severity | Points | Meaning |
|---|---|---|---|
http_error | error | 100 | The page answers with a 4xx or 5xx status. |
unreachable | error | 100 | No answer: timeout, TLS error, connection error or too many redirects. |
broken_links | error | 10 | At least one link on the page is broken. |
missing_title | error | 15 | No <title>. |
title_too_long | warning | 5 | More than 60 characters. |
title_too_short | notice | 2 | Fewer than 15 characters. |
duplicate_title | warning | 5 | Another crawled page has the same title. |
missing_description | warning | 8 | No meta description. |
description_too_long | notice | 2 | More than 160 characters. |
description_too_short | notice | 2 | Fewer than 50 characters. |
duplicate_description | notice | 3 | Another crawled page has the same description. |
missing_h1 | warning | 5 | No <h1>. |
multiple_h1 | notice | 2 | More than one <h1>. |
redirect_chain | warning | 5 | More than one redirect before the final page. |
slow | warning | 5 | The answer took more than 2,000 ms. |
not_https | warning | 10 | The final URL is not HTTPS. |
no_viewport | warning | 5 | No viewport meta tag. |
missing_lang | notice | 2 | No lang attribute on <html>. |
missing_canonical | notice | 2 | No canonical link. |
images_missing_alt | notice | 3 | At least one image has no alt attribute. |
not_in_sitemap | notice | 2 | Indexable page that the sitemap does not list. |
noindex | notice | 0 | The page asks search engines not to index it. Reported, not penalised. |
page_too_large | notice | 2 | The HTML is larger than 2 MB. |
A page with an error status or no answer scores 0 on its own. The score is a quick ranking aid, not a prediction of how a search engine ranks the page.
Use cases
- Technical SEO audit: run a full site audit before a pitch or a project and hand over a sorted list of problems with fixes.
- Broken link checking: find dead internal links and dead links to other sites, with the page they appear on, before visitors and search engines do.
- Site migrations and relaunches: crawl the old and the new site and compare redirects, status codes, titles and canonicals; run again after the switch to catch leftovers.
- Content and metadata clean-up: find missing, too long and duplicate titles and meta descriptions across hundreds of pages in one pass.
- Sitemap hygiene: find indexable pages the sitemap forgot, and sitemap URLs that no page links to.
- Monitoring: schedule a weekly audit and watch the average score and the number of broken links over time.
- Agency reporting: one run per client site, exported to a spreadsheet or pushed to a dashboard through the API.
- Pre-launch check: audit a staging site that is publicly reachable to catch missing
titles,
noindextags and mixed redirects before go-live.
How to use Website Health Audit
- Open the Actor in Apify Console and click Try for free or Start.
- Enter the site's address in Sites (
https://www.example.com/). You can add several sites; each is audited on its own. - Set Max pages per site. The default is 100. You pay per page audited, so start small on an unknown site.
- Decide whether to check external links and whether to compare with the sitemap. Both are on by default.
- Set a maximum cost per run in the run options if you want a hard spending limit.
- Click Start. The status message shows the page count and the link checking progress.
- Open the Output tab. The Overview view lists pages with score, status, broken links
and key SEO numbers; Broken links and Issues are two more views. Download everything
as JSON, CSV, Excel, XML or HTML. The site summary is the
SUMMARYrecord of the run's key-value store (linked in the output tab). - To monitor a site, create a Schedule with the same input, for example weekly.
Input
| Field | Type | Default | What it does |
|---|---|---|---|
startUrls | array of strings | required | Where the crawl starts. Up to 100 different sites per run; exact duplicates are removed. The crawl follows links on the same host only. Objects like {"url": "https://example.com"} also work. |
maxPagesPerSite | integer | 100 | Stop after this many pages for one site, from 1 to 10,000. |
checkExternalLinks | boolean | true | Also check links to other sites, once each, with a HEAD request first. Included in the page price. |
maxExternalLinksPerSite | integer | 300 | Check at most this many different external links per site, from 0 to 2,000. |
compareWithSitemap | boolean | true | Read the site's sitemaps and flag pages that are missing from them. |
timeoutSecs | integer | 20 | How long to wait for each response, from 5 to 120 seconds. |
Tips:
- Start with a small limit (25 to 100 pages) to see what the audit finds and how big the site is. Raise the limit for the real run.
- The crawl stays on one host.
www.example.comandexample.com, or ablog.subdomain, count as different hosts: links to them are treated as external links. If the start URL redirects to another host (for example fromexample.comtowww.example.com), the crawl follows it and audits that host. - Query strings count. Two URLs that differ in a query string are two pages. A site with endless filter or calendar URLs can use up your page limit, so keep the limit sensible.
- Fragments are ignored and
mailto:,tel:andjavascript:links are not followed or checked. - Raise
timeoutSecsfor slow servers; lower it to fail fast. - Set
maxExternalLinksPerSiteto 0 or turn offcheckExternalLinksif you only care about your own pages. The links to other sites are then not requested and show up asunchecked_links.
{"startUrls": ["https://www.example.com/"],"maxPagesPerSite": 500,"checkExternalLinks": true,"maxExternalLinksPerSite": 300,"compareWithSitemap": true,"timeoutSecs": 20}
Output
One dataset item per audited page, in crawl order. Example (shortened where noted):
{"site": "https://www.example.com/","url": "https://www.example.com/pricing","final_url": "https://www.example.com/pricing","final_url_audited_elsewhere": false,"depth": 1,"status": 200,"error": "","redirects": 0,"redirect_chain": [],"response_ms": 412,"content_type": "text/html","bytes": 48213,"https": true,"title_length": 71,"description_length": null,"duplicate_title": false,"duplicate_description": false,"h1_count": 1,"lang": "en","viewport": true,"images": 12,"images_missing_alt": 3,"canonical": "https://www.example.com/pricing","canonical_is_self": true,"noindex": false,"nofollow": false,"internal_links": 34,"external_links": 6,"unchecked_links": 0,"broken_link_count": 1,"broken_links": [{"url": "https://old-partner.example.net/logo", "status": 404, "error": "", "internal": false}],"in_sitemap": true,"issues": [{"code": "title_too_long", "severity": "warning", "fix": "Shorten the title to at most 60 characters; search engines cut it."},{"code": "missing_description", "severity": "warning", "fix": "Add a meta description (50 to 160 characters)."},{"code": "images_missing_alt", "severity": "notice", "fix": "Give images an alt attribute (empty for decoration)."},{"code": "broken_links", "severity": "error", "fix": "Links on this page lead to missing pages or dead hosts: fix or remove them."}],"score": 74,"checked_at": "2026-10-20T08:21:07Z"}
The score is 100 minus 5 (title), 8 (description), 3 (alt text) and 10 (broken links).
Output fields
| Field | Type | Meaning |
|---|---|---|
site | string | The site (origin) the page belongs to. |
url | string | The page URL as the crawler requested it. E-mail addresses inside URLs are replaced by [email]. |
final_url | string or null | The URL after redirects; null when there was no answer. |
final_url_audited_elsewhere | boolean | true when this URL redirects to a page that was audited under another URL. Such items are listed but free and have no on-page facts. |
depth | integer | Clicks from the start page; the start page is 0. |
status | integer or null | HTTP status of the final response; null when there was no answer. |
error | string | Empty, or why there was no answer: timeout, connection_error, tls_error, protocol_error, http_error (any other HTTP-level failure), dns_error or too_many_redirects. |
redirects | integer | Number of redirects followed (at most 10). |
redirect_chain | array | Each hop with its url and status. |
response_ms | integer or null | Time in milliseconds for the request, redirects included. |
content_type | string or null | Media type without parameters, for example text/html. |
bytes | integer or null | Size of the response body that was read (at most 2 MB). |
https | boolean | The final URL is HTTPS. |
title_length | integer or null | Characters in the <title>; null if missing or the page was not parsed. |
description_length | integer or null | Characters in the meta description; null if missing or not parsed. |
duplicate_title | boolean | Another crawled page has the same title. |
duplicate_description | boolean | Another crawled page has the same description. |
h1_count | integer or null | Number of <h1> elements; null if not parsed. |
lang | string or null | The lang attribute of <html> (first 20 characters). |
viewport | boolean or null | Whether a viewport meta tag exists. |
images | integer or null | Number of <img> elements. |
images_missing_alt | integer or null | <img> elements without an alt attribute. |
canonical | string or null | The canonical URL, resolved to an absolute URL. |
canonical_is_self | boolean or null | Whether the canonical equals the final URL; null without a canonical. |
noindex | boolean | noindex in the robots meta tag or X-Robots-Tag. |
nofollow | boolean | nofollow in the robots meta tag or X-Robots-Tag. Links on such a page are counted but never requested. |
internal_links | integer | Different links on the page to the same host. |
external_links | integer | Different links to other hosts. |
unchecked_links | integer | Links whose status is unknown: beyond the external link limit, not crawled because of the page limit or the time limit, or on a nofollow page. |
broken_link_count | integer | Number of broken links on the page. |
broken_links | array | Up to 50 broken links with url, status, error and internal. |
in_sitemap | boolean or null | Whether the page is in the sitemap; null when no sitemap URLs were found or the comparison is off. |
issues | array | Each issue with code, severity and fix. |
score | integer | 100 minus the issue points, at least 0. |
checked_at | string | UTC time of the check. |
Pages that are not HTML (a PDF reached through a link without a file extension, for example)
or that did not answer with status 200 have null on-page facts, because there is no HTML to
measure.
What counts as a broken link
- Links to your own site: any error status from 400 up, plus requests that failed (DNS error, connection error, TLS error, timeout, protocol error, too many redirects).
- Links to other sites: only 404, 410 and 5xx statuses and failed requests. Other sites often answer a bot with 400 or a special code, and that is not a sign of a dead link.
- Never broken: 401, 403 and 429 ("not for you", not "gone"), and links the target's
robots.txtdoes not allow this Actor to request.
The SUMMARY record
The key-value store record SUMMARY has one entry per site, keyed by the site's address. Example:
{"https://www.example.com/": {"site": "https://www.example.com/","pages_audited": 100,"redirects_to_audited_pages": 4,"crawl_complete": false,"stopped_by": "page_limit","average_score": 86,"pages_by_status": {"2xx": 101, "4xx": 3},"skipped": {"blocked_by_robots": 2},"robots_txt": "parsed","redirects_off_site": 1,"broken_internal_links": 3,"broken_external_links": 5,"external_links_checked": 212,"external_links_unchecked": 0,"files_checked": 18,"files_unchecked": 0,"average_response_ms": 388,"issues": {"missing_description": 31, "missing_canonical": 30, "broken_links": 6},"sitemap": {"found_from": "robots.txt","urls": 140,"crawled_in_sitemap": 97,"crawled_not_in_sitemap": 3,"in_sitemap_not_linked": null},"checked_at": "2026-10-20T08:24:51Z"}}
| Field | Meaning |
|---|---|
pages_audited | Pages audited and charged. |
redirects_to_audited_pages | URLs listed free because they redirect to a page audited under another URL. |
crawl_complete | false when the crawl stopped early. |
stopped_by | page_limit or time_limit when it stopped early, otherwise null. |
average_score | Average page score over the audited pages; null without pages. |
pages_by_status | Items by status class (2xx, 3xx, 4xx, 5xx, or error). |
skipped | Pages that were not requested, by reason (for example blocked_by_robots). |
robots_txt | parsed, missing or unreachable. |
redirects_off_site | Own URLs that redirect to another host. |
broken_internal_links, broken_external_links | Different broken link targets found. |
external_links_checked, external_links_unchecked | External links checked, and left unchecked by the limit or the time. |
files_checked, files_unchecked | Linked files (PDFs, images, archives, scripts and similar) checked with a HEAD request, up to 500 per site. |
average_response_ms | Average response time. |
issues | Number of pages per issue code. |
sitemap | Where the sitemap was found, how many URLs it lists, how many crawled pages are in it, how many crawled indexable pages are not, and how many sitemap URLs the crawl did not reach (only when the crawl was complete). |
A site that could not be audited has an entry like {"error": "invalid_url"} or
{"skipped": "this site was audited already in this run"} instead.
How much does it cost?
This Actor uses pay-per-event pricing: you pay USD 0.005 per page audited (USD 5.00 per 1,000 pages), plus Apify's standard actor start event of USD 0.00005 per run. With pay-per-event pricing the platform usage of the run is included in these prices.
| Pages audited | Page events | Actor start | Total per run |
|---|---|---|---|
| 100 | USD 0.50 | USD 0.00005 | USD 0.50005 |
| 1,000 | USD 5.00 | USD 0.00005 | USD 5.00005 |
| 10,000 | USD 50.00 | USD 0.00005 | USD 50.00005 |
| 100,000 | USD 500.00 | USD 0.00005 per run | USD 500.00005 for one run, more if split into several |
What is charged and what is free:
- Charged: each page that was requested and audited, including pages that answer with an error status or not at all.
- Included in the page price: the checks of links to other sites and of linked files, the
comparison with the sitemap and the
SUMMARYrecord. - Free: pages that
robots.txtdoes not allow or that are not public, URLs that redirect to a page already audited (listed in the dataset withfinal_url_audited_elsewhere: true), and inputs that are invalid or not public sites.
Set a maximum cost per run and the Actor stops when the limit is reached; the crawl never audits more pages than the limit pays for. Max pages per site caps each site on top of that. Apify's free plan includes monthly platform credit, which you can use to try the Actor.
Tips and best practices
- Work from the worst page up. Sort the dataset by
score, or open the Issues view. - Fix in bulk. The
SUMMARY.issuescounts tell you which problem is most common, such as 31 pages without a meta description, and often one template change fixes all of them. - Watch
unchecked_links. A high value means the page limit, the external link limit or the time limit cut the checking short. - Compare with the sitemap.
in_sitemap_not_linkedshows sitemap URLs that no crawled page links to; it needs a complete crawl, so raisemaxPagesPerSiteuntilcrawl_completeistrue. - Pair it with the other tools. Check long URL lists with the URL Status & Redirect Checker, and get the full sitemap list with the Sitemap URL Extractor.
- Schedule it and compare
average_scoreandbroken_internal_linksbetween runs.
Limitations
- HTML as served. The audit reads the HTML the server returns and runs no JavaScript. On a single-page app that builds its content in the browser, titles, headings and links may not be visible to the audit.
- robots.txt is obeyed. Pages it disallows are skipped, are not charged and are counted in
skipped. Ifrobots.txtcannot be read, nothing on that host is requested. A page withnofollow(meta tag orX-Robots-Tag) has its links counted but not requested. - Links only from
<a>and<area>. Images, scripts and style sheets are not checked for being broken. Images are counted only for missingaltattributes. Files linked with<a>are checked with a HEAD request. - Page size. At most the first 2 MB of a page's HTML is read; larger pages get
page_too_large. - Caps. Up to 100 sites per run, 10,000 pages per site, 2,000 external links per site and 500 linked files per site. If a site has more, the rest is reported as unchecked.
- Polite pace. Pages are requested one after another, with at least half a second between
two requests to the same host, and longer if
robots.txtasks for a crawl delay (up to 30 seconds). A site of 1,000 pages therefore needs at least about eight minutes for the crawl alone. - Time limit. The default run timeout is four hours. The Actor stops the crawl in time to
check the remaining links and save the results, and reports
stopped_by: time_limit. Audit very large sites in several runs. - Public sites only. Hosts on private or non-public addresses are refused, and URLs with a user name or password are not requested. Pages behind a login are not audited.
- Automated checks. Results can be incomplete and are no guarantee for rankings.
FAQ
Is it legal to use? The Actor requests public pages like a search engine bot would,
respects robots.txt, identifies itself and keeps a polite pace. It does not store page text
and does not return e-mail addresses. Audit only sites you own or may check, and see the fair
use rules at the end of this page.
Does it respect robots.txt? Yes, for every request, including the links to other sites. The
user agent is HelveticLabs-WebCheck, with a link to the provider's imprint.
Does it store personal data? No. The output has URLs, status codes, lengths, counts and
flags. E-mail addresses inside URLs are replaced by [email], and mailto: links are ignored.
How fast is it? Pages are fetched one at a time per host with a pause of at least half a second, so the speed depends on the site and on your page limit. A small site is done within minutes; check the status message for progress.
Can it audit a site that needs JavaScript? It audits the HTML as served, without running JavaScript. For many sites that is what search engines see first, but content added in the browser is not seen.
Why was a page not charged? Free are pages blocked by robots.txt, URLs that redirect to a
page that was audited under another URL, and invalid inputs. Everything that was requested and
audited is charged, including 404 pages.
Why does title_length say null? The page has no <title>, or it was not an HTML page
with status 200 and so was not parsed. Check status and content_type.
Why are external links reported as unchecked? Because of maxExternalLinksPerSite, because
checkExternalLinks is off, because of the time limit, or because the page has nofollow.
Can I schedule it? Yes, with a schedule in Apify Console, daily or weekly. A webhook can start the next step when a run finishes.
Can I use it from Python, JavaScript or the API? Yes, see Integrations below.
Can AI agents use it? Yes, through Apify's MCP server. An agent can run an audit and read the worst pages and the summary.
What happens on errors and timeouts? A page that fails is audited with its error and
the crawl goes on. A site that cannot be audited gets an error entry in SUMMARY. Before the run's
timeout, the Actor stops crawling by itself and saves what it has; a site that is not reached
in time is listed in SUMMARY as skipped. Each site's pages are pushed once its crawl is done,
so give large sites enough run time.
Integrations
The actor ID is HelveticLabs/website-health-audit.
Python (pip install apify-client):
from decimal import Decimalfrom apify_client import ApifyClientclient = ApifyClient("<YOUR_APIFY_TOKEN>")run = client.actor("HelveticLabs/website-health-audit").call(run_input={"startUrls": ["https://www.example.com/"],"maxPagesPerSite": 200,"checkExternalLinks": True,},max_total_charge_usd=Decimal("2"), # optional spending limit for this run)# apify-client 3.x; with older versions use run["defaultDatasetId"] and run["defaultKeyValueStoreId"]pages = list(client.dataset(run.default_dataset_id).iterate_items())summary = client.key_value_store(run.default_key_value_store_id).get_record("SUMMARY")worst = sorted(pages, key=lambda p: p["score"])[:10]print([(p["url"], p["score"]) for p in worst])
JavaScript (npm install apify-client):
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });const run = await client.actor('HelveticLabs/website-health-audit').call({ startUrls: ['https://www.example.com/'], maxPagesPerSite: 200, checkExternalLinks: true },{ maxTotalChargeUsd: 2 },);const { items } = await client.dataset(run.defaultDatasetId).listItems();const summary = await client.keyValueStore(run.defaultKeyValueStoreId).getRecord('SUMMARY');console.log(items.filter((p) => p.broken_link_count > 0).length, summary?.value);
HTTP API: POST https://api.apify.com/v2/acts/HelveticLabs~website-health-audit/runs with the
input as the JSON body; add maxTotalChargeUsd as a query parameter to cap the cost.
No-code and automation: connect the Actor to Make, Zapier or n8n, or add a webhook that
starts the next step when a run finishes, for example a message to your team when
broken_internal_links is above zero.
AI agents (MCP): Apify's MCP server lets AI agents and MCP clients run this Actor as a tool;
pass maxTotalChargeUsd in the call options to cap the spend.
Related HelveticLabs actors
| Actor | What it does |
|---|---|
| URL Status & Redirect Checker | Status code, redirect chain, final URL and response time for up to 100,000 URLs. |
| Sitemap URL Extractor & Validator | Every URL a site lists in its sitemaps, with validation. |
| Website Security Headers & TLS Audit | A+ to F security grade: HTTPS, headers, SSL certificate, SPF and DMARC. |
| AI Crawler Access Checker | Which AI bots a site's robots.txt allows, plus llms.txt. |
Support and feedback
A page that was judged wrongly, a check you miss, or a site the audit cannot read? Open an issue in the Issues tab on this Actor's page, or write to contact@helveticlabs.org. Include the run ID and the site if you can.
Provider, privacy and fair use
Provider: HelveticLabs, sole proprietorship of Marvin Krieg, c/o SwissMailBox 170, 12 Rue Le Corbusier, 1208 Genève, Switzerland. E-mail: contact@helveticlabs.org. Imprint: https://www.helveticlabs.org/imprint
Privacy: This Actor runs on the Apify platform, in your Apify account. It processes only the URLs or domains you enter and the public technical data the checked servers return (status codes, headers, certificates, DNS records, sitemap URLs, links). It does not collect personal data: it never stores or returns page text (where a page's HTML is read, it is read in memory only, to find links and measure tags), and it leaves out e-mail addresses, cookie names and values and similar values. Your input and results stay in your Apify account and are stored by Apify under its own terms (https://apify.com/privacy-policy). The provider sees only the usage statistics Apify shows to Actor authors (such as the number of users and runs), not your input or results, unless you share a run for support. Questions about privacy: contact@helveticlabs.org.
Fair use: Check only sites you own or may check. Do not use the results to attack systems or people. The Actor respects robots.txt, identifies itself, and keeps a polite pace per site.
No warranty: The results are automated checks and can be incomplete. They are not a penetration test, legal advice or a compliance certificate.