diff --git a/src/handlers.rs b/src/handlers.rs index 7d2638a..c33d776 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use askama::Template; use axum::extract::{Query, State}; -use axum::http::{HeaderMap, StatusCode}; +use axum::http::{HeaderMap, StatusCode, header}; use axum::response::{Html, IntoResponse, Response}; use crate::cli_detect::is_cli_user_agent; @@ -249,3 +249,42 @@ pub async fn tor_handler( let proxy = db::lookup_proxy(&reader, ip).ok_or(AppError::DbLookupFailed)?; Ok(format!("{}\n", proxy.is_tor)) } + +pub async fn robots_txt(State(state): State) -> Response { + let body = format!( + "User-agent: *\n\ + Allow: /\n\ + Disallow: /json\n\ + Disallow: /ip\n\ + Disallow: /asn\n\ + Disallow: /org\n\ + Disallow: /country\n\ + Disallow: /city\n\ + Disallow: /proxy\n\ + Disallow: /vpn\n\ + Disallow: /hosting\n\ + Disallow: /tor\n\ + Disallow: /health\n\ + \n\ + Sitemap: https://{}/sitemap.xml\n", + state.site_domain + ); + ([(header::CONTENT_TYPE, "text/plain; charset=utf-8")], body).into_response() +} + +pub async fn sitemap_xml(State(state): State) -> Response { + let body = format!( + "\n\ + \n\ + \x20 \n\ + \x20 https://{}/\n\ + \x20 \n\ + \n", + state.site_domain + ); + ( + [(header::CONTENT_TYPE, "application/xml; charset=utf-8")], + body, + ) + .into_response() +} diff --git a/src/routes.rs b/src/routes.rs index 26bcd98..8ea8172 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -8,6 +8,8 @@ pub fn build_router(state: AppState) -> Router { Router::new() .route("/", get(handlers::root)) .route("/health", get(handlers::health)) + .route("/robots.txt", get(handlers::robots_txt)) + .route("/sitemap.xml", get(handlers::sitemap_xml)) .route("/json", get(handlers::json_handler)) .route("/ip", get(handlers::ip_handler)) .route("/asn", get(handlers::asn_handler)) diff --git a/static/style.css b/static/style.css index 22efe5c..0d227b5 100644 --- a/static/style.css +++ b/static/style.css @@ -136,6 +136,7 @@ main { border-collapse: collapse; table-layout: fixed; + th, td { padding-block: 10px; border-block-start: 1px solid var(--color-separator); @@ -144,15 +145,17 @@ main { overflow-wrap: break-word; } + tr:last-child th, tr:last-child td { border-block-end: 1px solid var(--color-separator); } - .label { + th { inline-size: 42%; padding-inline: 24px 32px; font-weight: 600; color: var(--color-label); + text-align: start; } .bool-true { @@ -178,13 +181,14 @@ main { margin-block: 20px 16px; } + .info-table th, .info-table td { font-size: 14px; } } @media (max-width: 360px) { - .info-table .label { + .info-table th { inline-size: 36%; padding-inline: 12px 16px; } diff --git a/templates/index.html b/templates/index.html index 9cb2579..56ea516 100644 --- a/templates/index.html +++ b/templates/index.html @@ -4,6 +4,17 @@ What is my IP address? - {{ site_domain }} + + + + + + + + + + + @@ -23,6 +34,7 @@ placeholder="Type to search IP data" value="{{ query }}" autocomplete="off" + autocapitalize="none" spellcheck="false" > @@ -33,35 +45,35 @@ - + - + - + - + - + - + - + - + diff --git a/tests/integration.rs b/tests/integration.rs index 9983782..bdf5cf4 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -412,3 +412,65 @@ async fn field_endpoints_without_ip_or_header_return_400() { ); } } + +// --- SEO and accessibility tests --- + +#[tokio::test] +async fn robots_txt_returns_valid_response() { + let app = build_test_app(); + let (status, body) = get(&app, "/robots.txt").await; + assert_eq!(status, StatusCode::OK); + assert!(body.contains("User-agent: *")); + assert!(body.contains("Disallow: /json")); + assert!(body.contains("Disallow: /health")); + assert!(body.contains("Sitemap: https://test.example.com/sitemap.xml")); +} + +#[tokio::test] +async fn sitemap_xml_returns_valid_response() { + let app = build_test_app(); + let (status, body) = get(&app, "/sitemap.xml").await; + assert_eq!(status, StatusCode::OK); + assert!(body.contains("https://test.example.com/")); +} + +#[tokio::test] +async fn html_contains_seo_meta_tags() { + let app = build_test_app(); + let (status, body) = get_with_headers( + &app, + "/?ip=45.77.77.77", + vec![( + "User-Agent", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", + )], + ) + .await; + assert_eq!(status, StatusCode::OK); + assert!(body.contains("ASN")); + assert!(body.contains("")); + assert!(body.contains("autocapitalize=\"none\"")); +}
ASNASN {{ asn }}
OrgOrg {{ org }}
CountryCountry {{ country }}
CityCity {{ city }}
ProxyProxy {% if is_proxy %}Yes{% else %}No{% endif %}
VPNVPN {% if is_vpn %}Yes{% else %}No{% endif %}
HostingHosting {% if is_hosting %}Yes{% else %}No{% endif %}
TorTor {% if is_tor %}Yes{% else %}No{% endif %}
Country