IF
IndexFlowing
/ Docs
🌐 切换中文文档 GitHub ↗

Crate: indexflow-sitemap

indexflow-sitemap is a high-performance, fault-tolerant, Google-compliant XML and plaintext sitemap parser and recursive crawler written in Rust.


Installation

Add indexflow-sitemap to your Cargo.toml:

[dependencies]
indexflow-sitemap = { version = "0.1.0", path = "crates/indexflow-sitemap" }

Feature Flags

Feature Default Description
gzip Enabled Enables transparent .xml.gz decompression with bomb protection via flate2.
fetch Enabled Enables async HTTP downloading and recursive index expansion via reqwest.

To use in standalone minimal mode without HTTP capabilities:

[dependencies]
indexflow-sitemap = { version = "0.1.0", default-features = false, features = ["gzip"] }

Core Data Models

ParsedSitemap Enum

The primary output when parsing any raw sitemap document:

pub enum ParsedSitemap {
    /// A sitemap index pointing to child sitemaps (<sitemapindex>)
    Index { child_urls: Vec<String> },
    /// A standard URL set containing page records (<urlset>)
    UrlSet { entries: Vec<SitemapUrlEntry> },
    /// A line-oriented plaintext URL list (.txt)
    PlainText { urls: Vec<String> },
}

SitemapUrlEntry Struct

Represents a single <url> entry with Google extensions:

pub struct SitemapUrlEntry {
    pub loc: String,
    pub lastmod: Option<DateTime<Utc>>,
    pub changefreq: Option<ChangeFreq>,
    pub priority: Option<f64>,
    pub hreflangs: Vec<HreflangEntry>,
    pub images: Vec<SitemapImage>,
    pub videos: Vec<SitemapVideo>,
    pub news: Option<SitemapNews>,
}

Public API Reference

1. In-Memory Parsing (parse_sitemap)

Automatically detects whether the input string is XML or plaintext and extracts all entities:

use indexflow_sitemap::{parse_sitemap, ParsedSitemap, ChangeFreq};

fn main() {
    let xml_content = r#"<?xml version="1.0" encoding="UTF-8"?>
    <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
            xmlns:xhtml="http://www.w3.org/1999/xhtml"
            xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
      <url>
        <loc>https://example.com/blog/rust-guide</loc>
        <lastmod>2026-03-30T10:00:00Z</lastmod>
        <changefreq>weekly</changefreq>
        <priority>0.8</priority>
        <xhtml:link rel="alternate" hreflang="zh-Hans" href="https://example.com/zh/blog/rust-guide" />
        <image:image>
          <image:loc>https://example.com/cover.jpg</image:loc>
          <image:title>Rust Guide Cover</image:title>
        </image:image>
      </url>
    </urlset>"#;

    match parse_sitemap(xml_content) {
        ParsedSitemap::UrlSet { entries } => {
            for entry in entries {
                println!("URL: {}", entry.loc);
                println!("Last Modified: {:?}", entry.lastmod);
                println!("Priority: {:?}", entry.priority);
                println!("Hreflang count: {}", entry.hreflangs.len());
                println!("Images count: {}", entry.images.len());
            }
        }
        ParsedSitemap::Index { child_urls } => {
            println!("Sitemap index with {} child sitemaps", child_urls.len());
        }
        ParsedSitemap::PlainText { urls } => {
            println!("Plaintext sitemap with {} URLs", urls.len());
        }
    }
}

2. Recursive HTTP Expansion (SitemapFetcher)

Asynchronously crawls a root sitemap URL, recursively expanding all nested <sitemapindex> files while isolating branch failures and preventing cyclic loops:

use indexflow_sitemap::SitemapFetcher;
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let http_client = reqwest::Client::builder()
        .timeout(Duration::from_secs(15))
        .user_agent("Mozilla/5.0 (compatible; IndexFlowBot/1.0)")
        .build()?;

    let fetcher = SitemapFetcher::new(http_client);

    // Recursively expand up to 3 levels deep
    let (is_index, entries) = fetcher
        .expand_all("https://example.com/sitemap_index.xml", 3)
        .await?;

    println!("Root is index: {}", is_index);
    println!("Total extracted URLs: {}", entries.len());

    for (idx, entry) in entries.iter().take(5).enumerate() {
        println!("[#{}] {} (lastmod: {:?})", idx + 1, entry.loc, entry.lastmod);
    }

    Ok(())
}

3. Transparent Gzip Decompression (decode_if_gzipped)

Sniffs magic bytes (0x1F, 0x8B) or Unicode BOMs and safely inflates gzipped sitemaps:

use indexflow_sitemap::compression::{decode_if_gzipped, decode_if_gzipped_with_limit};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let raw_bytes: Vec<u8> = load_sitemap_bytes();

    // Default safety cap: 50 MiB
    let xml_string = decode_if_gzipped(&raw_bytes)?;

    // Custom safety cap: 10 MiB
    let custom_capped_string = decode_if_gzipped_with_limit(&raw_bytes, 10 * 1024 * 1024)?;

    Ok(())
}

Error Handling (SitemapError)

pub enum SitemapError {
    Xml(quick_xml::Error),
    Decompression(String),
    DecompressionBomb { limit: usize },
    PayloadTooLarge { size: u64, limit: u64 },
    Encoding(String),
    InvalidUrl(url::ParseError),
    Io(std::io::Error),
    Network(reqwest::Error),
    HttpStatus(u16),
    MaxDepthExceeded(u8),
    CircularReference(String),
}
  • Decompression Bomb: Triggered if inflated size exceeds limit.
  • Payload Too Large: Triggered if streaming HTTP response exceeds MAX_DOWNLOAD_BYTES (52 MiB).
  • Circular Reference: Triggered when a child sitemap resolves to an already visited canonical ancestor.