lintlab SEO Site QA
透過 Apify 的 MCP 伺服器運作的 MCP 工具,可爬取公開網站或其 XML 站點地圖,並回傳每個頁面的失效內部連結與技術性 SEO 問題:重複的標題、缺少 meta description、canonical 與標題標籤問題,每項皆附建議修正方式。僅支援 HTTP,不執行 JavaScript 渲染。依 Apify 上檢查的頁數計費。
託管 MCP 伺服器
npx add-mcp 'https://mcp.apify.com?tools=lintlab/seo-site-qa'可安裝到 Claude Code、Codex、Cursor 等客戶端
文件
Broken Link Checker & Technical SEO Audit (lintlab/seo-site-qa) Actor
Broken link checker and technical SEO audit for public sites: find broken internal links, duplicate titles, missing meta descriptions, canonical, heading, robots and hreflang issues. Crawl from a URL or XML sitemap for per-page fixes and a site summary. HTTP-only SEO crawler, no JavaScript.
- URL: https://apify.com/lintlab/seo-site-qa.md
- Developed by: lintlab (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.20 / 1,000 page auditeds
This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events. Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.
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 section below.
Each client library has reference documentation the quickstart does not restate: JavaScript/TypeScript (npm install apify-client) and Python (pip install apify-client).
README
Broken Link Checker & Technical SEO Audit
Find broken internal links and technical SEO issues across a public website. Start from a site URL or an XML sitemap and get page-level findings for links, titles, meta descriptions, canonicals, headings and more, plus a site summary with suggested fixes. It is an HTTP-based audit and does not render JavaScript.
Real output from a 10-page run on crawlee.dev (2026-10-01): the blog page record and its three findings; all 10 records total 1 error, 11 warnings and 9 notices.
What it checks
- Titles, meta descriptions, headings, canonicals, robots directives, hreflang, images, and structured data
- Duplicate titles and descriptions across the audited site
- Broken internal links and every redirecting internal link (status, final URL, hops), with cached HEAD checks and a GET fallback; rate-limited checks are reported as not verified, never as broken
- Ordinary, gzip-compressed, and nested sitemap indexes
- $0.004 per successfully fetched and saved page
This Actor is HTTP-only and does not render JavaScript.
Quick start
{"startUrls":[{"url":"https://example.com"}],"discoverSitemap":true,"maxPages":100,"checkLinks":true}
Try a ready-made example: Broken link checker. Open it, click Start, then copy it and swap in your own site.
Use with AI agents / MCP
Call lintlab/seo-site-qa through the Apify API or Apify MCP server. Use the per-page findings for precise fixes and SUMMARY or SUMMARY.md for a site-level plan.
Overview
SEO Site QA is an HTTP-only technical SEO audit for sites you own or are authorized to audit. It accepts pages, an XML sitemap, or a sitemap index; checks each allowed page; and writes compact, deterministic JSON plus a site summary. It is intended for developers, agencies, CI workflows, and AI agents that need findings they can act on without running a browser.
What it does
- Reads ordinary and gzip-compressed sitemaps, including nested sitemap indexes.
- Optionally discovers a sitemap from
robots.txtand then/sitemap.xmlwhen given only a homepage. - Uses Crawlee's
CheerioCrawlerwith guarded HTTP requests. It does not launch a browser. - Audits up to 5,000 pages and can optionally discover more pages from internal links.
- Checks internal links with cached HEAD requests and a GET fallback, within a configurable cap.
- Produces one dataset item per attempted page,
SUMMARYJSON, andSUMMARY.md. - Charges only after an allowed, successfully fetched page has been saved to the dataset.
Input
{
"startUrls": [{ "url": "https://crawlee.dev" }],
"discoverSitemap": true,
"maxPages": 100,
"sameHostOnly": true,
"followLinks": false,
"checkLinks": true,
"maxLinkChecks": 500,
"timeoutSecs": 20,
"concurrency": 5
}
You may use sitemapUrl instead of, or together with, startUrls. startUrls also accepts plain URL strings through the programmatic input path. Global concurrency defaults to 5; requests to one origin are limited to 2 concurrently.
Page output
Each dataset item describes one attempted page. Blocked and failed pages include an error and are not charged.
{
"url": "https://example.com/about",
"finalUrl": "https://example.com/about",
"status": 200,
"redirectChain": [],
"title": "About Example Company",
"metaDescription": "How our team builds useful products.",
"h1": ["About us"],
"canonical": "https://example.com/about",
"indexable": true,
"wordCount": 438,
"jsonLdTypes": ["AboutPage"],
"findings": [
{
"checkId": "opengraph.image_missing",
"severity": "notice",
"message": "Open Graph image is missing.",
"fix": "Add a og:image meta tag for richer social previews."
}
],
"counts": { "error": 0, "warning": 0, "notice": 1 },
"checkedAt": "2026-01-01T00:00:00.000Z"
}
redirectChain contains {url, status, location} objects. A finding always contains checkId, severity, message, and fix; value is included when useful.
Checks
Check IDs are stable API values. New checks may be added in later versions.
| Check ID | Severity | Condition |
|---|---|---|
title.missing | error | No <title> element |
title.empty | error | Title element has no text |
title.too_long | warning | Title is over 60 characters |
title.too_short | warning | Title is under 10 characters |
title.duplicate | warning | Same non-empty title appears on multiple audited pages |
description.missing | warning | Meta description is absent or empty |
description.too_long | warning | Meta description is over 160 characters |
description.duplicate | warning | Same non-empty description appears on multiple audited pages |
h1.missing | error | No H1 heading |
h1.multiple | warning | More than one H1 |
headings.level_skip | warning | Heading order jumps by more than one level |
canonical.missing | warning | Canonical link is absent or empty |
canonical.relative | warning | Canonical is not written as an absolute URL |
canonical.elsewhere | warning | Canonical resolves to a different URL |
canonical.malformed | error | Canonical cannot be parsed as a URL |
canonical.non_200 | error | Canonical target does not return HTTP 200 or cannot be fetched |
robots.meta_noindex | warning | Meta robots contains noindex |
robots.meta_nofollow | warning | Meta robots contains nofollow |
robots.x_noindex | warning | X-Robots-Tag contains noindex |
robots.x_nofollow | warning | X-Robots-Tag contains nofollow |
robots.blocked_sitemap | error | URL is in a sitemap but disallowed by robots.txt |
robots.disallowed | error | Non-sitemap start/discovered URL is disallowed |
http.non_200 | error | Final page response is outside 2xx |
sitemap.redirected | warning | A URL taken from the sitemap redirects |
html.lang_missing | warning | <html> has no lang |
html.lang_malformed | warning | lang is not a valid BCP 47 tag |
viewport.missing | warning | Viewport meta tag is absent |
images.alt_missing | warning | One or more images lack an alt attribute; value has count and first five sources |
opengraph.title_missing | notice | og:title is absent or empty |
opengraph.description_missing | notice | og:description is absent or empty |
opengraph.image_missing | notice | og:image is absent or empty |
jsonld.invalid | error | A JSON-LD block does not parse as JSON |
jsonld.types | notice | Valid JSON-LD types were found; types are reported in value and jsonLdTypes |
resources.mixed_content | error | An HTTPS page references an http:// resource |
html.too_large | warning | Uncompressed HTML is over 1.5 MB |
hreflang.malformed | warning | Hreflang language or absolute HTTP(S) target is malformed |
links.broken_internal | error | Internal link returns 4xx other than 429, or 5xx other than 503 with Retry-After, or has a network error |
links.rate_limited | notice | HTTP 429 or HTTP 503 with Retry-After persisted after two retries, so the link was not verified; value contains target, status, and retryAfter |
links.redirected | notice for 301/308; warning for 302/303/307 | Internal link has one redirect hop; value contains url, first-hop status and location, finalUrl, and hops |
links.redirect_chain | warning | Internal link has more than one redirect hop |
fetch.failed | error | Page fetch fails before an HTTP response is available |
An empty JSON-LD set is not an error. If valid JSON-LD exists, its @type values are reported. Link findings are attached to each source page; a redirect chain produces links.redirect_chain instead of links.redirected. The summary lists broken links and redirecting links.
Summary and score
The default key-value store receives:
SUMMARY: JSON with pages audited/attempted, aggregate counts, top issues, duplicate title and description groups,brokenLinks,redirectedLinks,redirectedLinksTruncated, sitemap errors, and the score.redirectedLinkscontains{from, url, status, finalUrl, hops}for up to 500 unique link URLs;statusis the first redirect status andfromis one source page.rateLimitedLinks: number oflinks.rate_limitedfindings inSUMMARY.SUMMARY.md: the same core results in a readable Markdown report.
The score is:
100 - min(100, (errors × 5 + warnings × 2 + notices × 0.25) / successful pages)
It is clamped to 0–100 and rounded to one decimal. The formula is deliberately simple and comparative; a business-critical issue can matter more than its numeric weight.
Pricing
The page-audited event costs $0.004 per successfully audited page, or $4 per 1,000 pages. Internal link checks are free and bounded by maxLinkChecks. Disallowed pages, network failures, and non-2xx final page responses are written for diagnosis but are not charged. The Actor stops writing further page results when the platform reports that the event charge limit has been reached.
Limits and behavior
- Maximum 5,000 scheduled pages per run and 10,000 unique internal link checks.
- Maximum 10 redirects per HTTP operation.
- HTML responses are capped at 10 MB; sitemap responses at 20 MB.
- Sitemap indexes are capped at 100 fetched sitemap documents per run.
- Link checks use HEAD, falling back to GET for HTTP 405 or 501. Canonical targets are checked separately and cached.
- Link checks stop after response headers; HTTP 429 and 503 with Retry-After get at most two retries, after which they are reported as unverified.
sameHostOnlycompares hostnames. URL fragments are removed during normalization.- The crawler evaluates server-delivered HTML only. It does not execute JavaScript, measure rendered layout, or run Core Web Vitals.
- Hreflang validation checks language syntax and absolute HTTP(S) targets; it does not verify reciprocal annotations.
- Duplicate matching is case-insensitive after trimming, not semantic.
robots.txtrules support user-agent groups, Allow/Disallow,*, and terminal$; unusual nonstandard directives are ignored.
Safety and compliance
Use this Actor only for sites you own or have permission to audit.
- User agent starts with
lintlab-seo-qa/1.0; robots product token:lintlab-seo-qa. robots.txtis cached per origin. Fetch/network errors and 5xx responses fail closed (all URLs on that origin are disallowed); 4xx responses allow crawling under RFC 9309 behavior.- Every page, sitemap, canonical, link check, and redirect hop is restricted to HTTP(S), DNS-resolved before connecting, and blocked if any result is private, loopback, link-local, CGNAT, multicast, reserved, unspecified, or IPv6 ULA/IPv4-mapped private space.
- Redirects are followed manually, at most 10 hops, so every destination receives the same SSRF validation.
- No proxy, login/cookie input, CAPTCHA handling, browser, or stealth behavior is used.
- The output does not extract emails, phone numbers, or personal names.
Local development
Requires Node.js 20 or newer.
npm ci
npm test
APIFY_LOCAL_STORAGE_DIR=./storage CRAWLEE_STORAGE_DIR=./storage node src/main.js
Before the final command, write the desired Actor input to storage/key_value_stores/default/INPUT.json (or use apify run --purge with the Apify CLI).
More lintlab tools
- XML Sitemap Checker, Validator & URL Extractor: validate sitemaps and export every URL
- Website Screenshot & Visual Regression Diff: full-page screenshots with pixel diffs
- PDF to Markdown & Text Extractor: page-aware Markdown for RAG and AI agents
Built by lintlab — small, reliable data tools. Tested before release. Support: open an issue on this Actor's Issues tab here on Apify.
Actor input Schema
startUrls (type: array):
Pages to audit. With one homepage and sitemap discovery enabled, the Actor also checks robots.txt and /sitemap.xml.
sitemapUrl (type: string):
Optional XML sitemap or sitemap index URL. Gzip-compressed sitemaps are supported.
discoverSitemap (type: boolean):
When the only start URL is a homepage, try Sitemap directives in robots.txt and then /sitemap.xml.
maxPages (type: integer):
Maximum number of unique pages scheduled for audit.
sameHostOnly (type: boolean):
Ignore sitemap entries and followed links on other hostnames.
followLinks (type: boolean):
Add internal links found on audited pages to the audit queue, bounded by maximum pages.
checkLinks (type: boolean):
Check internal link targets with HEAD and a GET fallback. Link checks are not charged.
maxLinkChecks (type: integer):
Maximum unique internal link targets checked across the run.
timeoutSecs (type: integer):
Timeout applied to each HTTP request.
concurrency (type: integer):
Maximum concurrent page tasks. Requests to any one host are limited to two at a time.
Actor input object example
{
"startUrls": [
{
"url": "https://crawlee.dev"
}
],
"discoverSitemap": true,
"maxPages": 100,
"sameHostOnly": true,
"followLinks": false,
"checkLinks": true,
"maxLinkChecks": 500,
"timeoutSecs": 20,
"concurrency": 5
}
Actor output Schema
pages (type: string):
No description
summary (type: string):
Includes rateLimitedLinks, the number of links.rate_limited findings.
summaryMarkdown (type: string):
No description
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
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://crawlee.dev"
}
]
};
// Run the Actor and wait for it to finish
const run = await client.actor("lintlab/seo-site-qa").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
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://crawlee.dev" }] }
# Run the Actor and wait for it to finish
run = client.actor("lintlab/seo-site-qa").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
echo '{
"startUrls": [
{
"url": "https://crawlee.dev"
}
]
}' |
apify call lintlab/seo-site-qa --silent --output-dataset
MCP server setup
{
"mcpServers": {
"apify": {
"type": "http",
"url": "https://mcp.apify.com/?tools=fetch-actor-details,lintlab/seo-site-qa"
}
}
}
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/SwggFLZIVFbAwipoj/builds/UblUFT9X56veK3F5y/openapi.json