New good news for everyone who runs Crawl4AI

Crawl4AI Cloud is live.
The same crawler, hosted.

No browsers to run, no proxies to buy, no cookies to babysit. One key with free credits every month, and your AI agent can scrape, extract, search, read Google's results page (SERP) and answer questions through the same engine you already use. Free: 10 a minute, 2 at a time, 1,000 credits every month. Plans: 120 a minute, 10 at a time, from $10 a month.

Always open source, forever. The library stays here and stays free. The cloud is for the days you want to build fast and let us run the infrastructure.

no cardfree credit to start Β· soft launch: prices can change, what you buy stays yoursEU Β· Singapore Β· USMCP built in

        
Paste it once. Your agent gets the whole web.cloud docs β†’
β˜… Crawl4AI Cloud is live β€” the same crawler, hosted. Get a key with free credits every month and paste one line into your agent. Get a key β†’

Domain Mapping: Discover Every URL Under a Domain

What Is Domain Mapping?

Domain mapping goes beyond URL seeding. Instead of checking a single sitemap or index, DomainMapper combines 8 discovery sources to find every URL under a domain β€” including subdomains you didn't know existed.

DomainMapper vs AsyncUrlSeeder

Aspect AsyncUrlSeeder DomainMapper
Scope Single host, listed URLs only Entire domain + all subdomains
Sources Sitemap + Common Crawl 8 sources (sitemap, CC, Wayback, crt.sh, probe, robots.txt, feeds, homepage)
Subdomain discovery No Yes (Certificate Transparency, DNS, Wayback)
Soft-404 detection No Yes (fingerprints SPA sites)
Best for Known domains with good sitemaps Full domain reconnaissance

Real-world example: For superdesign.dev, AsyncUrlSeeder found 4 URLs. DomainMapper found 171 URLs across 11 hosts β€” including docs, API servers, staging environments, and analytics dashboards that no sitemap listed.

Quick Start

import asyncio
from crawl4ai import DomainMapper, DomainMapperConfig

async def main():
    async with DomainMapper() as mapper:
        results = await mapper.scan("example.com")

    print(f"Found {len(results)} URLs")
    for r in results[:10]:
        print(f"  [{r['source']}] {r['url']}")
        if r.get("head_data", {}).get("title"):
            print(f"    Title: {r['head_data']['title']}")

asyncio.run(main())

Or via AsyncWebCrawler:

from crawl4ai import AsyncWebCrawler, DomainMapperConfig

async with AsyncWebCrawler() as crawler:
    results = await crawler.amap_domain("example.com")

The 8 Discovery Sources

DomainMapper combines these sources, each catching URLs the others miss:

1. sitemap β€” Sitemap Discovery

Checks /sitemap.xml, /sitemap_index.xml, and robots.txt Sitemap: directives on every discovered host β€” not just the root domain.

config = DomainMapperConfig(source="sitemap")

2. cc β€” Common Crawl

