feat(seo/ux): enrich homepage meta, schema, and add conversion CTAs
CI/CD / CI · dotnet build (push) Successful in 31s
CI/CD / Deploy · drsousan (push) Successful in 24s

SEO:
- Clean keyword-rich meta description (trim stray whitespace from editable subtitle)
- Add robots, author, theme-color meta tags
- Add og:url, og:site_name, og:image:alt + full Twitter card tags
- Enrich MedicalBusiness JSON-LD: image, areaServed, priceRange,
  sameAs (social), aggregateRating (from testimonials), @id
- Add FAQPage JSON-LD for rich results (loops over active FAQs)
- Keyword-rich alt text on hero + about images

UX / conversion:
- Tap-to-call phone (tel:), mailto email, Google Maps link for address
- Floating WhatsApp + Call buttons (sticky, RTL bottom-left)
- Hero image: width/height + fetchpriority=high + decoding=async (LCP/CLS)
- Fix hero-name nowrap overflow risk on small screens

Both JSON-LD blocks validated as well-formed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-10 23:53:57 +03:30
parent 21769deda6
commit 0c4315063e
+110 -16
View File
@@ -15,37 +15,104 @@
var ig = c.GetValueOrDefault("instagram","");
var wa = c.GetValueOrDefault("whatsapp","");
var tg = c.GetValueOrDefault("telegram","");
var phone = c.GetValueOrDefault("phone","");
var email = c.GetValueOrDefault("email","");
var address = c.GetValueOrDefault("address","");
// Clean, keyword-rich meta description (trim stray whitespace/newlines from the editable subtitle)
var rawSubtitle = (h.GetValueOrDefault("subtitle","") ?? "").Replace("\n"," ").Replace("\r"," ").Trim();
while (rawSubtitle.Contains(" ")) rawSubtitle = rawSubtitle.Replace(" "," ");
var metaDesc = string.IsNullOrWhiteSpace(rawSubtitle)
? "دکتر سوسن آل‌طه، متخصص زیبایی پوست در تهران. خدمات بوتاکس، فیلر، لیزر موهای زائد، مزوتراپی و پاکسازی پوست با تمرکز بر نتایج طبیعی. رزرو نوبت و مشاوره."
: rawSubtitle;
if (metaDesc.Length > 160) metaDesc = metaDesc.Substring(0, 157).TrimEnd() + "…";
var absHero = string.IsNullOrEmpty(heroImg) ? "" : (heroImg.StartsWith("http") ? heroImg : siteBaseUrl + heroImg);
// Aggregate rating from active testimonials (for rich results)
var ratedReviews = Model.Testimonials.Where(t => t.Rating > 0).ToList();
var reviewCount = ratedReviews.Count;
var avgRating = reviewCount > 0 ? Math.Round(ratedReviews.Average(t => t.Rating), 1) : 0d;
// Social profiles for schema sameAs
var socials = new[] { ig, tg }.Where(s => !string.IsNullOrEmpty(s) && s.StartsWith("http")).ToList();
}
@section Head {
<title>@ViewData["Title"]</title>
<meta name="description" content="@h.GetValueOrDefault("subtitle","")" />
<meta name="keywords" content="دکتر پوست تهران,بوتاکس,لیزر موهای زائد,فیلر,مزوتراپی,زیبایی پوست" />
<meta name="description" content="@metaDesc" />
<meta name="keywords" content="دکتر پوست تهران,متخصص پوست تهران,بوتاکس,لیزر موهای زائد,فیلر,مزوتراپی,پاکسازی پوست,جوانسازی پوست,دکتر سوسن آل‌طه" />
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1" />
<meta name="author" content="@siteName" />
<meta name="theme-color" content="#B8955A" />
<link rel="canonical" href="@(siteBaseUrl + "/")" />
<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:site_name" content="@siteName" />
<meta property="og:title" content="@ViewData["Title"]" />
<meta property="og:description" content="@h.GetValueOrDefault("subtitle","")" />
<meta property="og:description" content="@metaDesc" />
<meta property="og:url" content="@(siteBaseUrl + "/")" />
<meta property="og:locale" content="fa_IR" />
@if (!string.IsNullOrEmpty(heroImg))
@if (!string.IsNullOrEmpty(absHero))
{
var absHeroImg = heroImg.StartsWith("http") ? heroImg : (siteBaseUrl + heroImg);
<meta property="og:image" content="@absHeroImg" />
<meta property="og:image" content="@absHero" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="@siteName" />
}
<link rel="canonical" href="@(siteBaseUrl + "/")" />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="@ViewData["Title"]" />
<meta name="twitter:description" content="@metaDesc" />
@if (!string.IsNullOrEmpty(absHero))
{
<meta name="twitter:image" content="@absHero" />
}
<!-- Structured data: medical practice -->
<script type="application/ld+json">
{
"@@context":"https://schema.org",
"@@type":["MedicalBusiness","LocalBusiness"],
"@@id":"@(siteBaseUrl)/#business",
"name":"@siteName",
"description":"@h.GetValueOrDefault("subtitle","")",
"description":"@metaDesc",
"url":"@(siteBaseUrl)",
"telephone":"@c.GetValueOrDefault("phone","")",
"address":{"@@type":"PostalAddress","addressLocality":"تهران","addressCountry":"IR","streetAddress":"@c.GetValueOrDefault("address","")"},
"telephone":"@phone",
"priceRange":"$$",
"image":"@(string.IsNullOrEmpty(absHero) ? siteBaseUrl : absHero)",
"address":{"@@type":"PostalAddress","addressLocality":"تهران","addressRegion":"تهران","addressCountry":"IR","streetAddress":"@address"},
"areaServed":{"@@type":"City","name":"تهران"},
"openingHours":"@c.GetValueOrDefault("hours","")",
"medicalSpecialty":"Dermatology"
"medicalSpecialty":"Dermatology"@(socials.Any() ? "," : "")
@if (socials.Any())
{
@Html.Raw("\"sameAs\":[" + string.Join(",", socials.Select(s => "\"" + s + "\"")) + "]")
}@(reviewCount > 0 ? "," : "")
@if (reviewCount > 0)
{
@Html.Raw("\"aggregateRating\":{\"@type\":\"AggregateRating\",\"ratingValue\":\"" + avgRating + "\",\"reviewCount\":\"" + reviewCount + "\",\"bestRating\":\"5\"}")
}
}
</script>
<!-- Structured data: FAQ (rich results) -->
@if (Model.Faqs.Any())
{
<script type="application/ld+json">
{
"@@context":"https://schema.org",
"@@type":"FAQPage",
"mainEntity":[
@Html.Raw(string.Join(",", Model.Faqs.Select(f =>
"{\"@type\":\"Question\",\"name\":" + System.Text.Json.JsonSerializer.Serialize(f.Question) +
",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":" + System.Text.Json.JsonSerializer.Serialize(f.Answer) + "}}")))
]
}
</script>
}
<style>
/* ─── Hero ─────────────────────────────────────────────────── */
#hero { min-height:100svh; display:flex; align-items:center; padding:100px 0 3rem; position:relative; overflow:hidden; }
@@ -226,6 +293,16 @@
.form-group textarea { resize:vertical; min-height:110px; }
.form-submit { width:100%; background:var(--gold); color:var(--white); border:none; padding:0.9rem; border-radius:12px; font-family:'Vazirmatn',sans-serif; font-size:0.95rem; font-weight:600; cursor:pointer; transition:background 0.25s, transform 0.2s; }
.form-submit:hover { background:var(--gold-light); transform:translateY(-2px); }
/* ─── Floating contact buttons ─────────────────────────────── */
.fab-stack { position:fixed; bottom:1.5rem; left:1.5rem; z-index:90; display:flex; flex-direction:column; gap:0.7rem; }
.fab { width:54px; height:54px; border-radius:50%; display:flex; align-items:center; justify-content:center; box-shadow:0 6px 20px rgba(0,0,0,0.18); color:#fff; transition:transform 0.2s, box-shadow 0.2s; position:relative; }
.fab:hover { transform:translateY(-3px) scale(1.05); box-shadow:0 10px 28px rgba(0,0,0,0.25); }
.fab svg { width:26px; height:26px; }
.fab-wa { background:#25D366; }
.fab-call { background:var(--gold); }
.fab-pulse::after { content:''; position:absolute; inset:0; border-radius:50%; background:inherit; opacity:0.55; animation:fabPulse 2s ease-out infinite; z-index:-1; }
@@keyframes fabPulse { 0% { transform:scale(1); opacity:0.5; } 100% { transform:scale(1.9); opacity:0; } }
@@media (max-width:600px) { .fab-stack { bottom:1rem; left:1rem; gap:0.6rem; } .fab { width:50px; height:50px; } }
/* ─── Responsive ───────────────────────────────────────────── */
@@media (max-width:900px) {
.hero-inner { grid-template-columns:1fr; text-align:center; gap:2.5rem; }
@@ -245,6 +322,7 @@
@@media (max-width:600px) {
section { padding:3.5rem 0; }
.container { padding:0 1.2rem; }
.hero-name { white-space:normal; }
.hero-inner { padding:0 1.2rem; gap:2rem; }
.hero-image { max-width:260px; }
.hero-stats { gap:1.5rem; }
@@ -301,7 +379,7 @@
<div class="hero-image-frame">
@if (!string.IsNullOrEmpty(heroImg))
{
<img src="@heroImg" alt="@siteName" />
<img src="@heroImg" alt="@siteName — متخصص زیبایی پوست در تهران" width="640" height="800" fetchpriority="high" decoding="async" />
}
else
{
@@ -340,7 +418,7 @@
<div class="about-img-box">
@if (!string.IsNullOrEmpty(aboutImg))
{
<img src="@aboutImg" alt="@siteName" />
<img src="@aboutImg" alt="درباره @siteName — پزشک و متخصص زیبایی پوست" width="600" height="800" loading="lazy" decoding="async" />
}
else
{
@@ -681,7 +759,7 @@
</div>
<div class="info-text">
<strong>تلفن تماس</strong>
<p>@c.GetValueOrDefault("phone","")</p>
<p><a href="tel:@(new string(phone.Where(ch => char.IsDigit(ch) || ch=='+').ToArray()))" style="color:inherit">@phone</a></p>
</div>
</div>
}
@@ -696,7 +774,7 @@
</div>
<div class="info-text">
<strong>ایمیل</strong>
<p>@c.GetValueOrDefault("email","")</p>
<p><a href="mailto:@email" style="color:inherit">@email</a></p>
</div>
</div>
}
@@ -711,7 +789,7 @@
</div>
<div class="info-text">
<strong>آدرس مطب</strong>
<p>@c.GetValueOrDefault("address","")</p>
<p><a href="https://www.google.com/maps/search/?api=1&query=@Uri.EscapeDataString(address)" target="_blank" rel="noopener" style="color:inherit">@address</a></p>
</div>
</div>
}
@@ -805,6 +883,22 @@
</div>
</section>
<!-- ══════ FLOATING CONTACT BUTTONS ══════ -->
<div class="fab-stack">
@if (!string.IsNullOrEmpty(wa))
{
<a href="@wa" class="fab fab-wa fab-pulse" target="_blank" rel="noopener" aria-label="تماس از طریق واتساپ" title="واتساپ">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M.057 24l1.687-6.163a11.867 11.867 0 0 1-1.587-5.946C.16 5.335 5.495 0 12.05 0a11.82 11.82 0 0 1 8.413 3.488 11.82 11.82 0 0 1 3.48 8.414c-.003 6.557-5.338 11.892-11.893 11.892a11.9 11.9 0 0 1-5.688-1.448L.057 24zm6.597-3.807c1.676.995 3.276 1.591 5.392 1.592 5.448 0 9.886-4.434 9.889-9.885.002-5.462-4.415-9.89-9.881-9.892-5.452 0-9.887 4.434-9.889 9.884a9.86 9.86 0 0 0 1.51 5.26l-.999 3.648 3.737-.961zm11.387-5.464c-.074-.124-.272-.198-.57-.347-.297-.149-1.758-.868-2.031-.967-.272-.099-.47-.149-.669.149-.198.297-.768.967-.941 1.165-.173.198-.347.223-.644.074-.297-.149-1.255-.462-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.297-.347.446-.521.151-.172.2-.296.3-.495.099-.198.05-.372-.025-.521-.075-.148-.669-1.611-.916-2.206-.242-.579-.487-.501-.669-.51l-.57-.01c-.198 0-.52.074-.792.372s-1.04 1.016-1.04 2.479 1.065 2.876 1.213 3.074c.149.198 2.095 3.2 5.076 4.487.709.306 1.263.489 1.694.626.712.226 1.36.194 1.872.118.571-.085 1.758-.719 2.006-1.413.248-.695.248-1.29.173-1.414z"/></svg>
</a>
}
@if (!string.IsNullOrEmpty(phone))
{
<a href="tel:@(new string(phone.Where(ch => char.IsDigit(ch) || ch=='+').ToArray()))" class="fab fab-call" aria-label="تماس تلفنی" title="تماس تلفنی">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 13.6a19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 3.6 3h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L7.91 10.6a16 16 0 0 0 6 6l.91-.91a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z"/></svg>
</a>
}
</div>
@section Scripts {
<script>
// Before/After toggle