194 lines
6.7 KiB
C#
194 lines
6.7 KiB
C#
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using PlotLine.Services;
|
|
using PlotLine.ViewModels;
|
|
using System.Net;
|
|
using System.Text.Json;
|
|
|
|
namespace PlotLine.Controllers;
|
|
|
|
[Route("help")]
|
|
[AllowAnonymous]
|
|
public sealed class HelpController(IHelpService help, IHelpMarkdownRenderer markdown, IConfiguration configuration) : Controller
|
|
{
|
|
[HttpGet("")]
|
|
public IActionResult Index(string? q)
|
|
{
|
|
if (!IsCanonicalPath("/help"))
|
|
{
|
|
return RedirectPermanent(PreserveQuery("/help"));
|
|
}
|
|
|
|
var canonicalUrl = BuildAbsoluteUrl("/help");
|
|
ViewData["CanonicalUrl"] = canonicalUrl;
|
|
ViewData["StructuredData"] = new[]
|
|
{
|
|
JsonLd(new Dictionary<string, object?>
|
|
{
|
|
["@context"] = "https://schema.org",
|
|
["@type"] = "CollectionPage",
|
|
["name"] = "Help Centre",
|
|
["url"] = canonicalUrl,
|
|
["description"] = "Find practical guidance for planning, tracking, and shaping your story in PlotDirector."
|
|
}),
|
|
BuildBreadcrumbJsonLd(
|
|
("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}")]
|
|
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 = BuildAbsoluteUrl(canonicalPath);
|
|
ViewData["CanonicalUrl"] = canonicalUrl;
|
|
ViewData["StructuredData"] = new[]
|
|
{
|
|
JsonLd(new Dictionary<string, object?>
|
|
{
|
|
["@context"] = "https://schema.org",
|
|
["@type"] = "CollectionPage",
|
|
["name"] = $"{category} Help",
|
|
["url"] = canonicalUrl,
|
|
["description"] = $"Help articles about {category} in PlotDirector."
|
|
}),
|
|
BuildBreadcrumbJsonLd(
|
|
("Help", BuildAbsoluteUrl("/help")),
|
|
(category, canonicalUrl))
|
|
};
|
|
return View(new HelpCategoryArticlesViewModel
|
|
{
|
|
CategoryName = category,
|
|
CategorySlug = HelpService.Slugify(category),
|
|
Articles = articles
|
|
});
|
|
}
|
|
|
|
[HttpGet("{categorySlug}/{articleSlug}")]
|
|
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 = BuildAbsoluteUrl(article.Url);
|
|
ViewData["CanonicalUrl"] = canonicalUrl;
|
|
ViewData["StructuredData"] = new[]
|
|
{
|
|
JsonLd(new Dictionary<string, object?>
|
|
{
|
|
["@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")
|
|
}),
|
|
BuildBreadcrumbJsonLd(
|
|
("Help", BuildAbsoluteUrl("/help")),
|
|
(article.Category, BuildAbsoluteUrl(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}";
|
|
|
|
private string BuildAbsoluteUrl(string path)
|
|
{
|
|
var baseUrl = configuration["Site:PublicBaseUrl"]?.TrimEnd('/');
|
|
if (string.IsNullOrWhiteSpace(baseUrl))
|
|
{
|
|
baseUrl = "https://plotdirector.com";
|
|
}
|
|
|
|
return $"{baseUrl}{path}";
|
|
}
|
|
|
|
private static string BuildBreadcrumbJsonLd(params (string Name, string Url)[] items) =>
|
|
JsonLd(new Dictionary<string, object?>
|
|
{
|
|
["@context"] = "https://schema.org",
|
|
["@type"] = "BreadcrumbList",
|
|
["itemListElement"] = items.Select((item, index) => new Dictionary<string, object?>
|
|
{
|
|
["@type"] = "ListItem",
|
|
["position"] = index + 1,
|
|
["name"] = item.Name,
|
|
["item"] = item.Url
|
|
})
|
|
});
|
|
|
|
private static string JsonLd(object value) =>
|
|
JsonSerializer.Serialize(value, new JsonSerializerOptions
|
|
{
|
|
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
|
|
});
|
|
}
|