mplemented the public navigation and SEO coverage pass.

This commit is contained in:
Nick Beckley 2026-06-24 10:57:18 +01:00
parent bd5bf07906
commit 19d9bc4bb1
14 changed files with 192 additions and 166 deletions

View File

@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Authorization;
using PlotLine.Services; using PlotLine.Services;
using PlotLine.ViewModels; using PlotLine.ViewModels;
using System.Net; using System.Net;
using System.Text.Json;
namespace PlotLine.Controllers; namespace PlotLine.Controllers;
@ -11,7 +10,7 @@ namespace PlotLine.Controllers;
[AllowAnonymous] [AllowAnonymous]
public sealed class HelpController(IHelpService help, IHelpMarkdownRenderer markdown, IConfiguration configuration) : Controller public sealed class HelpController(IHelpService help, IHelpMarkdownRenderer markdown, IConfiguration configuration) : Controller
{ {
[HttpGet("")] [HttpGet("", Name = PublicRouteNames.Help)]
public IActionResult Index(string? q) public IActionResult Index(string? q)
{ {
if (!IsCanonicalPath("/help")) if (!IsCanonicalPath("/help"))
@ -19,11 +18,11 @@ public sealed class HelpController(IHelpService help, IHelpMarkdownRenderer mark
return RedirectPermanent(PreserveQuery("/help")); return RedirectPermanent(PreserveQuery("/help"));
} }
var canonicalUrl = BuildAbsoluteUrl("/help"); var canonicalUrl = PublicSeo.AbsoluteUrl(configuration, "/help");
ViewData["CanonicalUrl"] = canonicalUrl; ViewData["CanonicalUrl"] = canonicalUrl;
ViewData["StructuredData"] = new[] ViewData["StructuredData"] = new[]
{ {
JsonLd(new Dictionary<string, object?> PublicSeo.JsonLd(new Dictionary<string, object?>
{ {
["@context"] = "https://schema.org", ["@context"] = "https://schema.org",
["@type"] = "CollectionPage", ["@type"] = "CollectionPage",
@ -31,7 +30,7 @@ public sealed class HelpController(IHelpService help, IHelpMarkdownRenderer mark
["url"] = canonicalUrl, ["url"] = canonicalUrl,
["description"] = "Find practical guidance for planning, tracking, and shaping your story in PlotDirector." ["description"] = "Find practical guidance for planning, tracking, and shaping your story in PlotDirector."
}), }),
BuildBreadcrumbJsonLd( PublicSeo.BreadcrumbJsonLd(
("Help", canonicalUrl)) ("Help", canonicalUrl))
}; };
var articles = help.GetAllArticles(); var articles = help.GetAllArticles();
@ -52,7 +51,7 @@ public sealed class HelpController(IHelpService help, IHelpMarkdownRenderer mark
}); });
} }
[HttpGet("{categorySlug}")] [HttpGet("{categorySlug}", Name = PublicRouteNames.HelpCategory)]
public IActionResult Category(string categorySlug) public IActionResult Category(string categorySlug)
{ {
var category = help.GetCategoryBySlug(categorySlug); var category = help.GetCategoryBySlug(categorySlug);
@ -68,11 +67,11 @@ public sealed class HelpController(IHelpService help, IHelpMarkdownRenderer mark
} }
var articles = help.GetByCategorySlug(categorySlug); var articles = help.GetByCategorySlug(categorySlug);
var canonicalUrl = BuildAbsoluteUrl(canonicalPath); var canonicalUrl = PublicSeo.AbsoluteUrl(configuration, canonicalPath);
ViewData["CanonicalUrl"] = canonicalUrl; ViewData["CanonicalUrl"] = canonicalUrl;
ViewData["StructuredData"] = new[] ViewData["StructuredData"] = new[]
{ {
JsonLd(new Dictionary<string, object?> PublicSeo.JsonLd(new Dictionary<string, object?>
{ {
["@context"] = "https://schema.org", ["@context"] = "https://schema.org",
["@type"] = "CollectionPage", ["@type"] = "CollectionPage",
@ -80,8 +79,8 @@ public sealed class HelpController(IHelpService help, IHelpMarkdownRenderer mark
["url"] = canonicalUrl, ["url"] = canonicalUrl,
["description"] = $"Help articles about {category} in PlotDirector." ["description"] = $"Help articles about {category} in PlotDirector."
}), }),
BuildBreadcrumbJsonLd( PublicSeo.BreadcrumbJsonLd(
("Help", BuildAbsoluteUrl("/help")), ("Help", PublicSeo.AbsoluteUrl(configuration, "/help")),
(category, canonicalUrl)) (category, canonicalUrl))
}; };
return View(new HelpCategoryArticlesViewModel return View(new HelpCategoryArticlesViewModel
@ -92,7 +91,7 @@ public sealed class HelpController(IHelpService help, IHelpMarkdownRenderer mark
}); });
} }
[HttpGet("{categorySlug}/{articleSlug}")] [HttpGet("{categorySlug}/{articleSlug}", Name = PublicRouteNames.HelpArticle)]
public IActionResult Article(string categorySlug, string articleSlug) public IActionResult Article(string categorySlug, string articleSlug)
{ {
var article = help.GetBySlugs(categorySlug, articleSlug); var article = help.GetBySlugs(categorySlug, articleSlug);
@ -106,11 +105,11 @@ public sealed class HelpController(IHelpService help, IHelpMarkdownRenderer mark
return RedirectPermanent(article.Url); return RedirectPermanent(article.Url);
} }
var canonicalUrl = BuildAbsoluteUrl(article.Url); var canonicalUrl = PublicSeo.AbsoluteUrl(configuration, article.Url);
ViewData["CanonicalUrl"] = canonicalUrl; ViewData["CanonicalUrl"] = canonicalUrl;
ViewData["StructuredData"] = new[] ViewData["StructuredData"] = new[]
{ {
JsonLd(new Dictionary<string, object?> PublicSeo.JsonLd(new Dictionary<string, object?>
{ {
["@context"] = "https://schema.org", ["@context"] = "https://schema.org",
["@type"] = "TechArticle", ["@type"] = "TechArticle",
@ -120,9 +119,9 @@ public sealed class HelpController(IHelpService help, IHelpMarkdownRenderer mark
["description"] = string.IsNullOrWhiteSpace(article.QuickSummary) ? null : article.QuickSummary, ["description"] = string.IsNullOrWhiteSpace(article.QuickSummary) ? null : article.QuickSummary,
["dateModified"] = article.LastModifiedUtc == default ? null : article.LastModifiedUtc.ToString("O") ["dateModified"] = article.LastModifiedUtc == default ? null : article.LastModifiedUtc.ToString("O")
}), }),
BuildBreadcrumbJsonLd( PublicSeo.BreadcrumbJsonLd(
("Help", BuildAbsoluteUrl("/help")), ("Help", PublicSeo.AbsoluteUrl(configuration, "/help")),
(article.Category, BuildAbsoluteUrl(article.CategoryUrl)), (article.Category, PublicSeo.AbsoluteUrl(configuration, article.CategoryUrl)),
(article.Title, canonicalUrl)) (article.Title, canonicalUrl))
}; };
return View(new HelpCentreArticleViewModel return View(new HelpCentreArticleViewModel
@ -160,34 +159,4 @@ public sealed class HelpController(IHelpService help, IHelpMarkdownRenderer mark
? path ? path
: $"{path}{HttpContext.Request.QueryString.Value}"; : $"{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
});
} }

View File

@ -1,17 +1,35 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using PlotLine.Models; using PlotLine.Models;
using PlotLine.Services;
using System.Diagnostics; using System.Diagnostics;
namespace PlotLine.Controllers namespace PlotLine.Controllers
{ {
[AllowAnonymous] [AllowAnonymous]
public class HomeController : Controller public class HomeController(IConfiguration configuration) : Controller
{ {
[HttpGet("/", Name = PublicRouteNames.Home)]
public IActionResult Index() 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.JsonLd(new Dictionary<string, object?>
{
["@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)
};
return View(); return View();
} }

View File

@ -12,11 +12,22 @@ public sealed class PricingController(
ICurrentUserService currentUser, ICurrentUserService currentUser,
IUserRepository users, IUserRepository users,
ISubscriptionService subscriptions, ISubscriptionService subscriptions,
IStripeBillingService stripeBilling) : Controller IStripeBillingService stripeBilling,
IConfiguration configuration) : Controller
{ {
[HttpGet] [HttpGet("pricing", Name = PublicRouteNames.Pricing)]
public async Task<IActionResult> Index() public async Task<IActionResult> 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.WebPageJsonLd(configuration, "WebPage", "Pricing", "/pricing", description)
};
UserSubscriptionDetail? currentSubscription = null; UserSubscriptionDetail? currentSubscription = null;
if (currentUser.UserId is int userId) if (currentUser.UserId is int userId)
{ {
@ -47,7 +58,7 @@ public sealed class PricingController(
{ {
if (!currentUser.UserId.HasValue) if (!currentUser.UserId.HasValue)
{ {
var returnUrl = Url.Action(nameof(Checkout), "Pricing", new { id, billingInterval }) ?? "/Pricing"; var returnUrl = Url.Action(nameof(Checkout), "Pricing", new { id, billingInterval }) ?? "/pricing";
return RedirectToAction("Login", "Account", new { returnUrl }); return RedirectToAction("Login", "Account", new { returnUrl });
} }

View File

@ -1,27 +1,77 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using PlotLine.Services;
using PlotLine.ViewModels; using PlotLine.ViewModels;
namespace PlotLine.Controllers; namespace PlotLine.Controllers;
[AllowAnonymous] [AllowAnonymous]
[Route("")] [Route("")]
public sealed class PublicController : Controller public sealed class PublicController(IConfiguration configuration) : Controller
{ {
private const string SupportEmail = "hello@plotdirector.com"; private const string SupportEmail = "hello@plotdirector.com";
private const string PolicyDate = "June 7, 2026"; private const string PolicyDate = "June 7, 2026";
private const string 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";
[HttpGet("About")] [HttpGet("about", Name = PublicRouteNames.About)]
public IActionResult About() => View(CreateModel()); public IActionResult About()
{
SetSeo(
"About PlotDirector | Built for Novelists",
"Learn why PlotDirector was created by a novelist to help writers organise complex stories, manage continuity, and finish their books with confidence.",
"/about",
"AboutPage",
"About PlotDirector");
return View(CreateModel());
}
[HttpGet("Contact")] [HttpGet("contact", Name = PublicRouteNames.Contact)]
public IActionResult Contact() => View(CreateModel()); public IActionResult Contact()
{
SetSeo(
"Contact PlotDirector",
"Contact PlotDirector for support, questions, feedback, or feature suggestions about our novel planning and story organisation platform.",
"/contact",
"ContactPage",
"Contact PlotDirector");
return View(CreateModel());
}
[HttpGet("Privacy")] [HttpGet("privacy", Name = PublicRouteNames.Privacy)]
public IActionResult Privacy() => View(CreateModel()); public IActionResult Privacy()
{
SetSeo(
"Privacy Policy | PlotDirector",
"Read the PlotDirector Privacy Policy to learn how we protect your account information, subscription data, and story content.",
"/privacy",
"PrivacyPolicy",
"Privacy Policy");
return View(CreateModel());
}
[HttpGet("Terms")] [HttpGet("terms", Name = PublicRouteNames.Terms)]
public IActionResult Terms() => View(CreateModel()); public IActionResult Terms()
{
SetSeo(
"Terms of Service | PlotDirector",
"Read the PlotDirector Terms of Service for information about subscriptions, user content, acceptable use, and account responsibilities.",
"/terms",
"WebPage",
"Terms of Service");
return View(CreateModel());
}
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.WebPageJsonLd(configuration, schemaType, pageName, path, description)
};
}
private static PublicPageViewModel CreateModel() => new() private static PublicPageViewModel CreateModel() => new()
{ {

View File

@ -64,7 +64,7 @@ public sealed class SitemapController(IHelpService help, IConfiguration configur
foreach (var url in urls) foreach (var url in urls)
{ {
xml.WriteStartElement("url"); xml.WriteStartElement("url");
xml.WriteElementString("loc", BuildAbsoluteUrl(url.Path)); xml.WriteElementString("loc", PublicSeo.AbsoluteUrl(configuration, url.Path));
if (url.LastModifiedUtc is DateTime lastModifiedUtc && lastModifiedUtc != default) if (url.LastModifiedUtc is DateTime lastModifiedUtc && lastModifiedUtc != default)
{ {
xml.WriteElementString("lastmod", lastModifiedUtc.ToString("yyyy-MM-dd")); xml.WriteElementString("lastmod", lastModifiedUtc.ToString("yyyy-MM-dd"));
@ -81,17 +81,6 @@ public sealed class SitemapController(IHelpService help, IConfiguration configur
return writer.ToString(); return writer.ToString();
} }
private string BuildAbsoluteUrl(string path)
{
var baseUrl = configuration["Site:PublicBaseUrl"]?.TrimEnd('/');
if (string.IsNullOrWhiteSpace(baseUrl))
{
baseUrl = "https://plotdirector.com";
}
return $"{baseUrl}{path}";
}
private sealed record SitemapUrl(string Path, string ChangeFrequency, string Priority, DateTime? LastModifiedUtc = null); private sealed record SitemapUrl(string Path, string ChangeFrequency, string Priority, DateTime? LastModifiedUtc = null);
private sealed class Utf8StringWriter : StringWriter private sealed class Utf8StringWriter : StringWriter

View File

@ -0,0 +1,66 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace PlotLine.Services;
public static class PublicRouteNames
{
public const string Home = "PublicHome";
public const string Pricing = "PublicPricing";
public const string Help = "PublicHelp";
public const string HelpCategory = "PublicHelpCategory";
public const string HelpArticle = "PublicHelpArticle";
public const string About = "PublicAbout";
public const string Contact = "PublicContact";
public const string Privacy = "PublicPrivacy";
public const string Terms = "PublicTerms";
}
public static class PublicSeo
{
public const string DefaultBaseUrl = "https://plotdirector.com";
public static string BaseUrl(IConfiguration configuration)
{
var baseUrl = configuration["Site:PublicBaseUrl"]?.TrimEnd('/');
return string.IsNullOrWhiteSpace(baseUrl) ? DefaultBaseUrl : baseUrl;
}
public static string AbsoluteUrl(IConfiguration configuration, string path) =>
$"{BaseUrl(configuration)}{path}";
public static string JsonLd(object value) =>
JsonSerializer.Serialize(value, new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
});
public static string WebPageJsonLd(
IConfiguration configuration,
string schemaType,
string name,
string path,
string? description = null) =>
JsonLd(new Dictionary<string, object?>
{
["@context"] = "https://schema.org",
["@type"] = schemaType,
["name"] = name,
["url"] = AbsoluteUrl(configuration, path),
["description"] = description
});
public static string BreadcrumbJsonLd(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
})
});
}

View File

@ -1,48 +1,6 @@
@{ @{
ViewData["Title"] = "Finally finish your novel"; ViewData["Title"] = "Finally finish your novel";
ViewData["ShellClass"] = "marketing-shell"; ViewData["ShellClass"] = "marketing-shell";
ViewData["SeoTitle"] = "PlotDirector | Novel Planning Software for Authors";
ViewData["SeoDescription"] = "PlotDirector helps authors organise characters, plot threads, timelines, story assets, relationships, and continuity so they can finish complex novels with confidence.";
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"] = "https://plotdirector.com/";
ViewData["StructuredData"] = new[]
{
"""
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "PlotDirector",
"url": "https://plotdirector.com",
"description": "Novel planning and story organisation software for authors."
}
""",
"""
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "PlotDirector",
"applicationCategory": "WritingApplication",
"operatingSystem": "Web",
"description": "Novel planning software for authors to organise characters, plot threads, timelines, story assets, relationships, continuity, and series.",
"url": "https://plotdirector.com",
"offers": {
"@type": "Offer",
"priceCurrency": "GBP",
"availability": "https://schema.org/InStock"
}
}
""",
"""
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "PlotDirector",
"url": "https://plotdirector.com",
"logo": "https://plotdirector.com/android-chrome-512x512.png",
"sameAs": []
}
"""
};
} }
<article class="marketing-home"> <article class="marketing-home">

View File

@ -3,29 +3,6 @@
@{ @{
ViewData["Title"] = "Pricing"; ViewData["Title"] = "Pricing";
ViewData["ShellClass"] = "marketing-shell"; ViewData["ShellClass"] = "marketing-shell";
ViewData["SeoTitle"] = "Pricing | PlotDirector";
ViewData["SeoDescription"] = "Compare PlotDirector plans and choose the right subscription for organising your novel, series, characters, plot threads, timelines, and story details.";
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"] = "https://plotdirector.com/pricing";
ViewData["StructuredData"] = new[]
{
"""
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "PlotDirector",
"applicationCategory": "WritingApplication",
"operatingSystem": "Web",
"description": "Novel planning software for authors to organise characters, plot threads, timelines, story assets, relationships, continuity, and series.",
"url": "https://plotdirector.com",
"offers": {
"@type": "Offer",
"priceCurrency": "GBP",
"availability": "https://schema.org/InStock"
}
}
"""
};
var paidPlans = Model.Plans.Where(x => x.RequiresStripeSubscription).ToList(); var paidPlans = Model.Plans.Where(x => x.RequiresStripeSubscription).ToList();
var suggestedPlanId = paidPlans.Count >= 2 ? paidPlans[paidPlans.Count / 2].SubscriptionLevelID : (int?)null; var suggestedPlanId = paidPlans.Count >= 2 ? paidPlans[paidPlans.Count / 2].SubscriptionLevelID : (int?)null;
} }

View File

@ -2,10 +2,6 @@
@{ @{
ViewData["Title"] = "About"; ViewData["Title"] = "About";
ViewData["ShellClass"] = "marketing-shell"; ViewData["ShellClass"] = "marketing-shell";
ViewData["SeoTitle"] = "About PlotDirector | Built for Novelists";
ViewData["SeoDescription"] = "Learn why PlotDirector was created by a novelist to help writers organise complex stories, manage continuity, and finish their books with confidence.";
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"] = "https://plotdirector.com/about";
} }
<article class="marketing-home public-info-page"> <article class="marketing-home public-info-page">

View File

@ -2,10 +2,6 @@
@{ @{
ViewData["Title"] = "Contact"; ViewData["Title"] = "Contact";
ViewData["ShellClass"] = "marketing-shell"; ViewData["ShellClass"] = "marketing-shell";
ViewData["SeoTitle"] = "Contact PlotDirector";
ViewData["SeoDescription"] = "Contact PlotDirector for support, questions, feedback, or feature suggestions about our novel planning and story organisation platform.";
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"] = "https://plotdirector.com/contact";
} }
<article class="marketing-home public-info-page"> <article class="marketing-home public-info-page">

View File

@ -2,10 +2,6 @@
@{ @{
ViewData["Title"] = "Privacy Policy"; ViewData["Title"] = "Privacy Policy";
ViewData["ShellClass"] = "marketing-shell"; ViewData["ShellClass"] = "marketing-shell";
ViewData["SeoTitle"] = "Privacy Policy | PlotDirector";
ViewData["SeoDescription"] = "Read the PlotDirector Privacy Policy to learn how we protect your account information, subscription data, and story content.";
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"] = "https://plotdirector.com/privacy";
} }
<article class="marketing-home public-info-page"> <article class="marketing-home public-info-page">

View File

@ -2,10 +2,6 @@
@{ @{
ViewData["Title"] = "Terms of Service"; ViewData["Title"] = "Terms of Service";
ViewData["ShellClass"] = "marketing-shell"; ViewData["ShellClass"] = "marketing-shell";
ViewData["SeoTitle"] = "Terms of Service | PlotDirector";
ViewData["SeoDescription"] = "Read the PlotDirector Terms of Service for information about subscriptions, user content, acceptable use, and account responsibilities.";
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"] = "https://plotdirector.com/terms";
} }
<article class="marketing-home public-info-page"> <article class="marketing-home public-info-page">

View File

@ -105,7 +105,7 @@
<header> <header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm border-bottom plotline-nav mb-3"> <nav class="navbar navbar-expand-sm navbar-toggleable-sm border-bottom plotline-nav mb-3">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand fw-semibold" asp-area="" asp-controller="Home" asp-action="Index">PlotDirector</a> <a class="navbar-brand fw-semibold" asp-route="@PublicRouteNames.Home">PlotDirector</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent" <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation"> aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
@ -124,7 +124,7 @@
<a class="nav-link text-dark" asp-area="" asp-controller="Imports" asp-action="Index">Import</a> <a class="nav-link text-dark" asp-area="" asp-controller="Imports" asp-action="Index">Import</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-dark" href="/help">Help</a> <a class="nav-link text-dark" asp-route="@PublicRouteNames.Help">Help</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="FeatureRequests" asp-action="Index"> <a class="nav-link text-dark" asp-area="" asp-controller="FeatureRequests" asp-action="Index">
@ -151,13 +151,13 @@
else else
{ {
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-dark" href="/">Home</a> <a class="nav-link text-dark" asp-route="@PublicRouteNames.Home">Home</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-dark" href="/pricing">Pricing</a> <a class="nav-link text-dark" asp-route="@PublicRouteNames.Pricing">Pricing</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-dark" href="/help">Help</a> <a class="nav-link text-dark" asp-route="@PublicRouteNames.Help">Help</a>
</li> </li>
} }
</ul> </ul>
@ -245,14 +245,15 @@
{ {
<footer class="public-footer" aria-label="Public site footer"> <footer class="public-footer" aria-label="Public site footer">
<div class="marketing-container public-footer__inner"> <div class="marketing-container public-footer__inner">
<a class="public-footer__brand" asp-controller="Home" asp-action="Index">PlotDirector</a> <a class="public-footer__brand" asp-route="@PublicRouteNames.Home">PlotDirector</a>
<nav class="public-footer__nav" aria-label="Footer navigation"> <nav class="public-footer__nav" aria-label="Footer navigation">
<a asp-controller="Home" asp-action="Index">Home</a> <a asp-route="@PublicRouteNames.Home">Home</a>
<a asp-controller="Public" asp-action="About">About</a> <a asp-route="@PublicRouteNames.About">About</a>
<a asp-controller="Pricing" asp-action="Index">Pricing</a> <a asp-route="@PublicRouteNames.Pricing">Pricing</a>
<a asp-controller="Public" asp-action="Contact">Contact</a> <a asp-route="@PublicRouteNames.Help">Help</a>
<a asp-controller="Public" asp-action="Privacy">Privacy Policy</a> <a asp-route="@PublicRouteNames.Contact">Contact</a>
<a asp-controller="Public" asp-action="Terms">Terms of Service</a> <a asp-route="@PublicRouteNames.Privacy">Privacy Policy</a>
<a asp-route="@PublicRouteNames.Terms">Terms of Service</a>
</nav> </nav>
</div> </div>
</footer> </footer>

View File

@ -19,6 +19,9 @@
"Storage": { "Storage": {
"UploadsRoot": "" "UploadsRoot": ""
}, },
"Site": {
"PublicBaseUrl": "https://plotdirector.com"
},
"Admin": { "Admin": {
"AllowedEmails": ["nickbeckley5@gmail.com"] "AllowedEmails": ["nickbeckley5@gmail.com"]
}, },