SEO fixes and style fixes.
Build and Publish / Build and Publish Docker Image (push) Successful in 1m55s
Build and Publish / Build and Publish Docker Image (push) Successful in 1m55s
This commit is contained in:
@@ -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?;
|
||||
|
||||
@@ -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
@@ -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}",
|
||||
|
||||
Reference in New Issue
Block a user