using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Authorization; using PlotLine.Services; using PlotLine.ViewModels; using System.Net; namespace PlotLine.Controllers; [Route("help")] [AllowAnonymous] public sealed class HelpController(IHelpService help, IHelpMarkdownRenderer markdown, IConfiguration configuration) : Controller { [HttpGet("", Name = PublicRouteNames.Help)] public IActionResult Index(string? q) { if (!IsCanonicalPath("/help")) { return RedirectPermanent(PreserveQuery("/help")); } var canonicalUrl = PublicSeo.AbsoluteUrl(configuration, "/help"); ViewData["CanonicalUrl"] = canonicalUrl; ViewData["StructuredData"] = new[] { 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)) }; var articles = help.GetAllArticles(); return View(new HelpCentreIndexViewModel { Query = q, ArticleCount = articles.Count, Categories = help.GetCategories() .Select(category => new HelpCategoryViewModel { CategoryName = category, CategorySlug = HelpService.Slugify(category), ArticleCount = help.GetByCategory(category).Count }) .ToList(), SearchResults = string.IsNullOrWhiteSpace(q) ? [] : help.Search(q), RecentArticles = articles.OrderByDescending(x => x.RelativePath).Take(5).ToList() }); } [HttpGet("{categorySlug}", Name = PublicRouteNames.HelpCategory)] public IActionResult Category(string categorySlug) { var category = help.GetCategoryBySlug(categorySlug); if (category is null) { return NotFound(); } var canonicalPath = $"/help/{HelpService.Slugify(category)}"; if (!IsCanonicalPath(canonicalPath)) { return RedirectPermanent(canonicalPath); } var articles = help.GetByCategorySlug(categorySlug); var canonicalUrl = PublicSeo.AbsoluteUrl(configuration, canonicalPath); ViewData["CanonicalUrl"] = canonicalUrl; ViewData["StructuredData"] = new[] { 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)) }; return View(new HelpCategoryArticlesViewModel { CategoryName = category, CategorySlug = HelpService.Slugify(category), Articles = articles }); } [HttpGet("{categorySlug}/{articleSlug}", Name = PublicRouteNames.HelpArticle)] public IActionResult Article(string categorySlug, string articleSlug) { var article = help.GetBySlugs(categorySlug, articleSlug); if (article is null) { return View("ArticleNotFound", $"{categorySlug}/{articleSlug}"); } if (!IsCanonicalPath(article.Url)) { return RedirectPermanent(article.Url); } var canonicalUrl = PublicSeo.AbsoluteUrl(configuration, article.Url); ViewData["CanonicalUrl"] = canonicalUrl; ViewData["StructuredData"] = new[] { 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)) }; return View(new HelpCentreArticleViewModel { Article = article, RenderedHtml = markdown.Render(article.MarkdownContent), RelatedArticles = help.GetRelatedArticles(article) }); } [HttpGet("/Help/Category/{category}")] public IActionResult LegacyCategory(string category) { var articles = help.GetByCategory(WebUtility.UrlDecode(category)); var firstArticle = articles.FirstOrDefault(); return firstArticle is null ? NotFound() : RedirectPermanent(firstArticle.CategoryUrl); } [HttpGet("/Help/Article/{key}")] public IActionResult LegacyArticle(string key) { var article = help.GetByContextKey(WebUtility.UrlDecode(key)); return article is null ? View("ArticleNotFound", key) : RedirectPermanent(article.Url); } private bool IsCanonicalPath(string path) => string.Equals(HttpContext.Request.Path.Value, path, StringComparison.Ordinal); private string PreserveQuery(string path) => string.IsNullOrWhiteSpace(HttpContext.Request.QueryString.Value) ? path : $"{path}{HttpContext.Request.QueryString.Value}"; }