How to Audit Every URL in Your Sitemap and Find Orphan Pages

Uses SEO Audit Crawler — On-Page SEO Checker for Whole Sites

Short answer: Run the SEO Audit Crawler with useSitemap on and maxPages above your site's size; it audits every page it can reach plus every sitemap URL, and flags sitemap URLs no page links to as orphan-page. Each audited page costs $0.006, so this is a paid audit, not a free sitemap URL lister.

Your sitemap tells search engines which URLs you care about, but it does not guarantee those pages are healthy or linked from anywhere. Pages that sit only in the sitemap, with no internal links pointing at them, are orphans: they get little internal link equity and are often forgotten, outdated or duplicated.

Finding them by hand means downloading the sitemap, crawling the site, and diffing the two lists, then checking each page for titles, canonicals and status codes. The SEO Audit Crawler does both halves at once: it crawls links like a search engine, reads sitemap.xml and the sitemaps listed in robots.txt, audits the sitemap URLs nothing links to, and flags them.

To be clear about cost: every HTML page audited is charged at $0.006 ($6 per 1,000). If all you need is the list of URLs in a sitemap, parse the XML yourself for free, as shown in the last step.

Step by step

  1. Estimate your site size and set maxPages

    Orphan detection is only conclusive when the crawl covers the whole site, because an orphan is a sitemap URL that no audited page links to. Count the URLs in your sitemap and set maxPages comfortably above that number; the default is 200. The run summary includes orphanCheckComplete, which tells you whether the result is conclusive.

    {
      "startUrls": ["https://example.com/"],
      "maxPages": 3000,
      "maxDepth": 20,
      "useSitemap": true,
      "respectRobotsTxt": true,
      "excludeGlobs": ["**/tag/**", "**/page/*"]
    }
  2. Keep the sitemap on and trim noise

    Leave useSitemap on (the default). The actor reads sitemap.xml and any sitemaps listed in robots.txt, and after the pages reachable by links it audits sitemap URLs that no page links to. Use excludeGlobs to skip tag archives and pagination you do not want to pay for, but do not exclude sections that link to sitemap pages, or those pages may be wrongly reported as orphans.

  3. List the orphan pages

    Each page row has an issues list; orphan sitemap URLs carry the orphan-page issue code. Pull the dataset and filter. The snippet accepts issues as plain codes or as objects with a code field, since you will see the exact structure in your first run.

    import os
    import pandas as pd
    from apify_client import ApifyClient
    
    client = ApifyClient(os.environ["APIFY_TOKEN"])
    run = client.actor("actor-forge/seo-audit").call(run_input={
        "startUrls": ["https://example.com/"],
        "maxPages": 3000,
        "useSitemap": True,
    })
    rows = list(client.dataset(run["defaultDatasetId"]).iterate_items())
    
    def codes(row):
        return {i if isinstance(i, str) else i.get("code") for i in row.get("issues") or []}
    
    orphans = [r for r in rows if "orphan-page" in codes(r)]
    pd.DataFrame(orphans)[["url", "statusCode", "indexable", "score", "wordCount"]] \
      .to_csv("orphan_pages.csv", index=False)
    print(len(orphans), "orphan pages of", len(rows), "audited")
    
    summary = client.key_value_store(run["defaultKeyValueStoreId"]).get_record("OUTPUT")
    print("Orphan check complete:", (summary or {}).get("value", {}).get("orphanCheckComplete"))
  4. Triage the rest of the sitemap URLs

    Orphans are just one signal. For every audited URL you also get status code, redirect chain, indexability (noindex and canonical), title and meta description problems, H1 count, thin content and broken internal links. A sitemap should list only indexable, 200-status, self-canonical pages, so filter for rows where indexable is false or statusCode is not 200 and remove or fix them in the sitemap.

    df = pd.DataFrame(rows)
    bad_in_sitemap = df[(df["indexable"] == False) | (df["statusCode"] != 200)]
    print(bad_in_sitemap[["url", "statusCode", "indexable"]].head(50))
    
    top = df.explode("issues")["issues"].map(lambda i: i if isinstance(i, str) else (i or {}).get("code"))
    print(top.value_counts().head(15))  # most common issues across the site
  5. Decide what to do with each orphan

    For each orphan, choose: link to it from a relevant hub or category page if it still matters, merge it into a stronger page and redirect, or remove it from the sitemap if it should not be indexed. Re-run the audit afterwards; pages you linked should lose the orphan-page flag.

  6. Audit only a fixed list of sitemap URLs (optional)

    If you only want the pages in one sitemap audited, without crawling the rest of the site, fetch and parse the sitemap yourself (free), then pass the URLs as startUrls with maxDepth 0, which audits only the start pages. Turn useSitemap off in this mode. Inlinks and orphan detection are not meaningful here, because the crawl does not follow links.

    import requests
    import xml.etree.ElementTree as ET
    
    NS = {"sm": "http://www.sitemaps.org/schemas/sitemap/0.9"}
    xml = requests.get("https://example.com/sitemap.xml", timeout=30).content
    urls = [loc.text.strip() for loc in ET.fromstring(xml).findall(".//sm:loc", NS)]
    print(len(urls), "URLs in sitemap")  # a sitemap index lists child sitemaps instead
    
    run_input = {
        "startUrls": urls,
        "maxDepth": 0,
        "maxPages": len(urls),
        "useSitemap": False,
    }

