mirror of
https://github.com/NetworkCats/rustyip.git
synced 2026-11-13 16:17:46 +00:00
feat: improve SEO meta tags, robots.txt, sitemap.xml, and table accessibility
This commit is contained in:
+40
-1
@@ -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<AppState>) -> 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<AppState>) -> Response {
|
||||
let body = format!(
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
|
||||
<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n\
|
||||
\x20 <url>\n\
|
||||
\x20 <loc>https://{}/</loc>\n\
|
||||
\x20 </url>\n\
|
||||
</urlset>\n",
|
||||
state.site_domain
|
||||
);
|
||||
(
|
||||
[(header::CONTENT_TYPE, "application/xml; charset=utf-8")],
|
||||
body,
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
+6
-2
@@ -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;
|
||||
}
|
||||
|
||||
+20
-8
@@ -4,6 +4,17 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>What is my IP address? - {{ site_domain }}</title>
|
||||
<meta name="description" content="Find your public IP address instantly. Look up geolocation, ASN, organization, and proxy/VPN/Tor detection for any IP address.">
|
||||
<link rel="canonical" href="https://{{ site_domain }}/">
|
||||
<meta name="theme-color" content="#007AFF" media="(prefers-color-scheme: light)">
|
||||
<meta name="theme-color" content="#0A84FF" media="(prefers-color-scheme: dark)">
|
||||
<meta property="og:title" content="What is my IP address? - {{ site_domain }}">
|
||||
<meta property="og:description" content="Find your public IP address instantly. Look up geolocation, ASN, organization, and proxy/VPN/Tor detection for any IP address.">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://{{ site_domain }}/">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="What is my IP address? - {{ site_domain }}">
|
||||
<meta name="twitter:description" content="Find your public IP address instantly. Look up geolocation, ASN, organization, and proxy/VPN/Tor detection for any IP address.">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;600&display=swap">
|
||||
@@ -23,6 +34,7 @@
|
||||
placeholder="Type to search IP data"
|
||||
value="{{ query }}"
|
||||
autocomplete="off"
|
||||
autocapitalize="none"
|
||||
spellcheck="false"
|
||||
>
|
||||
</div>
|
||||
@@ -33,35 +45,35 @@
|
||||
<table class="info-table" aria-label="IP address information">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="label">ASN</td>
|
||||
<th scope="row">ASN</th>
|
||||
<td>{{ asn }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Org</td>
|
||||
<th scope="row">Org</th>
|
||||
<td>{{ org }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Country</td>
|
||||
<th scope="row">Country</th>
|
||||
<td>{{ country }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">City</td>
|
||||
<th scope="row">City</th>
|
||||
<td>{{ city }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Proxy</td>
|
||||
<th scope="row">Proxy</th>
|
||||
<td>{% if is_proxy %}<i class="icon-check bool-true" aria-hidden="true"></i><span class="sr-only">Yes</span>{% else %}<i class="icon-minus bool-false" aria-hidden="true"></i><span class="sr-only">No</span>{% endif %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">VPN</td>
|
||||
<th scope="row">VPN</th>
|
||||
<td>{% if is_vpn %}<i class="icon-check bool-true" aria-hidden="true"></i><span class="sr-only">Yes</span>{% else %}<i class="icon-minus bool-false" aria-hidden="true"></i><span class="sr-only">No</span>{% endif %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Hosting</td>
|
||||
<th scope="row">Hosting</th>
|
||||
<td>{% if is_hosting %}<i class="icon-check bool-true" aria-hidden="true"></i><span class="sr-only">Yes</span>{% else %}<i class="icon-minus bool-false" aria-hidden="true"></i><span class="sr-only">No</span>{% endif %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Tor</td>
|
||||
<th scope="row">Tor</th>
|
||||
<td>{% if is_tor %}<i class="icon-check bool-true" aria-hidden="true"></i><span class="sr-only">Yes</span>{% else %}<i class="icon-minus bool-false" aria-hidden="true"></i><span class="sr-only">No</span>{% endif %}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -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("<?xml version=\"1.0\""));
|
||||
assert!(body.contains("<loc>https://test.example.com/</loc>"));
|
||||
}
|
||||
|
||||
#[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("<meta name=\"description\""));
|
||||
assert!(body.contains("<link rel=\"canonical\""));
|
||||
assert!(body.contains("<meta property=\"og:title\""));
|
||||
assert!(body.contains("<meta property=\"og:description\""));
|
||||
assert!(body.contains("<meta property=\"og:type\" content=\"website\""));
|
||||
assert!(body.contains("<meta name=\"twitter:card\" content=\"summary\""));
|
||||
assert!(body.contains("<meta name=\"theme-color\""));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn html_uses_th_for_row_headers() {
|
||||
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("<th scope=\"row\">ASN</th>"));
|
||||
assert!(body.contains("<th scope=\"row\">Country</th>"));
|
||||
assert!(body.contains("autocapitalize=\"none\""));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user