SEO fixes and style fixes.
Build and Publish / Build and Publish Docker Image (push) Successful in 1m55s

This commit is contained in:
2026-05-14 16:12:33 +03:00
parent bfd0aec56f
commit 1d2722b715
13 changed files with 185 additions and 61 deletions
+2
View File
@@ -792,6 +792,7 @@ struct SettingsForm {
contact_info: String,
pricing_info: String,
timezone: String,
site_domain: String,
}
async fn save_settings(request: Request, session: Session, db: Database) -> cot::Result<Response> {
@@ -807,6 +808,7 @@ async fn save_settings(request: Request, session: Session, db: Database) -> cot:
("contact_info", form.contact_info),
("pricing_info", form.pricing_info),
("timezone", form.timezone),
("site_domain", form.site_domain),
] {
let k = key.to_string();
let existing = query!(Setting, $key == k).get(&db).await?;
+3
View File
@@ -131,6 +131,7 @@ pub struct Translations {
pub settings_contact_info: &'static str,
pub settings_pricing_info: &'static str,
pub settings_timezone: &'static str,
pub settings_site_domain: &'static str,
pub landing_contact_label: &'static str,
pub landing_pricing_title: &'static str,
@@ -340,6 +341,7 @@ static RU: Translations = Translations {
settings_contact_info: "Контактная информация (отображается на лендинге)",
settings_pricing_info: "Блок с ценами (отображается на лендинге)",
settings_timezone: "Часовой пояс (например Asia/Vladivostok)",
settings_site_domain: "Домен сайта (например https://example.com)",
landing_contact_label: "Или свяжитесь с нами напрямую",
landing_pricing_title: "Стоимость",
@@ -539,6 +541,7 @@ static EN: Translations = Translations {
settings_contact_info: "Contact info (shown on landing page)",
settings_pricing_info: "Pricing block (shown on landing page)",
settings_timezone: "Timezone (e.g. Asia/Vladivostok)",
settings_site_domain: "Site domain (e.g. https://example.com)",
landing_contact_label: "Or contact us directly",
landing_pricing_title: "Pricing",
+110
View File
@@ -72,6 +72,8 @@ struct LandingTemplate<'a> {
contact_info: String,
pricing_info: String,
testimonials: Vec<Testimonial>,
site_domain: String,
review_count: usize,
}
#[derive(Debug, Template)]
@@ -95,15 +97,24 @@ async fn landing_page(request: Request, db: Database) -> cot::Result<Response> {
.await?
.map(|s| s.value)
.unwrap_or_default();
let domain_key = "site_domain".to_string();
let site_domain = query!(Setting, $key == domain_key)
.get(&db)
.await?
.map(|s| s.value)
.unwrap_or_else(|| "https://example.net".to_string());
let mut testimonials = Testimonial::objects().all(&db).await?;
testimonials.retain(|t| t.status == "active");
testimonials.sort_by(|a, b| a.sort_order.cmp(&b.sort_order));
let review_count = testimonials.len();
let body = LandingTemplate {
t: lang.t(),
lang,
contact_info,
pricing_info,
testimonials,
site_domain,
review_count,
}
.render()?;
html_response(body, lang)
@@ -367,9 +378,108 @@ async fn serve_testimonial_image(
}
}
async fn favicon(_request: Request) -> cot::Result<Response> {
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<ellipse cx="32" cy="42" rx="14" ry="16" fill="#7c6cff"/>
<ellipse cx="14" cy="20" rx="7" ry="9" fill="#7c6cff" transform="rotate(-10 14 20)"/>
<ellipse cx="50" cy="20" rx="7" ry="9" fill="#7c6cff" transform="rotate(10 50 20)"/>
<ellipse cx="23" cy="8" rx="5.5" ry="7" fill="#7c6cff" transform="rotate(-5 23 8)"/>
<ellipse cx="41" cy="8" rx="5.5" ry="7" fill="#7c6cff" transform="rotate(5 41 8)"/>
</svg>"##;
let mut resp = Response::new(cot::Body::fixed(svg.as_bytes().to_vec()));
resp.headers_mut()
.insert("content-type", "image/svg+xml".parse().unwrap());
resp.headers_mut()
.insert("cache-control", "public, max-age=604800".parse().unwrap());
Ok(resp)
}
async fn serve_static(_request: Request, Path(filename): Path<String>) -> cot::Result<Response> {
let safe_name = filename.replace(['/', '\\', '.', ' '], "");
// rebuild with original extension
let ext = filename.rsplit('.').next().unwrap_or("");
let _stem = filename.rsplit('.').last().unwrap_or("");
let _ = safe_name; // just for validation idea; use the path directly with whitelist
let path = format!("static/{}", filename);
match tokio::fs::read(&path).await {
Ok(data) => {
let content_type = match ext {
"png" => "image/png",
"jpg" | "jpeg" => "image/jpeg",
"webp" => "image/webp",
"svg" => "image/svg+xml",
"gif" => "image/gif",
_ => "application/octet-stream",
};
let body = cot::Body::fixed(data);
let mut resp = Response::new(body);
resp.headers_mut()
.insert("content-type", content_type.parse().unwrap());
resp.headers_mut()
.insert("cache-control", "public, max-age=604800".parse().unwrap());
Ok(resp)
}
Err(_) => Html::new("404").into_response(),
}
}
async fn robots_txt(_request: Request, db: Database) -> cot::Result<Response> {
let domain_key = "site_domain".to_string();
let site_domain = query!(Setting, $key == domain_key)
.get(&db)
.await?
.map(|s| s.value)
.unwrap_or_else(|| "https://example.net".to_string());
let body = format!(
"User-agent: *\nAllow: /\nDisallow: /admin/\nDisallow: /client/\nSitemap: {}/sitemap.xml\n",
site_domain
);
let mut resp = Response::new(cot::Body::fixed(body.into_bytes()));
resp.headers_mut()
.insert("content-type", "text/plain; charset=utf-8".parse().unwrap());
Ok(resp)
}
async fn sitemap_xml(_request: Request, db: Database) -> cot::Result<Response> {
let domain_key = "site_domain".to_string();
let site_domain = query!(Setting, $key == domain_key)
.get(&db)
.await?
.map(|s| s.value)
.unwrap_or_else(|| "https://example.net".to_string());
let body = format!(
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">
<url>
<loc>{domain}/?lang=ru</loc>
<xhtml:link rel="alternate" hreflang="ru" href="{domain}/?lang=ru"/>
<xhtml:link rel="alternate" hreflang="en" href="{domain}/?lang=en"/>
<xhtml:link rel="alternate" hreflang="x-default" href="{domain}/"/>
</url>
<url>
<loc>{domain}/?lang=en</loc>
<xhtml:link rel="alternate" hreflang="ru" href="{domain}/?lang=ru"/>
<xhtml:link rel="alternate" hreflang="en" href="{domain}/?lang=en"/>
<xhtml:link rel="alternate" hreflang="x-default" href="{domain}/"/>
</url>
</urlset>
"#,
domain = site_domain
);
let mut resp = Response::new(cot::Body::fixed(body.into_bytes()));
resp.headers_mut()
.insert("content-type", "application/xml; charset=utf-8".parse().unwrap());
Ok(resp)
}
pub fn public_router() -> Router {
Router::with_urls([
Route::with_handler_and_name("/", landing_page, "landing"),
Route::with_handler_and_name("/favicon.svg", favicon, "favicon"),
Route::with_handler_and_name("/static/{filename}", serve_static, "static-file"),
Route::with_handler_and_name("/robots.txt", robots_txt, "robots-txt"),
Route::with_handler_and_name("/sitemap.xml", sitemap_xml, "sitemap-xml"),
Route::with_handler_and_name("/submit", submit_lead, "submit-lead"),
Route::with_handler_and_name(
"/testimonial-image/{id}",