2026-06-25 19:46:23 +01:00

187 lines
7.3 KiB
C#

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");
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
{
Title = "Help Centre",
Description = description,
Keywords = "PlotDirector help, writing software help, story planning help, novel planning guides",
CanonicalPath = "/help",
JsonLd =
[
PublicSeo.JsonLd(new Dictionary<string, object?>
{
["@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
{
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);
var description = CategoryDescription(category);
PublicSeo.Apply(ViewData, configuration, new SeoMetadata
{
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<string, object?>
{
["@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,
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);
var description = PublicSeo.HelpArticleDescription(article, help.GetAllArticles());
PublicSeo.Apply(ViewData, configuration, new SeoMetadata
{
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<string, object?>
{
["@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,
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 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.";
}