Queries the Common Crawl CDX API for *.domain.tld/*, catching URLs and subdomains the web's largest public crawl has indexed.

config = DomainMapperConfig(source="cc")

3. wayback β€” Wayback Machine

Queries the Internet Archive's CDX API. Often has different coverage than Common Crawl β€” including historical pages that have since been removed.

config = DomainMapperConfig(source="wayback")

4. crt β€” Certificate Transparency

Queries crt.sh for SSL certificates issued to *.domain.tld. This is the single most effective subdomain discovery technique β€” it found 14 subdomains for superdesign.dev that no other source knew about.

config = DomainMapperConfig(source="crt")

5. probe β€” Common Path Probing

Tries ~25 well-known paths on each discovered host (/docs, /api, /login, /dashboard, /openapi.json, etc.). Combined with soft-404 detection to avoid false positives.

config = DomainMapperConfig(source="probe")

# Add custom paths to probe
config = DomainMapperConfig(
    source="probe",
    probe_paths=["/custom-api", "/internal/status"]
)

6. robots β€” robots.txt Path Mining

Parses Disallow: and Allow: lines from robots.txt. These are confirmed real paths the site acknowledges exist β€” often revealing admin panels, APIs, and internal tools that aren't linked anywhere.

config = DomainMapperConfig(source="robots")

7. feed β€” RSS/Atom Feed Parsing

Discovers and parses RSS/Atom feeds at common paths (/feed, /rss, /atom.xml, etc.). Feeds are curated lists of content URLs maintained by the site.

config = DomainMapperConfig(source="feed")

Fetches each host's homepage via HTTP and extracts all internal links using quick_extract_links(). Also mines <link rel="alternate|preload|prefetch"> tags from the <head> for additional URLs. No browser needed.

config = DomainMapperConfig(source="homepage")

Combining Sources

Sources are combined with +:

# Default: most useful combination
config = DomainMapperConfig(source="sitemap+cc+crt+probe")

# Maximum coverage: all 8 sources
config = DomainMapperConfig(
    source="sitemap+cc+wayback+crt+probe+robots+feed+homepage"
)

# Lightweight: just sitemap + probing
config = DomainMapperConfig(source="sitemap+probe")

How It Works: The Three Phases

Phase 1: Host Discovery

DomainMapper first discovers all subdomains under your domain:

superdesign.dev
β”œβ”€β”€ crt.sh           β†’ docs, app, cloud, insights, staging-api, ui2web, ...
β”œβ”€β”€ Wayback CDX      β†’ api, app, docs, www, ...
β”œβ”€β”€ Common Crawl     β†’ app, www, ...
└── DNS guessing     β†’ www, app, api, docs, blog, admin, cloud, ...

Result: 13 validated hosts

Each discovered host is validated with an HTTP HEAD request. Hosts that don't respond are dropped.

Phase 2: Per-Host Scanning

For each validated host, DomainMapper runs all enabled sources in parallel:

docs.superdesign.dev
β”œβ”€β”€ Soft-404 fingerprint  β†’ (404 returns proper error β€” no SPA issue)
β”œβ”€β”€ robots.txt            β†’ 1 sitemap URL, 1 disallow path
β”œβ”€β”€ Sitemap parsing       β†’ 19 URLs
β”œβ”€β”€ Path probing          β†’ 2 valid (/docs, /)
β”œβ”€β”€ Feed discovery        β†’ (no feeds found)
└── Homepage extraction   β†’ 26 internal links

Phase 3: Post-Processing

All discovered URLs go through:

  1. URL normalization β€” using normalize_url() to canonicalize
  2. Deduplication β€” by normalized URL, merging source attribution
  3. Nonsense filtering β€” removes static assets (JS, CSS, images, fonts), webpack chunks, Wayback garbage
  4. Head extraction β€” parallel <head> fetching for metadata (optional)
  5. BM25 scoring β€” relevance scoring against a query (optional)

Soft-404 Detection

Many modern SPAs return HTTP 200 for every URL β€” even pages that don't exist. DomainMapper detects this:

  1. Fingerprinting: Fetches a guaranteed-nonexistent URL (e.g., /c4ai-probe-a1b2c3d4) on each host
  2. Recording: Captures the response title and body hash
  3. Filtering: When probing real paths, compares against the fingerprint. If they match β†’ soft-404, filtered out

For superdesign.dev, this correctly: - Blocked all 25+ probe paths on app.superdesign.dev (SPA that returns 200 for everything) - Blocked 476 sitemap URLs from app.superdesign.dev (all rendering the same shell) - Kept all 19 legitimate URLs from docs.superdesign.dev

# Soft-404 detection is on by default
config = DomainMapperConfig(soft_404_detection=True)

# Disable if you want raw results
config = DomainMapperConfig(soft_404_detection=False)

Configuration Reference

DomainMapperConfig

Parameter Type Default Description
source str "sitemap+cc+crt+probe" Discovery sources joined by +
max_urls int -1 Maximum URLs to return (-1 = unlimited)
concurrency int 50 Max concurrent requests across all hosts
hits_per_sec int 10 Rate limit in requests/second
force bool False Bypass all caches
extract_head bool True Fetch and parse <head> metadata
filter_nonsense_urls bool True Filter static assets and utility URLs
soft_404_detection bool True Fingerprint and filter soft-404 pages
query str None BM25 relevance query (requires extract_head=True)
score_threshold float None Minimum relevance score (0.0-1.0)
scoring_method str "bm25" Scoring algorithm
probe_paths List[str] None Extra paths to probe on each host
common_subdomains List[str] None Extra subdomain prefixes to guess
use_browser_for_homepage bool False Use Playwright for JS-rendered homepages
verbose bool None Override logger verbose setting
cache_ttl_hours int 24 Hours before cached results expire
dns_timeout float 3.0 Timeout for DNS resolution (seconds)
http_timeout float 10.0 Timeout for HTTP requests (seconds)

Output Format

Each result is a dict:

{
    "url": "https://docs.superdesign.dev/quickstart",
    "host": "docs.superdesign.dev",
    "source": "homepage+sitemap",     # which source(s) found it
    "status": "valid",                # valid | not_valid | soft_404
    "head_data": {                    # if extract_head=True
        "title": "Quickstart",
        "meta": {"description": "..."},
        "link": {...},
        "jsonld": [...]
    },
    "relevance_score": 0.85,          # if query provided
}

Practical Examples

Discover and Crawl Documentation

import asyncio
from crawl4ai import AsyncWebCrawler, DomainMapperConfig, CrawlerRunConfig

async def crawl_all_docs():
    async with AsyncWebCrawler() as crawler:
        # Step 1: Discover all URLs
        pages = await crawler.amap_domain("example.com", DomainMapperConfig(
            source="sitemap+crt+probe+homepage",
            extract_head=True,
            query="documentation tutorial guide",
        ))

        # Step 2: Filter for docs
        doc_urls = [
            p["url"] for p in pages
            if p.get("relevance_score", 0) > 0.3
        ]
        print(f"Found {len(doc_urls)} documentation pages")

        # Step 3: Crawl them
        results = await crawler.arun_many(
            doc_urls[:50],
            config=CrawlerRunConfig(only_text=True)
        )
        for r in results:
            if r.success:
                print(f"  Crawled: {r.url}")

asyncio.run(crawl_all_docs())

Security Audit: Find Exposed Services

async def audit_domain():
    async with DomainMapper() as mapper:
        results = await mapper.scan("company.com", DomainMapperConfig(
            source="crt+probe+robots",
            extract_head=True,
            probe_paths=[
                "/openapi.json", "/swagger.json", "/api-docs",
                "/graphql", "/.env", "/debug", "/admin",
                "/phpinfo.php", "/server-status",
            ],
        ))

        # Flag exposed services
        for r in results:
            title = r.get("head_data", {}).get("title", "")
            if any(x in title.lower() for x in ["swagger", "api", "admin", "debug"]):
                print(f"  EXPOSED: {r['url']} β€” {title}")

Compare Subdomains Across a Domain

async def map_infrastructure():
    async with DomainMapper() as mapper:
        results = await mapper.scan("company.com", DomainMapperConfig(
            source="crt+probe",
            extract_head=False,
        ))

        # Group by host
        from collections import defaultdict
        by_host = defaultdict(list)
        for r in results:
            by_host[r["host"]].append(r)

        print(f"Discovered {len(by_host)} hosts:")
        for host, urls in sorted(by_host.items()):
            print(f"  {host}: {len(urls)} URLs")

Tips and Best Practices

  1. Start with the default sources (sitemap+cc+crt+probe). Add wayback, robots, feed, and homepage if you need maximum coverage.

  2. Use extract_head=False for speed when you just need URL lists. Head extraction makes ~1 HTTP request per URL.

  3. The query parameter is powerful for finding specific content across a large domain without crawling anything.

  4. probe_paths is your extensibility hook β€” add domain-specific paths you suspect exist.

  5. Rate limiting matters β€” hits_per_sec=10 is respectful. Lower it for smaller sites, raise it for your own infrastructure.

  6. Soft-404 detection is critical for SPAs β€” without it, single-page apps flood your results with hundreds of identical shell pages.

See Also


> Feedback