# SEO Audit Leads: Bulk On-Page, Technical & Local SEO Checker (`mortenm/seo-audit-leads`) Actor

Bulk SEO audit for lead generation: check business websites for noindex, robots.txt and sitemap problems, missing titles, meta descriptions and H1s, HTTPS, mobile viewport, LocalBusiness schema and city mentions. SEO score, grade, lead level and fixes per site. Works with Google Maps Scraper.

- **URL**: https://apify.com/mortenm/seo-audit-leads.md
- **Developed by:** [Lead Notebook](https://apify.com/mortenm) (community)
- **Categories:** SEO tools, Lead generation, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 website seo-auditeds

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

## SEO Audit Leads: Bulk On-Page, Technical & Local SEO Checker

**Audit hundreds of business websites for SEO problems in one run, and get the fixes to pitch.** Paste a list of websites, or pick the results of a Google Maps Scraper run, and this Actor checks each site's public HTML for the things that keep local businesses out of Google: `noindex` tags, robots.txt that blocks Googlebot, missing or broken XML sitemaps, canonical tags pointing elsewhere, soft 404s, no HTTPS, no mobile viewport, missing or generic titles, missing meta descriptions and H1s, thin content, images without alt text, no LocalBusiness schema, no address or phone, the city or category missing from the title, and your target keywords missing from the title, H1 or page. Each website gets an **SEO score from 0 to 100, a grade from A to F, a lead level**, the list of issues with evidence, and the **three fixes with the best effort-to-impact ratio**.

You pay only for websites that were actually audited. **Unreachable, parked, blocked and missing websites are reported for free.**

### What it checks

| Area | Checks (points subtracted from 100) |
|---|---|
| 🚫 **Indexing** | `noindex` on the homepage (meta robots or `X-Robots-Tag`, 40), robots.txt blocking Googlebot (40) or every crawler (40, free row), canonical tag pointing to another site (25) or page (10), missing canonical (5), missing robots.txt (2) |
| 🗺️ **Sitemap** | No XML sitemap found (10), sitemap URL returns HTML, an empty response or invalid XML, or robots.txt points to a broken sitemap (10), newest `lastmod` older than two years (5). Sitemap indexes are followed into their first child. |
| 🔒 **HTTPS** | Served over plain HTTP with no working HTTPS (25), HTTPS exists but http:// is not redirected (15), mixed content on an HTTPS page (10) |
| 🧭 **Technical** | Soft 404s (missing pages return 200, 10), a page linked from the homepage returns 404 (10), slow homepage HTML (10–15), HTML over 512 KiB (3), no gzip/brotli (3) |
| 📝 **On-page** | Missing title (15), generic title such as "Home" (10), title too long (3) or too short (5), duplicate titles across pages (8), missing meta description (10), wrong length (3), duplicate descriptions (5), missing H1 (10), several H1s (5), skipped heading levels (2), images without alt (5–8), thin content under 150/300 words (10/3), missing `lang` (2), fewer than three internal links (5) |
| 📱 **Mobile** | No responsive viewport tag (20) |
| 🏷️ **Structured data & social** | No JSON-LD at all (8), no LocalBusiness entity with address and phone (8), no Open Graph tags (3), no favicon (1) |
| 📍 **Local SEO** | No phone number (10), no street address (5), the city you supplied not mentioned anywhere (10) or not in the title (5), the business category (from Google Maps or your input) not reflected in the title (8), no link to the Google Business Profile / Maps listing (3) |
| 🎯 **Keywords** | Your target keywords (per run or per business) missing from the title (6), from the H1 (3) or from every fetched page (8), with a per-keyword check in `keywordChecks` |
| 📊 **Tracking** | No analytics or tag manager observed (5; skipped for Wix/Squarespace-style builders with built-in analytics) |

Every issue comes with its **category, severity, points, effort (low/medium/high), the observed evidence** (the tag, the header, the URL, the measured value), the page it was found on, and a **recommendation** you can paste into a proposal.

### How to get SEO leads from Google Maps

1. Run [Google Maps Scraper](https://apify.com/compass/crawler-google-places) for a search such as "dentists in Denver" or "roofers in Dallas".
2. Open **SEO Audit Leads** and, under **Or: Google Maps Scraper results**, pick that run's dataset.
3. Run it. Sort the **SEO leads** table by **SEO score** ascending: the lowest scores are the businesses with the most SEO work to sell, with their emails and phone numbers next to them.

The business name, category, city, country and Google place ID are copied from Maps to every row; the category and city feed the local checks ("Dentist" not in the title, "Denver" not on the page). Businesses that have **no website** on Maps come back as `NO_WEBSITE` leads, free of charge.

You can also paste websites directly (one per line), or send rows with your own CRM IDs, category and keywords in **Businesses with your own IDs**.

### SEO score, grade and lead level

The score starts at 100 and loses the points of every issue (floor 0).

| Score | Grade | Lead level | Meaning |
|---|---|---|---|
| 90–100 | A | low | Well optimised; only details left |
| 80–89 | B | low | Minor on-page gaps |
| 65–79 | C | medium | Clear problems worth a proposal |
| 50–64 | D | medium | Several serious problems |
| 0–49 | F | high | Fundamental SEO problems, often not indexable |

The score measures **how much visible SEO work the website needs**. It is not a ranking prediction and says nothing about the business's budget or intent. Unreachable, parked and missing websites also get a score and a high lead level (they are reported for free).

### Output example

```json
{
  "seoScore": 0,
  "seoGrade": "F",
  "leadLevel": "high",
  "businessName": "Riverbend Dental",
  "website": "https://mortenmyrmel03.github.io/lbpq-demo-sites/riverbend-dental/",
  "seoStatus": "AUDITED",
  "issueCount": 19,
  "issues": ["noindex_homepage", "canonical_points_elsewhere", "thin_content", "generic_title", "missing_meta_description", "sitemap_missing", "..."],
  "issueSummary": "The homepage tells search engines not to index it (meta robots contains \"noindex\"). | The homepage canonical tag points to another website ...",
  "topFixes": [
    "Remove \"noindex\" from the homepage (meta robots tag or X-Robots-Tag header) so Google can list the site.",
    "Set the canonical tag to the page's own URL (same domain and protocol as the live site).",
    "Replace the generic title with \"<Service> in <City> | <Business name>\" (30-60 characters)."
  ],
  "title": "Home",
  "metaDescription": null,
  "h1Count": 2,
  "wordCount": 64,
  "indexable": false,
  "canonicalUrl": "https://www.riverbend-dental-old.example/",
  "sitemapStatus": "missing",
  "mobileViewport": true,
  "hasLocalBusinessSchema": false,
  "phones": ["+15415550138"],
  "issueDetails": [
    {
      "id": "noindex_homepage",
      "category": "indexing",
      "severity": "critical",
      "points": 40,
      "effort": "low",
      "message": "The homepage tells search engines not to index it (meta robots contains \"noindex\").",
      "recommendation": "Remove \"noindex\" from the homepage (meta robots tag or X-Robots-Tag header) so Google can list the site.",
      "evidence": "<meta name=\"robots\" content=\"noindex\">",
      "sourceUrl": "https://mortenmyrmel03.github.io/lbpq-demo-sites/riverbend-dental/"
    }
  ]
}
```

(Fictional demo business. Click **Try for free** to run the prefilled demo: a well-optimised site, a poor one, two `noindex` sites, a parked domain and a dead domain.)

Export the results to **CSV, Excel, JSON or Google Sheets**, or open the **Issue details** view (one row per finding, with the fix) or the **Technical facts** view (indexing, sitemap, HTTPS, speed per site).

### Pricing

**$10 per 1,000 audited websites** ($0.01 each), plus Apify's tiny run-start fee.

- Charged only when a homepage was actually fetched and analysed.
- **Free:** unreachable websites, parked domains, sites that block crawlers (including a robots.txt that blocks everyone, which is reported as the finding), businesses with no website, invalid URLs and rows over your limits.
- The same website appearing twice is audited and charged **once**.
- Set a maximum charge per run and the Actor stops starting new websites before it's reached. The rest come back as `NOT_PROCESSED` so you can resubmit them.

### Input options

| Field | Default | Meaning |
|---|---|---|
| `websites` | demo list | Websites or domains, one per line |
| `datasetId` | none | A Google Maps Scraper dataset (or any dataset with a `website` column; `categoryName` is used as the category) |
| `targetKeywords` | none | Up to 10 keywords checked on every site (title, H1, page text) |
| `maxWebsites` | 200 | Unique websites to audit (up to 5,000) |
| `maxPagesPerSite` | 3 | 1 = homepage only; 3 also reads the contact and services pages (duplicate-title checks, more contact details) |
| `technicalChecks` | true | Sitemap discovery (≤3 requests), one soft-404 probe and the HTTP→HTTPS check |
| `businesses` | none | `[{"website", "clientRecordId", "businessName", "category", "city", "country", "keywords": []}]` for CRM lists |
| `defaultCountry` | `US` | Country used to read phone numbers written without +country code |
| `excludedDomains` | none | Domains to skip, for example your current clients |

### Statuses

| `seoStatus` | Meaning | Charged |
|---|---|---|
| `AUDITED` | Homepage fetched and analysed | Yes |
| `UNREACHABLE` | Domain doesn't resolve, certificate or server error, timeout | No |
| `PARKED_DOMAIN` | Parked, for-sale or under-construction page | No |
| `NO_WEBSITE` | No website in your data | No |
| `BLOCKED` | The site refuses crawlers (robots.txt for everyone, 403, bot challenge) | No |
| `INVALID` | Not a usable URL, or a private/internal address | No |
| `NOT_PROCESSED` | Over your limits; resubmit later | No |

### How it works and its limits

- Reads the public homepage and by default two more pages (contact, services) with plain HTTP requests, plus the sitemap, one deliberately non-existent URL (to detect soft 404s) and the http:// or https:// variant of the homepage. No browser, no logins, no proxies, no third-party SEO APIs; robots.txt is respected (a sitemap or probe path that robots.txt disallows is skipped).
- Findings describe the **HTML the site serves**. Pages drawn entirely by JavaScript get the indexing, HTTPS, sitemap, speed, title and viewport checks; content checks are skipped and noted in `warnings`.
- "Not observed" means not found in the fetched pages, not proof that something doesn't exist anywhere on the site.
- No rankings, backlinks, Core Web Vitals or search volumes: those need Google's or a paid SEO vendor's data. This Actor finds the on-page, technical and local problems you can see from the site itself.
- Load time is one measurement from a US data center for the HTML only, so treat it as a hint.
- Dead domains can be temporary. Re-check before you mention it to the business.
- Emails and phone numbers are the ones the business publishes on its own website. Government addresses shown on a business's site, such as the licensing-board complaint notice Texas plumbers must display (`@tsbpe.texas.gov`, `@hpc.state.tx.us`), are never listed as the business's email, and the board's phone number in that notice (for example (512) 936-5200) is never listed as the business's phone; the row's `warnings` names them. Follow your local rules on cold outreach (CAN-SPAM, GDPR and similar).

### FAQ

**How is this different from a single-site SEO checker?** It is built for lists: hundreds of businesses per run, one row per business with your IDs, category and city preserved, a score to sort by and fixes to quote. Use it to find and qualify SEO leads, not to replace a full crawl of one site.

**Does it check keyword rankings?** No. It checks whether your target keywords appear in the title, H1, meta description and page text, which is what you can fix on the page.

**Does it work outside the US?** Yes, for any public website. Set `defaultCountry` (for example `GB` or `AU`) so local phone numbers are read correctly. The street-address check is English-language only; other findings are language-independent.

**Can I call it from the API, Make, Zapier or n8n?** Yes, like any Apify Actor. Send `{"websites": ["example.com"], "targetKeywords": ["emergency plumber"]}` and read the dataset.

Questions or a feature request? Open an issue on the **Issues** tab.

### More tools from the same developer

- [Web Design Leads](https://apify.com/mortenm/web-design-leads): audit websites in bulk and find businesses with outdated, broken or non-mobile sites, with a lead score.
- [Local Business Leads Enricher](https://apify.com/mortenm/local-business-prospect-qualifier): paste business websites, get emails, phones, contact forms, services and tech stack with the source page for each.
- [Local Business Leads by City & Category](https://apify.com/mortenm/business-leads-overture): find businesses by type and city (a Google Maps alternative from open data), with phones, websites and emails.
- [Google Trends Trending Now + Bluesky, Wikipedia & Songs](https://apify.com/mortenm/social-trends-radar): what is trending right now across Google Trends, Bluesky, Mastodon, Wikipedia, Hacker News and Apple Music.

# Actor input Schema

## `websites` (type: `array`):

One website or domain per line, e.g. bobsplumbing.com or https://www.example-roofing.com/. Up to 5,000 per run (together with the other inputs).

## `datasetId` (type: `string`):

Pick the dataset of a Google Maps Scraper run (or any dataset with a "website" column). Business name, category, city, country and place ID are kept in the output; the category is checked against the page title. Businesses without a website are listed as NO_WEBSITE leads for free. The Actor can read only the dataset you pick here.

## `targetKeywords` (type: `array`):

Keywords every website is checked for in the title, H1 and page text, e.g. "emergency plumber". Up to 10. Per-business keywords can be given in the Businesses field.

## `maxWebsites` (type: `integer`):

Stop after this many unique websites (duplicates are audited and billed once). Rows over the limit are returned as NOT_PROCESSED and can be resubmitted.

## `maxPagesPerSite` (type: `integer`):

1 = homepage only (fastest). 3 (default) also reads the contact and services pages, which allows duplicate-title checks and finds more contact details. Up to 4.

## `technicalChecks` (type: `boolean`):

Also look for the XML sitemap (robots.txt, <link rel="sitemap">, /sitemap.xml; at most 3 requests), request one non-existent page to detect soft 404s, and check whether http:// redirects to https://. Adds a few requests per website.

## `businesses` (type: `array`):

For CRM lists: \[{"website": "acme-roofing.com", "clientRecordId": "crm-123", "businessName": "Acme Roofing", "category": "Roofing contractor", "city": "Austin", "country": "US", "keywords": \["roof repair austin"]}]. Every field except website is optional and is copied to the output so results join back to your records. city, category and keywords drive the local and keyword checks.

## `defaultCountry` (type: `string`):

ISO country code used to read phone numbers written without a +country prefix (e.g. (512) 555-0147). Leave empty to keep only numbers with a + prefix. A country given per business overrides it.

## `excludedDomains` (type: `array`):

Websites on these domains are not fetched or billed (e.g. your existing clients).

## `runTimeBudgetSecs` (type: `integer`):

No new website is started after this time; the rest is returned as NOT_PROCESSED. About 300 websites take 3-5 minutes with the technical checks on.

## `perSiteTimeBudgetSecs` (type: `integer`):

Total time for one website's pages and probes.

## `requestTimeoutSecs` (type: `integer`):

Timeout for a single page request.

## Actor input object example

```json
{
  "websites": [
    "https://mortenmyrmel03.github.io/lbpq-demo-sites/lakeside-electric/",
    "https://mortenmyrmel03.github.io/lbpq-demo-sites/riverbend-dental/",
    "https://mortenmyrmel03.github.io/lbpq-demo-sites/oldtown-plumbing/",
    "https://mortenmyrmel03.github.io/lbpq-demo-sites/clearwater-plumbing/",
    "https://mortenmyrmel03.github.io/lbpq-demo-sites/parked-domain-demo/",
    "expired-demo-domain.invalid"
  ],
  "maxWebsites": 200,
  "maxPagesPerSite": 3,
  "technicalChecks": true,
  "defaultCountry": "US",
  "runTimeBudgetSecs": 1200,
  "perSiteTimeBudgetSecs": 45,
  "requestTimeoutSecs": 15
}
```

# Actor output Schema

## `leads` (type: `string`):

No description

## `issues` (type: `string`):

No description

## `technical` (type: `string`):

No description

## `allFields` (type: `string`):

No description

## `runSummary` (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

```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 = {
    "websites": [
        "https://mortenmyrmel03.github.io/lbpq-demo-sites/lakeside-electric/",
        "https://mortenmyrmel03.github.io/lbpq-demo-sites/riverbend-dental/",
        "https://mortenmyrmel03.github.io/lbpq-demo-sites/oldtown-plumbing/",
        "https://mortenmyrmel03.github.io/lbpq-demo-sites/clearwater-plumbing/",
        "https://mortenmyrmel03.github.io/lbpq-demo-sites/parked-domain-demo/",
        "expired-demo-domain.invalid"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("mortenm/seo-audit-leads").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 = { "websites": [
        "https://mortenmyrmel03.github.io/lbpq-demo-sites/lakeside-electric/",
        "https://mortenmyrmel03.github.io/lbpq-demo-sites/riverbend-dental/",
        "https://mortenmyrmel03.github.io/lbpq-demo-sites/oldtown-plumbing/",
        "https://mortenmyrmel03.github.io/lbpq-demo-sites/clearwater-plumbing/",
        "https://mortenmyrmel03.github.io/lbpq-demo-sites/parked-domain-demo/",
        "expired-demo-domain.invalid",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("mortenm/seo-audit-leads").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 '{
  "websites": [
    "https://mortenmyrmel03.github.io/lbpq-demo-sites/lakeside-electric/",
    "https://mortenmyrmel03.github.io/lbpq-demo-sites/riverbend-dental/",
    "https://mortenmyrmel03.github.io/lbpq-demo-sites/oldtown-plumbing/",
    "https://mortenmyrmel03.github.io/lbpq-demo-sites/clearwater-plumbing/",
    "https://mortenmyrmel03.github.io/lbpq-demo-sites/parked-domain-demo/",
    "expired-demo-domain.invalid"
  ]
}' |
apify call mortenm/seo-audit-leads --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,mortenm/seo-audit-leads"
        }
    }
}
```

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/RElWAssrflhVeRuLf/builds/llgcFxFOemnf2twfq/openapi.json
