Files
soroush.asadi 00a138fe46
CI/CD / CI · dotnet build (push) Successful in 2m1s
CI/CD / Deploy · drsousan (push) Successful in 35s
feat(seo): complete OG/Twitter/structured-data coverage + clean encoding
Blog list (/blog): add robots, full Open Graph + Twitter, Blog +
BreadcrumbList JSON-LD, per-page self-canonical, and rel=prev/next for
paginated pages.

Blog post: add robots, og:site_name, article:published_time /
modified_time / author / section, twitter:image, og:image:alt, and a
BreadcrumbList JSON-LD (Home → Blog → Category → Post).

Gallery (/gallery): add robots, full OG + Twitter (with first image as
og:image), ImageGallery + BreadcrumbList JSON-LD.

Encoding: register HtmlEncoder.Create(UnicodeRanges.All) so Persian text
in meta tags and JSON-LD renders literally instead of &#xXXXX; entities
(smaller, cleaner output; friendlier to SEO validators).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 13:44:37 +03:30

165 lines
9.9 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@page "/blog"
@model DrSousan.Api.Pages.Blog.BlogIndexModel
@{
var blogBase = Environment.GetEnvironmentVariable("SITE_BASE_URL")?.TrimEnd('/') ?? (Request.Scheme + "://" + Request.Host);
var catQs = string.IsNullOrEmpty(Model.ActiveCat) ? "" : "&category=" + Model.ActiveCat;
string PageUrl(int p) => p <= 1
? blogBase + "/blog" + (string.IsNullOrEmpty(Model.ActiveCat) ? "" : "?category=" + Model.ActiveCat)
: blogBase + "/blog?pg=" + p + catQs;
var blogDesc = "مقالات تخصصی دکتر سوسن آل‌طه درباره زیبایی پوست، بوتاکس، فیلر، لیزر و مراقبت از پوست.";
}
@section Head {
<title>@ViewData["Title"]</title>
<meta name="description" content="@blogDesc" />
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1" />
<meta name="author" content="@ViewData["SiteName"]" />
<meta name="theme-color" content="#B8955A" />
<link rel="canonical" href="@PageUrl(Model.CurrentPage)" />
@if (Model.CurrentPage > 1) { <link rel="prev" href="@PageUrl(Model.CurrentPage - 1)" /> }
@if (Model.CurrentPage < Model.TotalPages) { <link rel="next" href="@PageUrl(Model.CurrentPage + 1)" /> }
<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:site_name" content="@ViewData["SiteName"]" />
<meta property="og:title" content="@ViewData["Title"]" />
<meta property="og:description" content="@blogDesc" />
<meta property="og:url" content="@PageUrl(Model.CurrentPage)" />
<meta property="og:locale" content="fa_IR" />
<!-- Twitter -->
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="@ViewData["Title"]" />
<meta name="twitter:description" content="@blogDesc" />
<!-- Structured data: Blog + breadcrumb -->
<script type="application/ld+json">
{
"@@context": "https://schema.org",
"@@type": "Blog",
"name": "@ViewData["Title"]",
"description": "@blogDesc",
"url": "@(blogBase)/blog",
"inLanguage": "fa-IR",
"publisher": { "@@type": "Organization", "name": "@ViewData["SiteName"]", "url": "@blogBase" }
}
</script>
<script type="application/ld+json">
{
"@@context": "https://schema.org",
"@@type": "BreadcrumbList",
"itemListElement": [
{ "@@type": "ListItem", "position": 1, "name": "خانه", "item": "@blogBase/" },
{ "@@type": "ListItem", "position": 2, "name": "وبلاگ", "item": "@blogBase/blog" }
]
}
</script>
<style>
/* ─── Blog Hero ─────────────────────────────────────────────── */
.blog-hero{background:linear-gradient(135deg,var(--gold-pale) 0%,#EDE0CA 100%);padding:6rem 2rem 3rem;text-align:center}
.blog-hero h1{font-size:clamp(1.8rem,4vw,2.5rem);font-weight:700;color:var(--dark);margin-bottom:.6rem}
.blog-hero p{font-size:1rem;color:var(--mid);max-width:520px;margin:0 auto}
/* ─── Search ─────────────────────────────────────────────────── */
.search-wrap{max-width:480px;margin:1.5rem auto 0;position:relative}
.search-wrap input{width:100%;border:1.5px solid var(--border);border-radius:50px;padding:.65rem 1.2rem .65rem 3rem;font-family:'Vazirmatn',sans-serif;font-size:.9rem;direction:rtl;outline:none;background:var(--white);transition:border-color .2s}
.search-wrap input:focus{border-color:var(--gold)}
.search-wrap svg{position:absolute;left:1rem;top:50%;transform:translateY(-50%);width:18px;height:18px;color:var(--light)}
/* ─── Filter ─────────────────────────────────────────────────── */
.filter-bar{max-width:1100px;margin:2rem auto 0;padding:0 2rem;display:flex;gap:.6rem;flex-wrap:wrap}
.filter-btn{background:transparent;border:1.5px solid var(--border);color:var(--mid);padding:.4rem 1.1rem;border-radius:50px;font-family:'Vazirmatn',sans-serif;font-size:.85rem;cursor:pointer;transition:all .2s;text-decoration:none;display:inline-block}
.filter-btn.active,.filter-btn:hover{background:var(--gold);border-color:var(--gold);color:var(--white)}
/* ─── Blog Grid ──────────────────────────────────────────────── */
.blog-grid{max-width:1100px;margin:2rem auto;padding:0 2rem;display:grid;grid-template-columns:repeat(3,1fr);gap:1.5rem}
@@media(max-width:900px){.blog-grid{grid-template-columns:repeat(2,1fr)}}
@@media(max-width:600px){.blog-grid{grid-template-columns:1fr}}
.post-card{background:var(--white);border-radius:16px;border:1px solid var(--border);overflow:hidden;transition:transform .3s,box-shadow .3s;display:flex;flex-direction:column}
.post-card:hover{transform:translateY(-4px);box-shadow:0 12px 40px rgba(184,149,90,.15)}
.post-card-img{aspect-ratio:16/9;background:linear-gradient(135deg,var(--gold-pale),#EDE0CA);display:flex;align-items:center;justify-content:center;color:var(--gold);font-size:2rem;overflow:hidden}
.post-card-img img{width:100%;height:100%;object-fit:cover}
.post-card-body{padding:1.3rem;flex:1;display:flex;flex-direction:column;gap:.6rem}
.post-cat{font-size:.72rem;font-weight:600;color:var(--gold);background:var(--gold-pale);padding:.2rem .7rem;border-radius:50px;display:inline-block}
.post-title{font-size:1rem;font-weight:600;color:var(--dark);line-height:1.5}
.post-title:hover{color:var(--gold)}
.post-excerpt{font-size:.85rem;color:var(--mid);line-height:1.7;flex:1}
.post-meta{display:flex;align-items:center;justify-content:space-between;font-size:.75rem;color:var(--light);margin-top:auto;padding-top:.6rem;border-top:1px solid var(--border)}
.read-more{color:var(--gold);font-weight:500;font-size:.82rem}
/* ─── Empty ──────────────────────────────────────────────────── */
.empty{text-align:center;padding:4rem 2rem;color:var(--light);grid-column:1/-1}
/* ─── Pagination ─────────────────────────────────────────────── */
.pagination{display:flex;gap:.5rem;justify-content:center;padding:2rem;margin-top:1rem;max-width:1100px;margin-left:auto;margin-right:auto}
.page-btn{width:38px;height:38px;border-radius:8px;border:1.5px solid var(--border);background:transparent;font-family:'Vazirmatn',sans-serif;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s;text-decoration:none;color:var(--dark);font-size:.9rem}
.page-btn.active,.page-btn:hover{background:var(--gold);border-color:var(--gold);color:var(--white)}
</style>
}
<div class="blog-hero">
<h1>وبلاگ تخصصی پوست و زیبایی</h1>
<p>آخرین مقالات و راهنماهای تخصصی درباره مراقبت از پوست، زیبایی و درمان‌های تخصصی</p>
<div class="search-wrap">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input type="text" id="searchInput" placeholder="جستجو در مقالات..." />
</div>
</div>
<div class="filter-bar">
<a href="/blog" class="filter-btn @(string.IsNullOrEmpty(Model.ActiveCat) ? "active" : "")">همه</a>
@foreach (var cat in Model.Categories)
{
var active = Model.ActiveCat == cat.Slug;
var count = cat.Posts.Count(p => p.IsPublished);
<a href="/blog?category=@cat.Slug" class="filter-btn @(active ? "active" : "")">@cat.Name (@count)</a>
}
</div>
<div class="blog-grid" id="blogGrid">
@if (!Model.Posts.Any())
{
<div class="empty"><p>مقاله‌ای یافت نشد.</p></div>
}
else
{
@foreach (var post in Model.Posts)
{
<div class="post-card">
<div class="post-card-img">
@if (!string.IsNullOrEmpty(post.FeaturedImage))
{
<img src="@post.FeaturedImage" alt="@post.Title" loading="lazy" />
}
else { <span>📝</span> }
</div>
<div class="post-card-body">
@if (post.Category != null)
{
<span class="post-cat">@post.Category.Name</span>
}
<a href="/blog/@post.Slug" class="post-title">@post.Title</a>
<p class="post-excerpt">@(post.Excerpt.Length > 120 ? post.Excerpt.Substring(0, 120) + "..." : post.Excerpt)</p>
<div class="post-meta">
<span>🕐 @post.ReadingTimeMinutes دقیقه | 👁 @post.ViewCount</span>
<a href="/blog/@post.Slug" class="read-more">ادامه مطلب ←</a>
</div>
</div>
</div>
}
}
</div>
@if (Model.TotalPages > 1)
{
<div class="pagination">
@if (Model.CurrentPage > 1)
{
<a class="page-btn" href="/blog?pg=@(Model.CurrentPage - 1)@(!string.IsNullOrEmpty(Model.ActiveCat) ? "&category=" + Model.ActiveCat : "")"></a>
}
@for (int p = 1; p <= Model.TotalPages; p++)
{
<a class="page-btn @(p == Model.CurrentPage ? "active" : "")"
href="/blog?pg=@p@(!string.IsNullOrEmpty(Model.ActiveCat) ? "&category=" + Model.ActiveCat : "")">@p</a>
}
@if (Model.CurrentPage < Model.TotalPages)
{
<a class="page-btn" href="/blog?pg=@(Model.CurrentPage + 1)@(!string.IsNullOrEmpty(Model.ActiveCat) ? "&category=" + Model.ActiveCat : "")"></a>
}
</div>
}