From 18a14bbd0187e2d4590b13e2f1185108ebdcd8e8 Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Thu, 25 Jun 2026 19:46:23 +0100 Subject: [PATCH] Public SEO pass. --- PlotLine/Controllers/HelpController.cs | 112 +++++++----- PlotLine/Controllers/HomeController.cs | 44 +++-- PlotLine/Controllers/PricingController.cs | 45 ++++- PlotLine/Controllers/PublicController.cs | 25 +-- PlotLine/Services/PublicSeo.cs | 211 +++++++++++++++++++++- PlotLine/Views/Shared/_Layout.cshtml | 34 ++-- 6 files changed, 376 insertions(+), 95 deletions(-) diff --git a/PlotLine/Controllers/HelpController.cs b/PlotLine/Controllers/HelpController.cs index ff6512d..8be1f97 100644 --- a/PlotLine/Controllers/HelpController.cs +++ b/PlotLine/Controllers/HelpController.cs @@ -19,20 +19,27 @@ public sealed class HelpController(IHelpService help, IHelpMarkdownRenderer mark } var canonicalUrl = PublicSeo.AbsoluteUrl(configuration, "/help"); - ViewData["CanonicalUrl"] = canonicalUrl; - ViewData["StructuredData"] = new[] + const string description = "Browse PlotDirector help articles covering projects, books, chapters, scenes, timelines, continuity, characters, locations, and writing tools."; + PublicSeo.Apply(ViewData, configuration, new SeoMetadata { - PublicSeo.JsonLd(new Dictionary - { - ["@context"] = "https://schema.org", - ["@type"] = "CollectionPage", - ["name"] = "Help Centre", - ["url"] = canonicalUrl, - ["description"] = "Find practical guidance for planning, tracking, and shaping your story in PlotDirector." - }), - PublicSeo.BreadcrumbJsonLd( - ("Help", canonicalUrl)) - }; + Title = "Help Centre", + Description = description, + Keywords = "PlotDirector help, writing software help, story planning help, novel planning guides", + CanonicalPath = "/help", + JsonLd = + [ + PublicSeo.JsonLd(new Dictionary + { + ["@context"] = "https://schema.org", + ["@type"] = "CollectionPage", + ["name"] = "Help Centre", + ["url"] = canonicalUrl, + ["description"] = description + }), + PublicSeo.BreadcrumbJsonLd( + ("Help", canonicalUrl)) + ] + }); var articles = help.GetAllArticles(); return View(new HelpCentreIndexViewModel { @@ -68,21 +75,28 @@ public sealed class HelpController(IHelpService help, IHelpMarkdownRenderer mark var articles = help.GetByCategorySlug(categorySlug); var canonicalUrl = PublicSeo.AbsoluteUrl(configuration, canonicalPath); - ViewData["CanonicalUrl"] = canonicalUrl; - ViewData["StructuredData"] = new[] + var description = CategoryDescription(category); + PublicSeo.Apply(ViewData, configuration, new SeoMetadata { - PublicSeo.JsonLd(new Dictionary - { - ["@context"] = "https://schema.org", - ["@type"] = "CollectionPage", - ["name"] = $"{category} Help", - ["url"] = canonicalUrl, - ["description"] = $"Help articles about {category} in PlotDirector." - }), - PublicSeo.BreadcrumbJsonLd( - ("Help", PublicSeo.AbsoluteUrl(configuration, "/help")), - (category, canonicalUrl)) - }; + Title = $"{category} Help", + Description = description, + Keywords = PublicSeo.Keywords([category, "PlotDirector help", "novel planning guides"], articles.SelectMany(article => article.Tags)), + CanonicalPath = canonicalPath, + JsonLd = + [ + PublicSeo.JsonLd(new Dictionary + { + ["@context"] = "https://schema.org", + ["@type"] = "CollectionPage", + ["name"] = $"{category} Help", + ["url"] = canonicalUrl, + ["description"] = description + }), + PublicSeo.BreadcrumbJsonLd( + ("Help", PublicSeo.AbsoluteUrl(configuration, "/help")), + (category, canonicalUrl)) + ] + }); return View(new HelpCategoryArticlesViewModel { CategoryName = category, @@ -106,24 +120,32 @@ public sealed class HelpController(IHelpService help, IHelpMarkdownRenderer mark } var canonicalUrl = PublicSeo.AbsoluteUrl(configuration, article.Url); - ViewData["CanonicalUrl"] = canonicalUrl; - ViewData["StructuredData"] = new[] + var description = PublicSeo.HelpArticleDescription(article, help.GetAllArticles()); + PublicSeo.Apply(ViewData, configuration, new SeoMetadata { - PublicSeo.JsonLd(new Dictionary - { - ["@context"] = "https://schema.org", - ["@type"] = "TechArticle", - ["headline"] = article.Title, - ["name"] = article.Title, - ["url"] = canonicalUrl, - ["description"] = string.IsNullOrWhiteSpace(article.QuickSummary) ? null : article.QuickSummary, - ["dateModified"] = article.LastModifiedUtc == default ? null : article.LastModifiedUtc.ToString("O") - }), - PublicSeo.BreadcrumbJsonLd( - ("Help", PublicSeo.AbsoluteUrl(configuration, "/help")), - (article.Category, PublicSeo.AbsoluteUrl(configuration, article.CategoryUrl)), - (article.Title, canonicalUrl)) - }; + Title = article.Title, + Description = description, + Keywords = PublicSeo.Keywords([article.Category, article.Title, "PlotDirector help"], article.Tags), + CanonicalPath = article.Url, + OgType = "article", + JsonLd = + [ + PublicSeo.JsonLd(new Dictionary + { + ["@context"] = "https://schema.org", + ["@type"] = "TechArticle", + ["headline"] = article.Title, + ["name"] = article.Title, + ["url"] = canonicalUrl, + ["description"] = description, + ["dateModified"] = article.LastModifiedUtc == default ? null : article.LastModifiedUtc.ToString("O") + }), + PublicSeo.BreadcrumbJsonLd( + ("Help", PublicSeo.AbsoluteUrl(configuration, "/help")), + (article.Category, PublicSeo.AbsoluteUrl(configuration, article.CategoryUrl)), + (article.Title, canonicalUrl)) + ] + }); return View(new HelpCentreArticleViewModel { Article = article, @@ -159,4 +181,6 @@ public sealed class HelpController(IHelpService help, IHelpMarkdownRenderer mark ? path : $"{path}{HttpContext.Request.QueryString.Value}"; + private static string CategoryDescription(string category) => + $"Find PlotDirector help articles for {category.ToLowerInvariant()}, including practical guidance for organising story details, reviewing continuity, and keeping your novel on track."; } diff --git a/PlotLine/Controllers/HomeController.cs b/PlotLine/Controllers/HomeController.cs index d79c5f5..9b22cbc 100644 --- a/PlotLine/Controllers/HomeController.cs +++ b/PlotLine/Controllers/HomeController.cs @@ -13,23 +13,35 @@ namespace PlotLine.Controllers public IActionResult Index() { const string description = "PlotDirector helps authors organise characters, plot threads, timelines, story assets, relationships, and continuity so they can finish complex novels with confidence."; - var canonicalUrl = PublicSeo.AbsoluteUrl(configuration, "/"); - ViewData["SeoTitle"] = "PlotDirector | Novel Planning Software for Authors"; - ViewData["SeoDescription"] = description; - ViewData["SeoKeywords"] = "novel planning software, writing software, story planning app, author tools, plot tracking, character tracking, story timeline, novel organisation, fiction writing software, series writing software, continuity tracking, plot threads, story bible"; - ViewData["CanonicalUrl"] = canonicalUrl; - ViewData["StructuredData"] = new[] + PublicSeo.Apply(ViewData, configuration, new SeoMetadata { - PublicSeo.JsonLd(new Dictionary - { - ["@context"] = "https://schema.org", - ["@type"] = "WebSite", - ["name"] = "PlotDirector", - ["url"] = PublicSeo.BaseUrl(configuration), - ["description"] = "Novel planning and story organisation software for authors." - }), - PublicSeo.WebPageJsonLd(configuration, "WebPage", "PlotDirector", "/", description) - }; + Title = "Novel Planning Software for Authors", + Description = description, + Keywords = "novel planning software, writing software, story planning app, author tools, plot tracking, character tracking, story timeline, novel organisation, fiction writing software, series writing software, continuity tracking, plot threads, story bible", + CanonicalPath = "/", + JsonLd = + [ + PublicSeo.JsonLd(new Dictionary + { + ["@context"] = "https://schema.org", + ["@type"] = "WebSite", + ["name"] = "PlotDirector", + ["url"] = PublicSeo.BaseUrl(configuration), + ["description"] = "Novel planning and story organisation software for authors." + }), + PublicSeo.JsonLd(new Dictionary + { + ["@context"] = "https://schema.org", + ["@type"] = "SoftwareApplication", + ["name"] = "PlotDirector", + ["applicationCategory"] = "WritingApplication", + ["operatingSystem"] = "Web", + ["url"] = PublicSeo.BaseUrl(configuration), + ["description"] = description + }), + PublicSeo.WebPageJsonLd(configuration, "WebPage", "PlotDirector", "/", description) + ] + }); return View(); } diff --git a/PlotLine/Controllers/PricingController.cs b/PlotLine/Controllers/PricingController.cs index d44361a..5db2b38 100644 --- a/PlotLine/Controllers/PricingController.cs +++ b/PlotLine/Controllers/PricingController.cs @@ -19,14 +19,45 @@ public sealed class PricingController( public async Task Index() { const string description = "Compare PlotDirector plans and choose the right subscription for organising your novel, series, characters, plot threads, timelines, and story details."; - ViewData["SeoTitle"] = "Pricing | PlotDirector"; - ViewData["SeoDescription"] = description; - ViewData["SeoKeywords"] = "novel planning software, writing software, story planning app, author tools, plot tracking, character tracking, story timeline, novel organisation, fiction writing software, series writing software, continuity tracking, plot threads, story bible"; - ViewData["CanonicalUrl"] = PublicSeo.AbsoluteUrl(configuration, "/pricing"); - ViewData["StructuredData"] = new[] + PublicSeo.Apply(ViewData, configuration, new SeoMetadata { - PublicSeo.WebPageJsonLd(configuration, "WebPage", "Pricing", "/pricing", description) - }; + Title = "Pricing", + Description = description, + Keywords = "PlotDirector pricing, novel planning software pricing, writing software plans, author tools subscription", + CanonicalPath = "/pricing", + JsonLd = + [ + PublicSeo.WebPageJsonLd(configuration, "WebPage", "Pricing", "/pricing", description), + PublicSeo.JsonLd(new Dictionary + { + ["@context"] = "https://schema.org", + ["@type"] = "FAQPage", + ["mainEntity"] = new[] + { + new Dictionary + { + ["@type"] = "Question", + ["name"] = "Can I start with a free trial?", + ["acceptedAnswer"] = new Dictionary + { + ["@type"] = "Answer", + ["text"] = "Yes. Creating an account starts a 30-day Draft Desk trial immediately." + } + }, + new Dictionary + { + ["@type"] = "Question", + ["name"] = "Do I need a credit card for the trial?", + ["acceptedAnswer"] = new Dictionary + { + ["@type"] = "Answer", + ["text"] = "No. Payment details are not required for the free trial." + } + } + } + }) + ] + }); UserSubscriptionDetail? currentSubscription = null; if (currentUser.UserId is int userId) diff --git a/PlotLine/Controllers/PublicController.cs b/PlotLine/Controllers/PublicController.cs index 2fa0ddd..175e50f 100644 --- a/PlotLine/Controllers/PublicController.cs +++ b/PlotLine/Controllers/PublicController.cs @@ -17,7 +17,7 @@ public sealed class PublicController(IConfiguration configuration) : Controller public IActionResult About() { SetSeo( - "About PlotDirector | Built for Novelists", + "About", "Learn why PlotDirector was created by a novelist to help writers organise complex stories, manage continuity, and finish their books with confidence.", "/about", "AboutPage", @@ -29,7 +29,7 @@ public sealed class PublicController(IConfiguration configuration) : Controller public IActionResult Contact() { SetSeo( - "Contact PlotDirector", + "Contact", "Contact PlotDirector for support, questions, feedback, or feature suggestions about our novel planning and story organisation platform.", "/contact", "ContactPage", @@ -41,7 +41,7 @@ public sealed class PublicController(IConfiguration configuration) : Controller public IActionResult Privacy() { SetSeo( - "Privacy Policy | PlotDirector", + "Privacy Policy", "Read the PlotDirector Privacy Policy to learn how we protect your account information, subscription data, and story content.", "/privacy", "PrivacyPolicy", @@ -53,7 +53,7 @@ public sealed class PublicController(IConfiguration configuration) : Controller public IActionResult Terms() { SetSeo( - "Terms of Service | PlotDirector", + "Terms of Service", "Read the PlotDirector Terms of Service for information about subscriptions, user content, acceptable use, and account responsibilities.", "/terms", "WebPage", @@ -63,14 +63,17 @@ public sealed class PublicController(IConfiguration configuration) : Controller private void SetSeo(string title, string description, string path, string schemaType, string pageName) { - ViewData["SeoTitle"] = title; - ViewData["SeoDescription"] = description; - ViewData["SeoKeywords"] = Keywords; - ViewData["CanonicalUrl"] = PublicSeo.AbsoluteUrl(configuration, path); - ViewData["StructuredData"] = new[] + PublicSeo.Apply(ViewData, configuration, new SeoMetadata { - PublicSeo.WebPageJsonLd(configuration, schemaType, pageName, path, description) - }; + Title = title, + Description = description, + Keywords = Keywords, + CanonicalPath = path, + JsonLd = + [ + PublicSeo.WebPageJsonLd(configuration, schemaType, pageName, path, description) + ] + }); } private static PublicPageViewModel CreateModel() => new() diff --git a/PlotLine/Services/PublicSeo.cs b/PlotLine/Services/PublicSeo.cs index 1aed44f..2a37fcf 100644 --- a/PlotLine/Services/PublicSeo.cs +++ b/PlotLine/Services/PublicSeo.cs @@ -1,5 +1,8 @@ using System.Text.Json; using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using PlotLine.Models; namespace PlotLine.Services; @@ -16,18 +19,108 @@ public static class PublicRouteNames public const string Terms = "PublicTerms"; } -public static class PublicSeo +public static partial class PublicSeo { public const string DefaultBaseUrl = "https://plotdirector.com"; + public const string DefaultOgImagePath = "/og-image.png"; + private const int DescriptionTargetLength = 155; public static string BaseUrl(IConfiguration configuration) { var baseUrl = configuration["Site:PublicBaseUrl"]?.TrimEnd('/'); - return string.IsNullOrWhiteSpace(baseUrl) ? DefaultBaseUrl : baseUrl; + if (string.IsNullOrWhiteSpace(baseUrl)) + { + return DefaultBaseUrl; + } + + if (!Uri.TryCreate(baseUrl, UriKind.Absolute, out var uri) + || !string.Equals(uri.Host, "plotdirector.com", StringComparison.OrdinalIgnoreCase)) + { + return DefaultBaseUrl; + } + + return DefaultBaseUrl; } public static string AbsoluteUrl(IConfiguration configuration, string path) => - $"{BaseUrl(configuration)}{path}"; + $"{BaseUrl(configuration)}{NormalisePath(path)}"; + + public static string DisplayTitle(string? title) + { + if (string.IsNullOrWhiteSpace(title)) + { + return "PlotDirector"; + } + + var trimmed = title.Trim(); + return string.Equals(trimmed, "PlotDirector", StringComparison.OrdinalIgnoreCase) + || trimmed.EndsWith("| PlotDirector", StringComparison.OrdinalIgnoreCase) + || trimmed.EndsWith("- PlotDirector", StringComparison.OrdinalIgnoreCase) + ? trimmed + : $"{trimmed} | PlotDirector"; + } + + public static void Apply(ViewDataDictionary viewData, IConfiguration configuration, SeoMetadata metadata) + { + var canonicalUrl = string.IsNullOrWhiteSpace(metadata.CanonicalUrl) + ? AbsoluteUrl(configuration, metadata.CanonicalPath ?? "/") + : CanonicalUrl(configuration, metadata.CanonicalUrl); + var ogImage = string.IsNullOrWhiteSpace(metadata.OgImage) + ? AbsoluteUrl(configuration, DefaultOgImagePath) + : CanonicalUrl(configuration, metadata.OgImage); + var fullTitle = DisplayTitle(metadata.Title); + + viewData["Seo"] = metadata with + { + CanonicalUrl = canonicalUrl, + OgImage = ogImage + }; + viewData["SeoTitle"] = fullTitle; + viewData["SeoDescription"] = metadata.Description; + viewData["SeoKeywords"] = metadata.Keywords; + viewData["CanonicalUrl"] = canonicalUrl; + viewData["OgTitle"] = DisplayTitle(metadata.OgTitle ?? metadata.Title); + viewData["OgDescription"] = metadata.OgDescription ?? metadata.Description; + viewData["OgType"] = string.IsNullOrWhiteSpace(metadata.OgType) ? "website" : metadata.OgType; + viewData["OgImage"] = ogImage; + viewData["TwitterCard"] = string.IsNullOrWhiteSpace(metadata.TwitterCard) ? "summary_large_image" : metadata.TwitterCard; + viewData["StructuredData"] = metadata.JsonLd; + } + + public static string HelpArticleDescription(HelpArticle article, IEnumerable allArticles) + { + var description = MetaDescription(article.QuickSummary, article.MarkdownContent); + var duplicates = allArticles + .Where(other => !ReferenceEquals(other, article)) + .Select(other => MetaDescription(other.QuickSummary, other.MarkdownContent)) + .Any(otherDescription => string.Equals(otherDescription, description, StringComparison.OrdinalIgnoreCase)); + + if (!duplicates) + { + return description; + } + + var fallback = $"{article.Title}: {description}"; + return TrimDescription(fallback); + } + + public static string MetaDescription(string? summary, string? markdown) + { + var source = !string.IsNullOrWhiteSpace(summary) + ? summary + : FirstMeaningfulParagraph(markdown); + return TrimDescription(CleanText(source)); + } + + public static string Keywords(params IEnumerable[] groups) + { + return string.Join(", ", groups + .SelectMany(group => group) + .Where(value => !string.IsNullOrWhiteSpace(value)) + .Select(value => value!.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .Take(12)); + } public static string JsonLd(object value) => JsonSerializer.Serialize(value, new JsonSerializerOptions @@ -63,4 +156,116 @@ public static class PublicSeo ["item"] = item.Url }) }); + + private static string CanonicalUrl(IConfiguration configuration, string value) + { + if (!Uri.TryCreate(value, UriKind.Absolute, out var uri)) + { + return AbsoluteUrl(configuration, value); + } + + return $"{BaseUrl(configuration)}{NormalisePath(uri.PathAndQuery)}"; + } + + private static string NormalisePath(string? path) + { + if (string.IsNullOrWhiteSpace(path) || path == "/") + { + return "/"; + } + + return path.StartsWith("/", StringComparison.Ordinal) ? path : $"/{path}"; + } + + private static string FirstMeaningfulParagraph(string? markdown) + { + if (string.IsNullOrWhiteSpace(markdown)) + { + return "Find practical guidance for planning, tracking, and shaping your story in PlotDirector."; + } + + var withoutCode = CodeFenceRegex().Replace(markdown, " "); + foreach (var paragraph in ParagraphSplitRegex().Split(withoutCode)) + { + var cleaned = CleanText(paragraph); + if (cleaned.Length >= 40) + { + return cleaned; + } + } + + return CleanText(withoutCode); + } + + private static string CleanText(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return string.Empty; + } + + var cleaned = HtmlTagRegex().Replace(value, " "); + cleaned = MarkdownImageRegex().Replace(cleaned, "${alt}"); + cleaned = MarkdownLinkRegex().Replace(cleaned, "${text}"); + cleaned = MarkdownSyntaxRegex().Replace(cleaned, " "); + cleaned = HelperSyntaxRegex().Replace(cleaned, " "); + cleaned = WhitespaceRegex().Replace(cleaned, " "); + return System.Net.WebUtility.HtmlDecode(cleaned).Trim(); + } + + private static string TrimDescription(string value) + { + var cleaned = CleanText(value); + if (cleaned.Length <= DescriptionTargetLength) + { + return cleaned; + } + + var cutoff = cleaned.LastIndexOf(' ', DescriptionTargetLength); + if (cutoff < 120) + { + cutoff = DescriptionTargetLength; + } + + return $"{cleaned[..cutoff].TrimEnd('.', ',', ';', ':', ' ')}."; + } + + [GeneratedRegex(@"```[\s\S]*?```")] + private static partial Regex CodeFenceRegex(); + + [GeneratedRegex(@"\r?\n\s*\r?\n+")] + private static partial Regex ParagraphSplitRegex(); + + [GeneratedRegex("<[^>]+>")] + private static partial Regex HtmlTagRegex(); + + [GeneratedRegex(@"!\[(?[^\]]*)\]\([^)]+\)")] + private static partial Regex MarkdownImageRegex(); + + [GeneratedRegex(@"\[(?[^\]]+)\]\([^)]+\)")] + private static partial Regex MarkdownLinkRegex(); + + [GeneratedRegex(@"[#>*_`~\-\|]+")] + private static partial Regex MarkdownSyntaxRegex(); + + [GeneratedRegex(@"]*>|@\w+\([^)]*\)")] + private static partial Regex HelperSyntaxRegex(); + + [GeneratedRegex(@"\s+")] + private static partial Regex WhitespaceRegex(); +} + +public sealed record SeoMetadata +{ + public string Title { get; init; } = string.Empty; + public string Description { get; init; } = string.Empty; + public string? Keywords { get; init; } + public string? CanonicalPath { get; init; } + public string? CanonicalUrl { get; init; } + public string? OgTitle { get; init; } + public string? OgDescription { get; init; } + public string OgType { get; init; } = "website"; + public string? OgImage { get; init; } + public string TwitterCard { get; init; } = "summary_large_image"; + public IReadOnlyList JsonLd { get; init; } = []; } diff --git a/PlotLine/Views/Shared/_Layout.cshtml b/PlotLine/Views/Shared/_Layout.cshtml index 577feb9..2b6f0ea 100644 --- a/PlotLine/Views/Shared/_Layout.cshtml +++ b/PlotLine/Views/Shared/_Layout.cshtml @@ -14,19 +14,25 @@ && adminEmails.Any(email => string.Equals(email, userEmail, StringComparison.OrdinalIgnoreCase)); var featureRequestAttentionCount = userId.HasValue ? await FeatureRequestService.CountAttentionForUserAsync(userId.Value) : 0; var adminFeatureRequestAttentionCount = isAdmin ? await FeatureRequestService.CountWaitingForAdminAsync() : 0; - var defaultTitle = $"{ViewData["Title"]} - PlotDirector"; - var seoTitle = isMarketingPage && ViewData["SeoTitle"] is string seoTitleValue ? seoTitleValue : defaultTitle; - var seoDescription = isMarketingPage && ViewData["SeoDescription"] is string seoDescriptionValue ? seoDescriptionValue : null; + var seoMetadata = ViewData["Seo"] as SeoMetadata; + var defaultTitle = PublicSeo.DisplayTitle(ViewData["Title"] as string); + var seoTitle = ViewData["SeoTitle"] is string seoTitleValue ? seoTitleValue : defaultTitle; + var hasSeoMetadata = seoMetadata is not null || ViewData["SeoDescription"] is string || ViewData["CanonicalUrl"] is string; + var seoDescription = ViewData["SeoDescription"] as string; var canonicalUrl = ViewData["CanonicalUrl"] as string; - var seoKeywords = isMarketingPage && ViewData["SeoKeywords"] is string seoKeywordsValue ? seoKeywordsValue : null; + var seoKeywords = ViewData["SeoKeywords"] as string; var structuredData = ViewData["StructuredData"] is IEnumerable structuredDataValues ? structuredDataValues : Array.Empty(); - const string ogImageUrl = "https://plotdirector.com/og-image.png"; + var ogTitle = ViewData["OgTitle"] as string ?? seoTitle; + var ogDescription = ViewData["OgDescription"] as string ?? seoDescription; + var ogType = ViewData["OgType"] as string ?? "website"; + var ogImageUrl = ViewData["OgImage"] as string ?? "https://plotdirector.com/og-image.png"; + var twitterCard = ViewData["TwitterCard"] as string ?? "summary_large_image"; } @seoTitle - @if (isMarketingPage) + @if (hasSeoMetadata) { @if (!string.IsNullOrWhiteSpace(seoDescription)) { @@ -38,22 +44,22 @@ } - - @if (!string.IsNullOrWhiteSpace(seoDescription)) + + @if (!string.IsNullOrWhiteSpace(ogDescription)) { - + } - + @if (!string.IsNullOrWhiteSpace(canonicalUrl)) { } - - - @if (!string.IsNullOrWhiteSpace(seoDescription)) + + + @if (!string.IsNullOrWhiteSpace(ogDescription)) { - + } }