Crate: indexflow-seo
indexflow-seo is a standalone, high-performance, zero-panic Rust library designed for technical SEO quality gate evaluations and Generative Engine Optimization (GEO / AI search bot) HTML inspections.
It powers the pre-flight quality gate inside IndexFlow-core and can also be used independently in custom web crawlers, static site generators, test suites, and CI/CD pipelines.
Key Features
- Zero-Panic HTML Parser: Safe string slicing with character boundary clamping (
safe_slice) preventing panics on multibyte Unicode (CJK, emojis). - Non-Following HTTP Prober (
SeoProbeClient): Probes URLs without following redirects (exposing 3xx status codes directly as gate issues) and applies a 5 MiB memory safety streaming cap. - RFC 3986 Canonical Normalization: Comprehensive canonical matching supporting relative, protocol-relative, and query-sorted URL equivalence.
- GEO & AI Search Bot Auditing: Detects robots directives specifically targeting
GPTBot,ClaudeBot,PerplexityBot, andGoogle-Extended. - Structured Data Extraction: Walks Schema.org JSON-LD blocks, resolving
@graphtrees and multi-typed entities. - Single-Pass Entity Decoding: Decodes XML predefined entities, HTML named entities, and numeric/hexadecimal character references.
Installation
Add indexflow-seo to your Cargo.toml:
[dependencies]
indexflow-seo = { version = "0.1.0", path = "crates/indexflow-seo" }
Feature Flags
| Feature | Default | Description |
|---|---|---|
probe |
Enabled | Enables SeoProbeClient for HTTP network probes using reqwest. |
To use in pure in-memory or WebAssembly (WASM) environments without network dependencies:
[dependencies]
indexflow-seo = { version = "0.1.0", default-features = false }
Data Models Reference
1. SeoAuditResult
The comprehensive output produced by both in-memory evaluation and live network probes:
pub struct SeoAuditResult {
// Network Layer
pub http_status: Option<i32>,
pub response_time_ms: Option<i32>,
pub payload_bytes: Option<i32>,
// Core Technical SEO Tags
pub page_title: Option<String>,
pub meta_description: Option<String>,
pub h1_content: Option<String>,
pub h1_count: usize,
// Indexing & Canonical Directives
pub canonical_url: Option<String>,
pub has_canonical: bool,
pub has_noindex: bool,
pub has_nofollow: bool,
pub robots_directive: Option<String>,
pub hreflang: Vec<HreflangItem>,
// Social & Mobile Metadata
pub opengraph: OpenGraphMeta,
pub twitter_card: TwitterCardMeta,
pub has_viewport: bool,
pub html_lang: Option<String>,
pub images_missing_alt: usize,
// GEO / AI Search & Structured Data
pub json_ld: Vec<JsonLdBlock>,
pub ai_directives: AiBotDirectives,
// Decision Verdict & Soft Warnings
pub passed: bool,
pub block_reason: Option<String>,
pub warnings: Vec<String>,
}
Helper Methods on SeoAuditResult
result.schema_types() -> Vec<String>: Returns all unique Schema.org@typestrings extracted across all JSON-LD blocks in document order.result.hreflang_json() -> Option<String>: Serializes the hreflang vector into a JSON string for database persistence.
2. AiBotDirectives
Sniffs indexing and training restrictions targeting major AI search crawlers:
pub struct AiBotDirectives {
pub gptbot_blocked: bool, // OpenAI GPTBot / ChatGPT Search
pub perplexity_blocked: bool, // Perplexity AI Crawler
pub claudebot_blocked: bool, // Anthropic ClaudeBot / Claude-Web
pub google_extended_blocked: bool, // Google-Extended (Gemini Training / AI Overviews)
}
impl AiBotDirectives {
pub fn any_blocked(&self) -> bool;
pub fn blocked_names(&self) -> Vec<&str>;
}
3. OpenGraphMeta & TwitterCardMeta
pub struct OpenGraphMeta {
pub title: Option<String>,
pub description: Option<String>,
pub image: Option<String>,
pub og_type: Option<String>,
pub url: Option<String>,
pub site_name: Option<String>,
}
pub struct TwitterCardMeta {
pub card: Option<String>,
pub title: Option<String>,
pub description: Option<String>,
pub image: Option<String>,
}
Public API & Code Examples
1. In-Memory Evaluation (evaluate_html)
Evaluates an HTML string and optional HTTP response headers without network calls:
use indexflow_seo::evaluate_html;
fn main() {
let page_url = "https://example.com/blog/rust-guide";
let status_code = 200;
let elapsed_ms = 45;
let x_robots_header = Some("noarchive");
let html_body = r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Rust SEO Best Practices & Guide</title>
<meta name="description" content="A complete guide to technical SEO and GEO in Rust." />
<link rel="canonical" href="https://example.com/blog/rust-guide" />
<link rel="alternate" hreflang="zh-Hans" href="https://example.com/zh/blog/rust-guide" />
<!-- OpenGraph & Twitter -->
<meta property="og:title" content="Rust SEO Best Practices" />
<meta property="og:image" content="https://example.com/cover.jpg" />
<meta property="og:type" content="article" />
<meta name="twitter:card" content="summary_large_image" />
<!-- AI Bot Directives -->
<meta name="gptbot" content="noindex" />
<meta name="perplexitybot" content="index, follow" />
<!-- Structured Data (Schema.org) -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Organization",
"name": "IndexFlow"
},
{
"@type": "TechArticle",
"headline": "Rust SEO Best Practices"
}
]
}
</script>
</head>
<body>
<h1>Complete Rust Guide</h1>
<img src="/arch.png" alt="Architecture" />
<img src="/chart.png" />
</body>
</html>"#;
let res = evaluate_html(page_url, status_code, elapsed_ms, x_robots_header, html_body);
// Hard Gate Decision
assert!(res.passed);
assert_eq!(res.block_reason, None);
// Core Tags
assert_eq!(res.page_title.as_deref(), Some("Rust SEO Best Practices & Guide"));
assert_eq!(res.h1_content.as_deref(), Some("Complete Rust Guide"));
assert_eq!(res.h1_count, 1);
assert!(res.has_canonical);
assert!(res.has_viewport);
assert_eq!(res.html_lang.as_deref(), Some("en"));
assert_eq!(res.images_missing_alt, 1);
// GEO / AI Search Directives
assert!(res.ai_directives.gptbot_blocked);
assert!(!res.ai_directives.perplexity_blocked);
// Schema.org Extraction
assert_eq!(res.schema_types(), vec!["Organization".to_string(), "TechArticle".to_string()]);
}
2. Live URL Network Probe (SeoProbeClient)
Probes live web endpoints. Redirects (301/302) are not followed and are surfaced as gate failures:
use indexflow_seo::SeoProbeClient;
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let prober = SeoProbeClient::new(
"Mozilla/5.0 (compatible; IndexFlowBot/1.0; +https://www.indexflowing.com)",
Duration::from_secs(15),
)?;
let result = prober.check_url("https://example.com/blog/rust-guide").await;
if result.passed {
println!("✅ Quality Gate Passed (Ready for engine submission)");
} else {
println!("❌ Quality Gate Blocked: {:?}", result.block_reason);
}
println!("HTTP Status : {:?}", result.http_status);
println!("Response Time : {:?} ms", result.response_time_ms);
println!("Payload Size : {:?} bytes", result.payload_bytes);
println!("Warnings ({}) :", result.warnings.len());
for warning in &result.warnings {
println!(" - {}", warning);
}
Ok(())
}
3. Canonical URL Equivalence Normalization (canonical_matches_page)
Verifies canonical URL equivalence against RFC 3986 normalization rules:
use indexflow_seo::canonical_matches_page;
fn main() {
let page = "https://example.com/blog/post-1/";
// Equivalent URLs (Returns true)
assert!(canonical_matches_page(page, "https://example.com/blog/post-1"));
assert!(canonical_matches_page(page, "https://example.com:443/blog/post-1"));
assert!(canonical_matches_page(page, "/blog/post-1"));
assert!(canonical_matches_page(page, "./post-1"));
assert!(canonical_matches_page(page, "//example.com/blog/post-1"));
assert!(canonical_matches_page(page, "https://example.com/blog/post-1?b=2&a=1")); // Query sorted match
// Non-equivalent URLs (Returns false)
assert!(!canonical_matches_page(page, "https://example.com/blog/other-post"));
assert!(!canonical_matches_page(page, "https://example.com/BLOG/post-1")); // Path is case-sensitive
}
4. Zero-Panic Entity Decoding (decode_basic_entities)
Decodes XML predefined entities, HTML named entities, and numeric references in a single pass:
use indexflow_seo::decode_basic_entities;
fn main() {
let raw = "Tom & Jerry 'Special' — 😀 © 2026";
let decoded = decode_basic_entities(raw);
assert_eq!(decoded, "Tom & Jerry 'Special' — 😀 © 2026");
}