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>
This commit is contained in:
@@ -1,10 +1,58 @@
|
|||||||
@page "/blog"
|
@page "/blog"
|
||||||
@model DrSousan.Api.Pages.Blog.BlogIndexModel
|
@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 {
|
@section Head {
|
||||||
<title>@ViewData["Title"]</title>
|
<title>@ViewData["Title"]</title>
|
||||||
<meta name="description" content="مقالات تخصصی دکتر سوسن آلطه درباره زیبایی پوست، بوتاکس، فیلر، لیزر و مراقبت از پوست." />
|
<meta name="description" content="@blogDesc" />
|
||||||
<link rel="canonical" href="@((Environment.GetEnvironmentVariable("SITE_BASE_URL")?.TrimEnd('/') ?? (Request.Scheme + "://" + Request.Host)) + "/blog")" />
|
<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>
|
<style>
|
||||||
/* ─── Blog Hero ─────────────────────────────────────────────── */
|
/* ─── Blog Hero ─────────────────────────────────────────────── */
|
||||||
.blog-hero{background:linear-gradient(135deg,var(--gold-pale) 0%,#EDE0CA 100%);padding:6rem 2rem 3rem;text-align:center}
|
.blog-hero{background:linear-gradient(135deg,var(--gold-pale) 0%,#EDE0CA 100%);padding:6rem 2rem 3rem;text-align:center}
|
||||||
|
|||||||
@@ -18,17 +18,28 @@
|
|||||||
@if (!string.IsNullOrEmpty(ViewData["Keywords"]?.ToString())) {
|
@if (!string.IsNullOrEmpty(ViewData["Keywords"]?.ToString())) {
|
||||||
<meta name="keywords" content="@ViewData["Keywords"]" />
|
<meta name="keywords" content="@ViewData["Keywords"]" />
|
||||||
}
|
}
|
||||||
|
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1" />
|
||||||
|
<meta name="author" content="@post.Author" />
|
||||||
<meta property="og:type" content="article" />
|
<meta property="og:type" content="article" />
|
||||||
|
<meta property="og:site_name" content="@ViewData["SiteName"]" />
|
||||||
<meta property="og:title" content="@ViewData["Title"]" />
|
<meta property="og:title" content="@ViewData["Title"]" />
|
||||||
<meta property="og:description" content="@ViewData["MetaDesc"]" />
|
<meta property="og:description" content="@ViewData["MetaDesc"]" />
|
||||||
<meta property="og:url" content="@canonicalUrl" />
|
<meta property="og:url" content="@canonicalUrl" />
|
||||||
<meta property="og:locale" content="fa_IR" />
|
<meta property="og:locale" content="fa_IR" />
|
||||||
|
@if (!string.IsNullOrEmpty(pubDate)) { <meta property="article:published_time" content="@pubDate" /> }
|
||||||
|
<meta property="article:modified_time" content="@updDate" />
|
||||||
|
<meta property="article:author" content="@post.Author" />
|
||||||
|
@if (post.Category != null) { <meta property="article:section" content="@post.Category.Name" /> }
|
||||||
@if (!string.IsNullOrEmpty(ogImage)) {
|
@if (!string.IsNullOrEmpty(ogImage)) {
|
||||||
<meta property="og:image" content="@(ogImage.StartsWith("http") ? ogImage : baseUrl + ogImage)" />
|
<meta property="og:image" content="@(ogImage.StartsWith("http") ? ogImage : baseUrl + ogImage)" />
|
||||||
|
<meta property="og:image:alt" content="@post.Title" />
|
||||||
}
|
}
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<meta name="twitter:title" content="@ViewData["Title"]" />
|
<meta name="twitter:title" content="@ViewData["Title"]" />
|
||||||
<meta name="twitter:description" content="@ViewData["MetaDesc"]" />
|
<meta name="twitter:description" content="@ViewData["MetaDesc"]" />
|
||||||
|
@if (!string.IsNullOrEmpty(ogImage)) {
|
||||||
|
<meta name="twitter:image" content="@(ogImage.StartsWith("http") ? ogImage : baseUrl + ogImage)" />
|
||||||
|
}
|
||||||
<link rel="canonical" href="@canonicalUrl" />
|
<link rel="canonical" href="@canonicalUrl" />
|
||||||
|
|
||||||
<script type="application/ld+json">
|
<script type="application/ld+json">
|
||||||
@@ -52,6 +63,24 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@@context": "https://schema.org",
|
||||||
|
"@@type": "BreadcrumbList",
|
||||||
|
"itemListElement": [
|
||||||
|
{ "@@type": "ListItem", "position": 1, "name": "خانه", "item": "@baseUrl/" },
|
||||||
|
{ "@@type": "ListItem", "position": 2, "name": "وبلاگ", "item": "@baseUrl/blog" }@(post.Category != null ? "," : "")
|
||||||
|
@if (post.Category != null) {
|
||||||
|
@:{ "@@type": "ListItem", "position": 3, "name": "@J(post.Category.Name)", "item": "@baseUrl/blog?category=@post.Category.Slug" },
|
||||||
|
@:{ "@@type": "ListItem", "position": 4, "name": "@J(post.Title)", "item": "@canonicalUrl" }
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
@:{ "@@type": "ListItem", "position": 3, "name": "@J(post.Title)", "item": "@canonicalUrl" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* ─── Post Layout ──────────────────────────────────────────────── */
|
/* ─── Post Layout ──────────────────────────────────────────────── */
|
||||||
.post-layout{max-width:1100px;margin:0 auto;padding:5rem 2rem 3rem;display:grid;grid-template-columns:1fr 320px;gap:3rem;align-items:start}
|
.post-layout{max-width:1100px;margin:0 auto;padding:5rem 2rem 3rem;display:grid;grid-template-columns:1fr 320px;gap:3rem;align-items:start}
|
||||||
|
|||||||
@@ -1,10 +1,62 @@
|
|||||||
@page "/gallery"
|
@page "/gallery"
|
||||||
@model DrSousan.Api.Pages.GalleryModel
|
@model DrSousan.Api.Pages.GalleryModel
|
||||||
|
@{
|
||||||
|
var galBase = Environment.GetEnvironmentVariable("SITE_BASE_URL")?.TrimEnd('/') ?? (Request.Scheme + "://" + Request.Host);
|
||||||
|
var galUrl = galBase + "/gallery";
|
||||||
|
var galDesc = "گالری نتایج واقعی قبل و بعد درمانهای زیبایی پوست دکتر سوسن آلطه — بوتاکس، فیلر، لیزر، مزوتراپی و پاکسازی پوست.";
|
||||||
|
var firstImg = Model.Items
|
||||||
|
.Select(i => !string.IsNullOrEmpty(i.BeforeImageUrl) ? i.BeforeImageUrl : i.ImageUrl)
|
||||||
|
.FirstOrDefault(u => !string.IsNullOrEmpty(u)) ?? "";
|
||||||
|
var absFirstImg = string.IsNullOrEmpty(firstImg) ? "" : (firstImg.StartsWith("http") ? firstImg : galBase + firstImg);
|
||||||
|
}
|
||||||
|
|
||||||
@section Head {
|
@section Head {
|
||||||
<title>@ViewData["Title"]</title>
|
<title>@ViewData["Title"]</title>
|
||||||
<meta name="description" content="گالری نتایج واقعی قبل و بعد درمانهای زیبایی پوست دکتر سوسن آلطه — بوتاکس، فیلر، لیزر، مزوتراپی و پاکسازی پوست." />
|
<meta name="description" content="@galDesc" />
|
||||||
<link rel="canonical" href="@((Environment.GetEnvironmentVariable("SITE_BASE_URL")?.TrimEnd('/') ?? (Request.Scheme + "://" + Request.Host)) + "/gallery")" />
|
<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="@galUrl" />
|
||||||
|
|
||||||
|
<!-- 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="@galDesc" />
|
||||||
|
<meta property="og:url" content="@galUrl" />
|
||||||
|
<meta property="og:locale" content="fa_IR" />
|
||||||
|
@if (!string.IsNullOrEmpty(absFirstImg)) {
|
||||||
|
<meta property="og:image" content="@absFirstImg" />
|
||||||
|
}
|
||||||
|
<!-- Twitter -->
|
||||||
|
<meta name="twitter:card" content="@(string.IsNullOrEmpty(absFirstImg) ? "summary" : "summary_large_image")" />
|
||||||
|
<meta name="twitter:title" content="@ViewData["Title"]" />
|
||||||
|
<meta name="twitter:description" content="@galDesc" />
|
||||||
|
@if (!string.IsNullOrEmpty(absFirstImg)) {
|
||||||
|
<meta name="twitter:image" content="@absFirstImg" />
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Structured data: ImageGallery + breadcrumb -->
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@@context": "https://schema.org",
|
||||||
|
"@@type": "ImageGallery",
|
||||||
|
"name": "@ViewData["Title"]",
|
||||||
|
"description": "@galDesc",
|
||||||
|
"url": "@galUrl",
|
||||||
|
"inLanguage": "fa-IR"
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@@context": "https://schema.org",
|
||||||
|
"@@type": "BreadcrumbList",
|
||||||
|
"itemListElement": [
|
||||||
|
{ "@@type": "ListItem", "position": 1, "name": "خانه", "item": "@galBase/" },
|
||||||
|
{ "@@type": "ListItem", "position": 2, "name": "گالری", "item": "@galUrl" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<style>
|
<style>
|
||||||
.gal-hero{background:linear-gradient(135deg,var(--gold-pale) 0%,#EDE0CA 100%);padding:6rem 2rem 3rem;text-align:center}
|
.gal-hero{background:linear-gradient(135deg,var(--gold-pale) 0%,#EDE0CA 100%);padding:6rem 2rem 3rem;text-align:center}
|
||||||
.gal-hero h1{font-size:clamp(1.8rem,4vw,2.5rem);font-weight:700;color:var(--dark);margin-bottom:.6rem}
|
.gal-hero h1{font-size:clamp(1.8rem,4vw,2.5rem);font-weight:700;color:var(--dark);margin-bottom:.6rem}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using System.IdentityModel.Tokens.Jwt;
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
using System.Text.Unicode;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
@@ -49,6 +51,10 @@ builder.Services.AddCors(opt =>
|
|||||||
|
|
||||||
// Razor Pages for SSR public pages
|
// Razor Pages for SSR public pages
|
||||||
builder.Services.AddRazorPages();
|
builder.Services.AddRazorPages();
|
||||||
|
// Don't entity-encode non-ASCII (Persian) or chars like '+' in markup output.
|
||||||
|
// Default encoder turns "application/ld+json" into "application/ld+json" and
|
||||||
|
// Persian text into \XX; entities — valid but bloated and trips some validators.
|
||||||
|
builder.Services.AddSingleton(HtmlEncoder.Create(UnicodeRanges.All));
|
||||||
|
|
||||||
// Fix circular JSON references (BlogPost ↔ BlogCategory)
|
// Fix circular JSON references (BlogPost ↔ BlogCategory)
|
||||||
builder.Services.ConfigureHttpJsonOptions(opts =>
|
builder.Services.ConfigureHttpJsonOptions(opts =>
|
||||||
|
|||||||
Reference in New Issue
Block a user