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
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "web-petting" name = "web-petting"
version = "0.1.6" version = "0.1.7"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
+2
View File
@@ -792,6 +792,7 @@ struct SettingsForm {
contact_info: String, contact_info: String,
pricing_info: String, pricing_info: String,
timezone: String, timezone: String,
site_domain: String,
} }
async fn save_settings(request: Request, session: Session, db: Database) -> cot::Result<Response> { 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), ("contact_info", form.contact_info),
("pricing_info", form.pricing_info), ("pricing_info", form.pricing_info),
("timezone", form.timezone), ("timezone", form.timezone),
("site_domain", form.site_domain),
] { ] {
let k = key.to_string(); let k = key.to_string();
let existing = query!(Setting, $key == k).get(&db).await?; 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_contact_info: &'static str,
pub settings_pricing_info: &'static str, pub settings_pricing_info: &'static str,
pub settings_timezone: &'static str, pub settings_timezone: &'static str,
pub settings_site_domain: &'static str,
pub landing_contact_label: &'static str, pub landing_contact_label: &'static str,
pub landing_pricing_title: &'static str, pub landing_pricing_title: &'static str,
@@ -340,6 +341,7 @@ static RU: Translations = Translations {
settings_contact_info: "Контактная информация (отображается на лендинге)", settings_contact_info: "Контактная информация (отображается на лендинге)",
settings_pricing_info: "Блок с ценами (отображается на лендинге)", settings_pricing_info: "Блок с ценами (отображается на лендинге)",
settings_timezone: "Часовой пояс (например Asia/Vladivostok)", settings_timezone: "Часовой пояс (например Asia/Vladivostok)",
settings_site_domain: "Домен сайта (например https://example.com)",
landing_contact_label: "Или свяжитесь с нами напрямую", landing_contact_label: "Или свяжитесь с нами напрямую",
landing_pricing_title: "Стоимость", landing_pricing_title: "Стоимость",
@@ -539,6 +541,7 @@ static EN: Translations = Translations {
settings_contact_info: "Contact info (shown on landing page)", settings_contact_info: "Contact info (shown on landing page)",
settings_pricing_info: "Pricing block (shown on landing page)", settings_pricing_info: "Pricing block (shown on landing page)",
settings_timezone: "Timezone (e.g. Asia/Vladivostok)", 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_contact_label: "Or contact us directly",
landing_pricing_title: "Pricing", landing_pricing_title: "Pricing",
+110
View File
@@ -72,6 +72,8 @@ struct LandingTemplate<'a> {
contact_info: String, contact_info: String,
pricing_info: String, pricing_info: String,
testimonials: Vec<Testimonial>, testimonials: Vec<Testimonial>,
site_domain: String,
review_count: usize,
} }
#[derive(Debug, Template)] #[derive(Debug, Template)]
@@ -95,15 +97,24 @@ async fn landing_page(request: Request, db: Database) -> cot::Result<Response> {
.await? .await?
.map(|s| s.value) .map(|s| s.value)
.unwrap_or_default(); .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?; let mut testimonials = Testimonial::objects().all(&db).await?;
testimonials.retain(|t| t.status == "active"); testimonials.retain(|t| t.status == "active");
testimonials.sort_by(|a, b| a.sort_order.cmp(&b.sort_order)); testimonials.sort_by(|a, b| a.sort_order.cmp(&b.sort_order));
let review_count = testimonials.len();
let body = LandingTemplate { let body = LandingTemplate {
t: lang.t(), t: lang.t(),
lang, lang,
contact_info, contact_info,
pricing_info, pricing_info,
testimonials, testimonials,
site_domain,
review_count,
} }
.render()?; .render()?;
html_response(body, lang) 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 { pub fn public_router() -> Router {
Router::with_urls([ Router::with_urls([
Route::with_handler_and_name("/", landing_page, "landing"), 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("/submit", submit_lead, "submit-lead"),
Route::with_handler_and_name( Route::with_handler_and_name(
"/testimonial-image/{id}", "/testimonial-image/{id}",
Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 KiB

+1
View File
@@ -4,6 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ t.nav_title }} — {% block title %}{% endblock %}</title> <title>{{ t.nav_title }} — {% block title %}{% endblock %}</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1/css/bulma.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1/css/bulma.min.css">
<style> <style>
:root { --accent: #6c63ff; color-scheme: light; } :root { --accent: #6c63ff; color-scheme: light; }
+8 -1
View File
@@ -4,11 +4,18 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ t.nav_title }} — {{ t.login_title }}</title> <title>{{ t.nav_title }} — {{ t.login_title }}</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1/css/bulma.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1/css/bulma.min.css">
<style> <style>
body { background: #f5f5f5; display: flex; align-items: center; justify-content: center; min-height: 100vh; } :root { color-scheme: light; }
body { background: #f5f5f5; display: flex; align-items: center; justify-content: center; min-height: 100vh; color: #333; }
.login-box { width: 100%; max-width: 380px; padding: 0 1rem; } .login-box { width: 100%; max-width: 380px; padding: 0 1rem; }
.login-card { background: #fff; border-radius: 12px; padding: 2rem 1.5rem; box-shadow: 0 2px 12px rgba(0,0,0,0.06); } .login-card { background: #fff; border-radius: 12px; padding: 2rem 1.5rem; box-shadow: 0 2px 12px rgba(0,0,0,0.06); }
input, textarea, select, .input, .textarea, .select select {
background-color: #fff !important; color: #333 !important; border-color: #dbdbdb !important;
}
.label, label { color: #363636 !important; }
.notification { color: #333 !important; }
</style> </style>
</head> </head>
<body> <body>
+6
View File
@@ -38,6 +38,12 @@
<textarea class="input" name="pricing_info" rows="3" style="min-height:70px;resize:vertical;" placeholder="от 600 рублей за визит">{% for s in &settings %}{% if s.key == "pricing_info" %}{{ s.value }}{% endif %}{% endfor %}</textarea> <textarea class="input" name="pricing_info" rows="3" style="min-height:70px;resize:vertical;" placeholder="от 600 рублей за визит">{% for s in &settings %}{% if s.key == "pricing_info" %}{{ s.value }}{% endif %}{% endfor %}</textarea>
</div> </div>
</div> </div>
<div class="field">
<label class="label">{{ t.settings_site_domain }}</label>
<div class="control">
<input class="input" type="text" name="site_domain" placeholder="https://example.com" value="{% for s in &settings %}{% if s.key == "site_domain" %}{{ s.value }}{% endif %}{% endfor %}">
</div>
</div>
<div class="field"> <div class="field">
<label class="label">{{ t.settings_timezone }}</label> <label class="label">{{ t.settings_timezone }}</label>
<div class="control"> <div class="control">
+8 -1
View File
@@ -4,11 +4,18 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ t.nav_title }} — {{ t.setup_title }}</title> <title>{{ t.nav_title }} — {{ t.setup_title }}</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1/css/bulma.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1/css/bulma.min.css">
<style> <style>
body { background: #f5f5f5; display: flex; align-items: center; justify-content: center; min-height: 100vh; } :root { color-scheme: light; }
body { background: #f5f5f5; display: flex; align-items: center; justify-content: center; min-height: 100vh; color: #333; }
.login-box { width: 100%; max-width: 400px; padding: 0 1rem; } .login-box { width: 100%; max-width: 400px; padding: 0 1rem; }
.login-card { background: #fff; border-radius: 12px; padding: 2rem 1.5rem; box-shadow: 0 2px 12px rgba(0,0,0,0.06); } .login-card { background: #fff; border-radius: 12px; padding: 2rem 1.5rem; box-shadow: 0 2px 12px rgba(0,0,0,0.06); }
input, textarea, select, .input, .textarea, .select select {
background-color: #fff !important; color: #333 !important; border-color: #dbdbdb !important;
}
.label, label { color: #363636 !important; }
.notification { color: #333 !important; }
</style> </style>
</head> </head>
<body> <body>
+3
View File
@@ -4,7 +4,9 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ t.portal_title }} — {{ client.name }}</title> <title>{{ t.portal_title }} — {{ client.name }}</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<style> <style>
:root { color-scheme: light; }
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
@@ -54,6 +56,7 @@
.feedback-form textarea { .feedback-form textarea {
width: 100%; padding: 0.5rem 0.75rem; border: 1px solid #ddd; border-radius: 8px; width: 100%; padding: 0.5rem 0.75rem; border: 1px solid #ddd; border-radius: 8px;
font-size: 0.85rem; font-family: inherit; resize: vertical; min-height: 50px; font-size: 0.85rem; font-family: inherit; resize: vertical; min-height: 50px;
background-color: #fff; color: #333;
} }
.feedback-form textarea:focus { outline: none; border-color: #7c6cff; } .feedback-form textarea:focus { outline: none; border-color: #7c6cff; }
.feedback-form button { .feedback-form button {
+41 -58
View File
@@ -5,11 +5,26 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="{{ t.landing_meta_description }}"> <meta name="description" content="{{ t.landing_meta_description }}">
<title>{{ t.nav_title }} — {{ t.landing_hero_title }}</title> <title>{{ t.nav_title }} — {{ t.landing_hero_title }}</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<!-- Canonical & Hreflang -->
<link rel="canonical" href="{{ site_domain }}/?lang={{ lang.code() }}">
<link rel="alternate" hreflang="ru" href="{{ site_domain }}/?lang=ru">
<link rel="alternate" hreflang="en" href="{{ site_domain }}/?lang=en">
<link rel="alternate" hreflang="x-default" href="{{ site_domain }}/">
<!-- Open Graph --> <!-- Open Graph -->
<meta property="og:title" content="{{ t.nav_title }} — {{ t.landing_hero_title }}"> <meta property="og:title" content="{{ t.nav_title }} — {{ t.landing_hero_title }}">
<meta property="og:description" content="{{ t.landing_meta_description }}"> <meta property="og:description" content="{{ t.landing_meta_description }}">
<meta property="og:type" content="website"> <meta property="og:type" content="website">
<meta property="og:url" content="{{ site_domain }}/?lang={{ lang.code() }}">
<meta property="og:locale" content="{% if lang.code() == "ru" %}ru_RU{% else %}en_US{% endif %}">
<meta property="og:site_name" content="{{ t.nav_title }}">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="{{ t.nav_title }} — {{ t.landing_hero_title }}">
<meta name="twitter:description" content="{{ t.landing_meta_description }}">
<!-- Structured Data --> <!-- Structured Data -->
<script type="application/ld+json"> <script type="application/ld+json">
@@ -19,7 +34,15 @@
"name": "{{ t.nav_title }}", "name": "{{ t.nav_title }}",
"description": "{{ t.landing_meta_description }}", "description": "{{ t.landing_meta_description }}",
"serviceType": "Pet Sitting", "serviceType": "Pet Sitting",
"@id": "#business" "url": "{{ site_domain }}/",
"@id": "#business"{% if review_count > 0 %},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "5",
"bestRating": "5",
"ratingCount": "{{ review_count }}",
"reviewCount": "{{ review_count }}"
}{% endif %}
} }
</script> </script>
@@ -96,50 +119,24 @@
box-shadow: 0 4px 20px rgba(124,108,255,0.35); box-shadow: 0 4px 20px rgba(124,108,255,0.35);
} }
.hero-cta:hover { transform: translateY(-2px); box-shadow: 0 8px 30px rgba(124,108,255,0.45); } .hero-cta:hover { transform: translateY(-2px); box-shadow: 0 8px 30px rgba(124,108,255,0.45); }
.hero { position: relative; } .hero { position: relative; overflow: clip; }
.hero-blob { .hero-photo {
position: absolute; position: absolute; z-index: 0;
width: 420px; height: 420px;
border-radius: 50%; border-radius: 50%;
z-index: 0; border: 20px solid #ffe0ec;
background-size: cover; object-fit: cover;
background-position: center; box-sizing: border-box;
background-repeat: no-repeat;
}
.hero-blob-br {
width: 360px; height: 400px;
right: -100px; bottom: -110px;
background-color: #ffe0ec;
background-image: radial-gradient(ellipse at 40% 40%, #ffd6e8 0%, #f8c4d8 60%, #eab0d0 100%);
border-radius: 50%;
overflow: hidden;
}
.hero-blob-tl {
width: 240px; height: 270px;
left: -65px; top: -15px;
background-color: #d8ecff;
background-image: radial-gradient(ellipse at 60% 60%, #d0e8ff 0%, #b8d8f8 60%, #a4c8f0 100%);
border-radius: 50%;
overflow: hidden;
}
.hero-blob-bl {
width: 290px; height: 330px;
left: -80px; bottom: -90px;
background-color: #d4f0d8;
background-image: radial-gradient(ellipse at 55% 45%, #ddf5e0 0%, #c4e8ca 60%, #b0dbb8 100%);
border-radius: 50%;
overflow: hidden;
}
.hero-blob svg {
position: absolute; opacity: 0.3;
}
.hero-blob img {
width: 100%; height: 100%; object-fit: contain;
} }
.hero-photo-tr { right: -100px; top: -100px; }
.hero-photo-bl { left: -100px; bottom: -150px; }
.hero-content { position: relative; z-index: 1; } .hero-content { position: relative; z-index: 1; }
.section-alt { position: relative; z-index: 1; }
@media (max-width: 600px) { @media (max-width: 600px) {
.hero-blob-br { width: 200px; height: 240px; right: -55px; bottom: -65px; } .hero-photo { width: 200px; height: 200px; border-width: 12px; }
.hero-blob-bl { width: 180px; height: 210px; left: -50px; bottom: -55px; } .hero-photo-tr { right: -50px; top: -50px; }
.hero-blob-tl { width: 140px; height: 160px; left: -40px; top: -10px; } .hero-photo-bl { left: -50px; bottom: -70px; }
.hero-cta { display: block; width: fit-content; margin-left: auto; margin-right: 0; }
} }
.hero-emoji { font-size: 4rem; margin-bottom: 1rem; display: block; } .hero-emoji { font-size: 4rem; margin-bottom: 1rem; display: block; }
.hero-desc { .hero-desc {
@@ -230,7 +227,7 @@
transform: translateY(-4px); transform: translateY(-4px);
box-shadow: 0 12px 36px rgba(124,108,255,0.12); box-shadow: 0 12px 36px rgba(124,108,255,0.12);
} }
.service-icon { font-size: 2.5rem; margin-bottom: 0.75rem; display: block; } .service-icon { font-size: 2.5rem; margin-bottom: 0.75rem; display: block; text-align: center; }
.service-card h3 { font-size: 1.2rem; font-weight: 700; margin-bottom: 0.5rem; color: #2d2b55; } .service-card h3 { font-size: 1.2rem; font-weight: 700; margin-bottom: 0.5rem; color: #2d2b55; }
.service-card p { color: #5a5680; font-size: 0.95rem; line-height: 1.6; } .service-card p { color: #5a5680; font-size: 0.95rem; line-height: 1.6; }
@@ -335,22 +332,8 @@
<!-- Hero --> <!-- Hero -->
<section class="hero"> <section class="hero">
<div class="hero-blob hero-blob-tl"> <img class="hero-photo hero-photo-tr" src="/static/cat_up_right.png" alt="">
<svg viewBox="0 0 60 70" width="50" height="58" style="left:15%;top:18%;transform:rotate(-15deg);"><ellipse cx="30" cy="46" rx="16" ry="18" fill="#8cb8e8" opacity="0.55"/><ellipse cx="14" cy="22" rx="8" ry="10" fill="#8cb8e8" opacity="0.55" transform="rotate(-10 14 22)"/><ellipse cx="46" cy="22" rx="8" ry="10" fill="#8cb8e8" opacity="0.55" transform="rotate(10 46 22)"/><ellipse cx="24" cy="8" rx="6" ry="8" fill="#8cb8e8" opacity="0.55" transform="rotate(-5 24 8)"/><ellipse cx="38" cy="8" rx="6" ry="8" fill="#8cb8e8" opacity="0.55" transform="rotate(5 38 8)"/></svg> <img class="hero-photo hero-photo-bl" src="/static/cat_bottom_left.png" alt="">
<svg viewBox="0 0 60 55" width="32" height="30" style="right:20%;bottom:22%;transform:rotate(12deg);"><path d="M30 50 C10 35 0 22 0 14 C0 5 7 0 15 0 C21 0 26 3 30 9 C34 3 39 0 45 0 C53 0 60 5 60 14 C60 22 50 35 30 50Z" fill="#7baad4" opacity="0.5"/></svg>
<svg viewBox="0 0 80 40" width="44" height="22" style="left:50%;top:60%;transform:rotate(8deg);"><path d="M60 20 Q48 2 28 8 Q10 14 10 20 Q10 26 28 32 Q48 38 60 20Z" fill="#9ac4e8" opacity="0.3"/><path d="M60 20 L74 6 L74 34 Z" fill="#9ac4e8" opacity="0.3"/></svg>
</div>
<div class="hero-blob hero-blob-bl">
<svg viewBox="0 0 60 70" width="45" height="52" style="right:18%;top:15%;transform:rotate(20deg);"><path d="M30 65 C30 65 10 45 10 25 C10 10 25 2 30 2 C35 2 50 10 50 25 C50 45 30 65 30 65Z" fill="#8cc49a" opacity="0.35"/><path d="M30 60 L30 20 M30 35 Q20 30 15 25 M30 45 Q40 40 45 35" fill="none" stroke="#7ab88a" stroke-width="2" stroke-linecap="round"/></svg>
<svg viewBox="0 0 80 40" width="40" height="20" style="left:30%;bottom:25%;transform:rotate(-12deg);"><path d="M60 20 Q48 2 28 8 Q10 14 10 20 Q10 26 28 32 Q48 38 60 20Z" fill="#90c8a0" opacity="0.3"/><path d="M60 20 L74 6 L74 34 Z" fill="#90c8a0" opacity="0.3"/></svg>
<svg viewBox="0 0 60 55" width="30" height="28" style="right:30%;bottom:40%;transform:rotate(15deg);"><path d="M30 50 C10 35 0 22 0 14 C0 5 7 0 15 0 C21 0 26 3 30 9 C34 3 39 0 45 0 C53 0 60 5 60 14 C60 22 50 35 30 50Z" fill="#7cb88c" opacity="0.5"/></svg>
</div>
<div class="hero-blob hero-blob-br">
<svg viewBox="0 0 80 80" width="60" height="60" style="left:12%;top:15%;transform:rotate(10deg);"><path d="M20 15 L10 2 L18 18 M60 15 L70 2 L62 18 M40 70 C18 70 8 52 8 38 C8 20 22 8 40 8 C58 8 72 20 72 38 C72 52 62 70 40 70Z" fill="none" stroke="#e89cb8" stroke-width="3" stroke-linecap="round"/><circle cx="28" cy="36" r="3.5" fill="#e89cb8"/><circle cx="52" cy="36" r="3.5" fill="#e89cb8"/><ellipse cx="40" cy="48" rx="4" ry="2.5" fill="#e89cb8"/></svg>
<svg viewBox="0 0 60 70" width="40" height="47" style="right:15%;top:50%;transform:rotate(-20deg);"><ellipse cx="30" cy="46" rx="16" ry="18" fill="#e8a0b8" opacity="0.55"/><ellipse cx="14" cy="22" rx="8" ry="10" fill="#e8a0b8" opacity="0.55" transform="rotate(-10 14 22)"/><ellipse cx="46" cy="22" rx="8" ry="10" fill="#e8a0b8" opacity="0.55" transform="rotate(10 46 22)"/><ellipse cx="24" cy="8" rx="6" ry="8" fill="#e8a0b8" opacity="0.55" transform="rotate(-5 24 8)"/><ellipse cx="38" cy="8" rx="6" ry="8" fill="#e8a0b8" opacity="0.55" transform="rotate(5 38 8)"/></svg>
<svg viewBox="0 0 60 55" width="36" height="33" style="left:55%;bottom:18%;transform:rotate(-8deg);"><path d="M30 50 C10 35 0 22 0 14 C0 5 7 0 15 0 C21 0 26 3 30 9 C34 3 39 0 45 0 C53 0 60 5 60 14 C60 22 50 35 30 50Z" fill="#d48aaa" opacity="0.5"/></svg>
<svg viewBox="0 0 60 70" width="30" height="35" style="left:20%;bottom:30%;transform:rotate(25deg);"><path d="M30 65 C30 65 10 45 10 25 C10 10 25 2 30 2 C35 2 50 10 50 25 C50 45 30 65 30 65Z" fill="#e8b0c4" opacity="0.35"/><path d="M30 60 L30 20 M30 35 Q20 30 15 25 M30 45 Q40 40 45 35" fill="none" stroke="#d49ab0" stroke-width="2" stroke-linecap="round"/></svg>
</div>
<div class="hero-content"> <div class="hero-content">
<span class="hero-emoji" role="img" aria-label="pets">🐱🐹🦎</span> <span class="hero-emoji" role="img" aria-label="pets">🐱🐹🦎</span>
<h1>{{ t.landing_hero_title }}</h1> <h1>{{ t.landing_hero_title }}</h1>
+2
View File
@@ -4,7 +4,9 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ t.nav_title }} — {{ t.landing_thank_you_title }}</title> <title>{{ t.nav_title }} — {{ t.landing_thank_you_title }}</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<style> <style>
:root { color-scheme: light; }
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;