Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
This commit is contained in:
@@ -30,6 +30,7 @@ using CMSMicroservice.Protobuf.Protos.DiscountCategory;
|
||||
using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart;
|
||||
using CMSMicroservice.Protobuf.Protos.DiscountOrder;
|
||||
using FrontOffice.Main.Utilities;
|
||||
using FrontOffice.Main.Utilities.Seo;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@@ -95,6 +96,10 @@ public static class ConfigureServices
|
||||
// SignalR Token Notification Service
|
||||
services.AddScoped<TokenNotificationService>();
|
||||
|
||||
// SEO
|
||||
services.AddScoped<SeoMetadataProvider>();
|
||||
services.AddScoped<SitemapGenerator>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@page "/"
|
||||
@model FrontOffice.Main.Pages.HostModel
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@namespace FrontOffice.Main.Pages
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@@ -8,6 +9,26 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>@Model.Seo.Title</title>
|
||||
<meta name="description" content="@Model.Seo.Description" />
|
||||
<link rel="canonical" href="@Model.Seo.CanonicalUrl" />
|
||||
@if (Model.Seo.NoIndex)
|
||||
{
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<meta name="robots" content="index, follow" />
|
||||
}
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:locale" content="fa_IR" />
|
||||
<meta property="og:title" content="@Model.Seo.Title" />
|
||||
<meta property="og:description" content="@Model.Seo.Description" />
|
||||
<meta property="og:url" content="@Model.Seo.CanonicalUrl" />
|
||||
<meta property="og:site_name" content="کارابازار سلامت" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:title" content="@Model.Seo.Title" />
|
||||
<meta name="twitter:description" content="@Model.Seo.Description" />
|
||||
<meta name="theme-color" content="#7c4dff" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
@@ -23,8 +44,26 @@
|
||||
<link href="FrontOffice.Main.styles.css" rel="stylesheet" />
|
||||
<!-- d3-org-chart custom styles -->
|
||||
<link href="css/org-chart.css" rel="stylesheet" asp-append-version="true" />
|
||||
<style>
|
||||
.seo-crawlable {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@if (!string.IsNullOrWhiteSpace(Model.Seo.CrawlableContent))
|
||||
{
|
||||
<div class="seo-crawlable">@Model.Seo.CrawlableContent</div>
|
||||
}
|
||||
|
||||
<component type="typeof(App)" render-mode="Server" />
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using FrontOffice.Main.Utilities.Seo;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace FrontOffice.Main.Pages;
|
||||
|
||||
public class HostModel : PageModel
|
||||
{
|
||||
private readonly SeoMetadataProvider _seoMetadataProvider;
|
||||
|
||||
public SeoPageMetadata Seo { get; private set; } = default!;
|
||||
|
||||
public HostModel(SeoMetadataProvider seoMetadataProvider)
|
||||
{
|
||||
_seoMetadataProvider = seoMetadataProvider;
|
||||
}
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
Seo = await _seoMetadataProvider.GetForPathAsync(Request.Path);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using FluentValidation;
|
||||
using FrontOffice.Main.Utilities;
|
||||
using FrontOffice.Main.Utilities.Seo;
|
||||
using System.Net;
|
||||
using FrontOffice.Main.Utilities.Pdf;
|
||||
|
||||
@@ -12,6 +13,7 @@ builder.Services.AddServerSideBlazor();
|
||||
#region AddCommonServices
|
||||
|
||||
builder.Services.AddCommonServices();
|
||||
builder.Services.Configure<SeoSettings>(builder.Configuration.GetSection(SeoSettings.SectionName));
|
||||
builder.Services.AddSingleton<MainService>();
|
||||
|
||||
#endregion
|
||||
@@ -55,6 +57,13 @@ webApp.UseStaticFiles();
|
||||
webApp.UseRouting();
|
||||
|
||||
webApp.MapBlazorHub();
|
||||
|
||||
webApp.MapGet("/sitemap.xml", async (SitemapGenerator sitemapGenerator, CancellationToken cancellationToken) =>
|
||||
{
|
||||
var xml = await sitemapGenerator.GenerateAsync(cancellationToken);
|
||||
return Results.Content(xml, "application/xml; charset=utf-8");
|
||||
});
|
||||
|
||||
webApp.MapFallbackToPage("/_Host");
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace FrontOffice.Main.Utilities.Seo;
|
||||
|
||||
public class SeoMetadataProvider
|
||||
{
|
||||
private readonly BlogPostService _blogPostService;
|
||||
private readonly SeoSettings _settings;
|
||||
|
||||
private static readonly Dictionary<string, (string Title, string Description)> StaticPages = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["/"] = (
|
||||
"کارابازار سلامت | باشگاه مشتریان KBS",
|
||||
"باشگاه مشتریان کارابازار سلامت — رشد تیم، فروش واقعی و پاداش شفاف. فروشگاه محصولات سلامت، کیف پول و ثبتنام رایگان."),
|
||||
[RouteConstants.About.Index] = (
|
||||
"درباره ما | کارابازار سلامت",
|
||||
"آشنایی با کارابازار سلامت، باشگاه مشتریان KBS و خدمات فروشگاه محصولات سلامت."),
|
||||
[RouteConstants.FAQ.Index] = (
|
||||
"سوالات متداول | کارابازار سلامت",
|
||||
"پاسخ سوالات متداول درباره باشگاه مشتریان، کیف پول، پاداشها و خرید از کارابازار سلامت."),
|
||||
[RouteConstants.Contact.Index] = (
|
||||
"ارتباط با ما | کارابازار سلامت",
|
||||
"تماس با تیم پشتیبانی کارابازار سلامت — سوالات، پیشنهادات و پشتیبانی کاربران."),
|
||||
[RouteConstants.Licenses.Index] = (
|
||||
"مجوزها و گواهینامهها | کارابازار سلامت",
|
||||
"مجوزها، گواهینامهها و مدارک رسمی کارابازار سلامت."),
|
||||
[RouteConstants.Blog.Index] = (
|
||||
"بلاگ | کارابازار سلامت",
|
||||
"آخرین مقالات و اخبار باشگاه مشتریان کارابازار سلامت."),
|
||||
[RouteConstants.Store.Products] = (
|
||||
"محصولات | کارابازار سلامت",
|
||||
"فروشگاه محصولات سلامت کارابازار — خرید آنلاین با پاداش باشگاه مشتریان."),
|
||||
[RouteConstants.Package.List] = (
|
||||
"پکیجها | کارابازار سلامت",
|
||||
"پکیجهای عضویت باشگاه مشتریان کارابازار سلامت."),
|
||||
[RouteConstants.Club.Membership] = (
|
||||
"عضویت باشگاه مشتریان | کارابازار سلامت",
|
||||
"عضویت در باشگاه مشتریان KBS و بهرهمندی از مزایا و پاداشها."),
|
||||
[RouteConstants.Club.Features] = (
|
||||
"ویژگیهای باشگاه مشتریان | کارابازار سلامت",
|
||||
"امکانات و ویژگیهای باشگاه مشتریان کارابازار سلامت."),
|
||||
[RouteConstants.DiscountStore.Products] = (
|
||||
"فروشگاه اعتباری | کارابازار سلامت",
|
||||
"خرید از فروشگاه اعتباری کارابازار سلامت با کیف پول تخفیفی."),
|
||||
[RouteConstants.Registration.Wizard] = (
|
||||
"ثبتنام | کارابازار سلامت",
|
||||
"ثبتنام سریع در باشگاه مشتریان کارابازار سلامت."),
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> NoIndexPrefixes = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
RouteConstants.Profile.Index,
|
||||
"/profile/",
|
||||
RouteConstants.Checkout.Index,
|
||||
RouteConstants.Store.Cart,
|
||||
RouteConstants.Store.CheckoutSummary,
|
||||
RouteConstants.Store.Orders,
|
||||
"/order/",
|
||||
RouteConstants.DiscountStore.Cart,
|
||||
RouteConstants.DiscountStore.Checkout,
|
||||
RouteConstants.DiscountStore.Orders,
|
||||
"/discount-store/order/",
|
||||
RouteConstants.Gateway.OrdersChooser,
|
||||
RouteConstants.Gateway.CartChooser,
|
||||
};
|
||||
|
||||
public SeoMetadataProvider(BlogPostService blogPostService, IOptions<SeoSettings> settings)
|
||||
{
|
||||
_blogPostService = blogPostService;
|
||||
_settings = settings.Value;
|
||||
}
|
||||
|
||||
public async Task<SeoPageMetadata> GetForPathAsync(string path, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var normalizedPath = NormalizePath(path);
|
||||
var baseUrl = _settings.SiteUrl.TrimEnd('/');
|
||||
|
||||
if (IsNoIndexPath(normalizedPath))
|
||||
{
|
||||
return new SeoPageMetadata
|
||||
{
|
||||
Title = _settings.SiteName,
|
||||
Description = _settings.DefaultDescription,
|
||||
CanonicalUrl = $"{baseUrl}{normalizedPath}",
|
||||
NoIndex = true
|
||||
};
|
||||
}
|
||||
|
||||
if (normalizedPath.StartsWith(RouteConstants.Blog.Post, StringComparison.OrdinalIgnoreCase)
|
||||
&& normalizedPath.Length > RouteConstants.Blog.Post.Length)
|
||||
{
|
||||
var slug = normalizedPath[RouteConstants.Blog.Post.Length..].Trim('/');
|
||||
if (!string.IsNullOrWhiteSpace(slug))
|
||||
{
|
||||
var post = await _blogPostService.GetBySlugAsync(slug);
|
||||
if (post != null)
|
||||
{
|
||||
var description = !string.IsNullOrWhiteSpace(post.Summary)
|
||||
? post.Summary
|
||||
: $"مقاله {post.Title} — بلاگ کارابازار سلامت";
|
||||
|
||||
return new SeoPageMetadata
|
||||
{
|
||||
Title = $"{post.Title} | بلاگ کارابازار سلامت",
|
||||
Description = description,
|
||||
CanonicalUrl = $"{baseUrl}{RouteConstants.Blog.Post}{post.Slug}",
|
||||
CrawlableContent = $"{post.Title}. {description}"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (StaticPages.TryGetValue(normalizedPath, out var page))
|
||||
{
|
||||
return new SeoPageMetadata
|
||||
{
|
||||
Title = page.Title,
|
||||
Description = page.Description,
|
||||
CanonicalUrl = $"{baseUrl}{normalizedPath}",
|
||||
CrawlableContent = normalizedPath == "/" ? GetHomeCrawlableContent() : null
|
||||
};
|
||||
}
|
||||
|
||||
return new SeoPageMetadata
|
||||
{
|
||||
Title = _settings.SiteName,
|
||||
Description = _settings.DefaultDescription,
|
||||
CanonicalUrl = $"{baseUrl}{normalizedPath}"
|
||||
};
|
||||
}
|
||||
|
||||
private static string NormalizePath(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
return "/";
|
||||
|
||||
var normalized = path.Split('?', '#')[0].Trim();
|
||||
if (!normalized.StartsWith('/'))
|
||||
normalized = "/" + normalized;
|
||||
|
||||
if (normalized.Length > 1 && normalized.EndsWith('/'))
|
||||
normalized = normalized.TrimEnd('/');
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static bool IsNoIndexPath(string path)
|
||||
{
|
||||
foreach (var prefix in NoIndexPrefixes)
|
||||
{
|
||||
if (path.Equals(prefix, StringComparison.OrdinalIgnoreCase)
|
||||
|| path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string GetHomeCrawlableContent() =>
|
||||
"کارابازار سلامت (KBS) — باشگاه مشتریان و فروشگاه محصولات سلامت. " +
|
||||
"با عضویت در باشگاه مشتریان KBS از پاداش شفاف، کیف پول جادویی، فروشگاه اعتباری و شبکه فروش بهرهمند شوید. " +
|
||||
"ثبتنام رایگان در kbs2.ir.";
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace FrontOffice.Main.Utilities.Seo;
|
||||
|
||||
public sealed class SeoPageMetadata
|
||||
{
|
||||
public required string Title { get; init; }
|
||||
public required string Description { get; init; }
|
||||
public required string CanonicalUrl { get; init; }
|
||||
public string? CrawlableContent { get; init; }
|
||||
public bool NoIndex { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace FrontOffice.Main.Utilities.Seo;
|
||||
|
||||
public class SeoSettings
|
||||
{
|
||||
public const string SectionName = "Seo";
|
||||
|
||||
public string SiteUrl { get; set; } = "https://kbs2.ir";
|
||||
public string SiteName { get; set; } = "کارابازار سلامت";
|
||||
public string DefaultDescription { get; set; } =
|
||||
"باشگاه مشتریان کارابازار سلامت (KBS) — فروشگاه محصولات سلامت، پاداش شفاف، کیف پول و شبکه فروش.";
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using System.Text;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace FrontOffice.Main.Utilities.Seo;
|
||||
|
||||
public class SitemapGenerator
|
||||
{
|
||||
private readonly BlogPostService _blogPostService;
|
||||
private readonly SeoSettings _settings;
|
||||
|
||||
private static readonly string[] StaticPaths =
|
||||
[
|
||||
RouteConstants.Main.MainPage,
|
||||
RouteConstants.About.Index,
|
||||
RouteConstants.FAQ.Index,
|
||||
RouteConstants.Contact.Index,
|
||||
RouteConstants.Licenses.Index,
|
||||
RouteConstants.Blog.Index,
|
||||
RouteConstants.Store.Products,
|
||||
RouteConstants.Package.List,
|
||||
RouteConstants.Club.Membership,
|
||||
RouteConstants.Club.Features,
|
||||
RouteConstants.DiscountStore.Products,
|
||||
RouteConstants.Registration.Wizard,
|
||||
];
|
||||
|
||||
public SitemapGenerator(BlogPostService blogPostService, IOptions<SeoSettings> settings)
|
||||
{
|
||||
_blogPostService = blogPostService;
|
||||
_settings = settings.Value;
|
||||
}
|
||||
|
||||
public async Task<string> GenerateAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var baseUrl = _settings.SiteUrl.TrimEnd('/');
|
||||
var ns = XNamespace.Get("http://www.sitemaps.org/schemas/sitemap/0.9");
|
||||
var urls = new List<XElement>();
|
||||
|
||||
foreach (var path in StaticPaths)
|
||||
{
|
||||
urls.Add(CreateUrlElement(ns, $"{baseUrl}{path}", priority: path == "/" ? "1.0" : "0.8"));
|
||||
}
|
||||
|
||||
var page = 1;
|
||||
const int pageSize = 100;
|
||||
|
||||
while (true)
|
||||
{
|
||||
var result = await _blogPostService.GetPublishedPostsAsync(page: page, pageSize: pageSize);
|
||||
if (result.Posts.Count == 0)
|
||||
break;
|
||||
|
||||
foreach (var post in result.Posts)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(post.Slug))
|
||||
continue;
|
||||
|
||||
var lastMod = post.PublishedAt?.ToString("yyyy-MM-dd") ?? DateTime.UtcNow.ToString("yyyy-MM-dd");
|
||||
urls.Add(CreateUrlElement(
|
||||
ns,
|
||||
$"{baseUrl}{RouteConstants.Blog.Post}{post.Slug}",
|
||||
lastMod: lastMod,
|
||||
priority: "0.6"));
|
||||
}
|
||||
|
||||
if (page >= result.TotalPages)
|
||||
break;
|
||||
|
||||
page++;
|
||||
}
|
||||
|
||||
var document = new XDocument(
|
||||
new XDeclaration("1.0", "UTF-8", null),
|
||||
new XElement(ns + "urlset", urls));
|
||||
|
||||
var builder = new StringBuilder();
|
||||
using (var writer = new StringWriter(builder))
|
||||
{
|
||||
document.Save(writer);
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static XElement CreateUrlElement(
|
||||
XNamespace ns,
|
||||
string loc,
|
||||
string? lastMod = null,
|
||||
string changefreq = "weekly",
|
||||
string priority = "0.5")
|
||||
{
|
||||
var element = new XElement(ns + "url",
|
||||
new XElement(ns + "loc", loc),
|
||||
new XElement(ns + "changefreq", changefreq),
|
||||
new XElement(ns + "priority", priority));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(lastMod))
|
||||
element.Add(new XElement(ns + "lastmod", lastMod));
|
||||
|
||||
return element;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"Seo": {
|
||||
"SiteUrl": "https://kbs1.ir",
|
||||
"SiteName": "کارابازار سلامت",
|
||||
"DefaultDescription": "باشگاه مشتریان کارابازار سلامت (KBS) — فروشگاه محصولات سلامت، پاداش شفاف، کیف پول و شبکه فروش."
|
||||
},
|
||||
"GwUrl": "https://cms.se.kbs1.ir",
|
||||
"DownloadUrl": "",
|
||||
"EncryptionSettings": {
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"Seo": {
|
||||
"SiteUrl": "http://localhost:5268",
|
||||
"SiteName": "کارابازار سلامت",
|
||||
"DefaultDescription": "باشگاه مشتریان کارابازار سلامت (KBS) — فروشگاه محصولات سلامت، پاداش شفاف، کیف پول و شبکه فروش."
|
||||
},
|
||||
"GwUrl": "https://cms.se.kbs1.ir",
|
||||
"DownloadUrl": "",
|
||||
"EncryptionSettings": {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Disallow: /profile/
|
||||
Disallow: /checkout
|
||||
Disallow: /checkout-summary
|
||||
Disallow: /cart
|
||||
Disallow: /orders
|
||||
Disallow: /order/
|
||||
Disallow: /discount-store/cart
|
||||
Disallow: /discount-store/checkout
|
||||
Disallow: /discount-store/orders
|
||||
Disallow: /discount-store/order/
|
||||
Disallow: /my-orders
|
||||
Disallow: /my-cart
|
||||
|
||||
Sitemap: https://kbs2.ir/sitemap.xml
|
||||
Reference in New Issue
Block a user