Sample input

{
  "startUrls": [
    "https://crawlee.dev/js/docs/guides"
  ],
  "maxPages": 500,
  "maxDepth": 20,
  "useSitemap": true,
  "respectRobotsTxt": true
}

Sample output

The sample includes a page flagged orphan-page with zero inlinks next to well-linked pages from the same section.

urlscorestatusCodeindexabletitleLengthmetaDescriptionLengthh1CountwordCountinlinksissues
https://crawlee.dev/js/docs/guides/motivation83200true68146180meta-description-duplicate, thin-content, title-too-long, orphan-page, structured-data-missing
https://crawlee.dev/js/docs/guides/cheerio-crawler-guide89200true7856162219meta-description-duplicate, title-too-long, meta-description-too-short
https://crawlee.dev/js/docs/guides/custom-http-client94200true9966147419title-too-long, meta-description-too-short
https://crawlee.dev/js/docs/quick-start95200true691181120721title-too-long

Run it from code

cURL

curl -X POST "https://api.apify.com/v2/acts/actor-forge~seo-audit/run-sync-get-dataset-items" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"startUrls": ["https://crawlee.dev/js/docs/guides"], "maxPages": 500, "maxDepth": 20, "useSitemap": true, "respectRobotsTxt": true}'

JavaScript (Node.js)

// Node.js 18+ (built-in fetch). Set APIFY_TOKEN in your environment.
const input = {
  "startUrls": [
    "https://crawlee.dev/js/docs/guides"
  ],
  "maxPages": 500,
  "maxDepth": 20,
  "useSitemap": true,
  "respectRobotsTxt": true
};

const res = await fetch(
  "https://api.apify.com/v2/acts/actor-forge~seo-audit/run-sync-get-dataset-items",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.APIFY_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(input),
  }
);
const items = await res.json();
console.log(items.length, items[0]);

Python

# pip install apify-client   (set APIFY_TOKEN in your environment)
import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run_input = {
    "startUrls": [
        "https://crawlee.dev/js/docs/guides"
    ],
    "maxPages": 500,
    "maxDepth": 20,
    "useSitemap": True,
    "respectRobotsTxt": True
}

run = client.actor("actor-forge/seo-audit").call(run_input=run_input)
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

The run-sync-get-dataset-items endpoint waits for the run (up to 5 minutes) and returns the dataset rows directly. For longer runs, start the run asynchronously and read the dataset when it finishes, as the Python example does. Replace APIFY_TOKEN with your own token from the Apify console.

What it costs

Auditing a site with 2,500 HTML pages costs 2,500 x $0.006 = $15.00; a 100-page site costs $0.60. Pages that answer 4xx/5xx, time out or are blocked are reported free and do not count.

Charged forWhenPricePer 1,000Worked example
Page auditedCharged per HTML page of your site fetched and audited (one dataset row)$0.006$6100 pages = $0.60; a 1,000-page site = $6.00. Pages that answer 4xx/5xx or time out are reported free
Actor Start (platform start fee)Charged when the Actor starts running. Number of events charged depends on Actor memory (one event per GB, minimum one event).$0.00005—One run at 1 GB = $0.00005

Ready to try it? SEO Audit Crawler — On-Page SEO Checker for Whole Sites runs on Apify, pay per result.

Run on Apify → Read the full actor page

Tips and pitfalls

FAQ

What is an orphan page in SEO?

A page with no internal links pointing to it. Here it specifically means a URL listed in your sitemap that none of the audited pages link to, which the actor flags with the orphan-page issue.

Can I just get a list of all URLs in my sitemap?

This actor audits pages and charges per audited page, so it is not a free URL lister. To list sitemap URLs only, parse the sitemap XML with a few lines of Python as in the last step, at no cost.

Does it follow sitemap index files and robots.txt sitemaps?

It reads sitemap.xml and the sitemaps listed in robots.txt when useSitemap is on. Sitemap URLs that the link crawl did not reach are audited afterwards and checked for inbound links.

Why does a page I link to show up as an orphan?

Usually because the linking page was not audited: it was beyond maxPages, excluded by a glob, outside the start path, or the link is only added by JavaScript. Check orphanCheckComplete and your crawl settings.

Actors used in this guide

SEO Audit Crawler — On-Page SEO Checker for Whole Sites

Crawl a whole website and audit every page for technical and on-page SEO: titles, meta descriptions, H1s, canonicals, noindex, duplicates, broken…

$6 per 1,000 · page audited

Related guides

How to Use a Wayback Machine Snapshots API and See When a Page Changed

Query archive.org's captures through the Wayback Snapshots actor with collapse set to digest to keep only versions where the content changed, then…

All guides