From 19d9bc4bb1947141843002a1e5700184e13a2f19 Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Wed, 24 Jun 2026 10:57:18 +0100 Subject: [PATCH] mplemented the public navigation and SEO coverage pass. --- PlotLine/Controllers/HelpController.cs | 61 +++++--------------- PlotLine/Controllers/HomeController.cs | 24 +++++++- PlotLine/Controllers/PricingController.cs | 17 +++++- PlotLine/Controllers/PublicController.cs | 68 ++++++++++++++++++++--- PlotLine/Controllers/SitemapController.cs | 13 +---- PlotLine/Services/PublicSeo.cs | 66 ++++++++++++++++++++++ PlotLine/Views/Home/Index.cshtml | 42 -------------- PlotLine/Views/Pricing/Index.cshtml | 23 -------- PlotLine/Views/Public/About.cshtml | 4 -- PlotLine/Views/Public/Contact.cshtml | 4 -- PlotLine/Views/Public/Privacy.cshtml | 4 -- PlotLine/Views/Public/Terms.cshtml | 4 -- PlotLine/Views/Shared/_Layout.cshtml | 25 +++++---- PlotLine/appsettings.json | 3 + 14 files changed, 192 insertions(+), 166 deletions(-) create mode 100644 PlotLine/Services/PublicSeo.cs diff --git a/PlotLine/Controllers/HelpController.cs b/PlotLine/Controllers/HelpController.cs index 2b4d3ef..ff6512d 100644 --- a/PlotLine/Controllers/HelpController.cs +++ b/PlotLine/Controllers/HelpController.cs @@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Authorization; using PlotLine.Services; using PlotLine.ViewModels; using System.Net; -using System.Text.Json; namespace PlotLine.Controllers; @@ -11,7 +10,7 @@ namespace PlotLine.Controllers; [AllowAnonymous] public sealed class HelpController(IHelpService help, IHelpMarkdownRenderer markdown, IConfiguration configuration) : Controller { - [HttpGet("")] + [HttpGet("", Name = PublicRouteNames.Help)] public IActionResult Index(string? q) { if (!IsCanonicalPath("/help")) @@ -19,11 +18,11 @@ public sealed class HelpController(IHelpService help, IHelpMarkdownRenderer mark return RedirectPermanent(PreserveQuery("/help")); } - var canonicalUrl = BuildAbsoluteUrl("/help"); + var canonicalUrl = PublicSeo.AbsoluteUrl(configuration, "/help"); ViewData["CanonicalUrl"] = canonicalUrl; ViewData["StructuredData"] = new[] { - JsonLd(new Dictionary + PublicSeo.JsonLd(new Dictionary { ["@context"] = "https://schema.org", ["@type"] = "CollectionPage", @@ -31,7 +30,7 @@ public sealed class HelpController(IHelpService help, IHelpMarkdownRenderer mark ["url"] = canonicalUrl, ["description"] = "Find practical guidance for planning, tracking, and shaping your story in PlotDirector." }), - BuildBreadcrumbJsonLd( + PublicSeo.BreadcrumbJsonLd( ("Help", canonicalUrl)) }; 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) { var category = help.GetCategoryBySlug(categorySlug); @@ -68,11 +67,11 @@ public sealed class HelpController(IHelpService help, IHelpMarkdownRenderer mark } var articles = help.GetByCategorySlug(categorySlug); - var canonicalUrl = BuildAbsoluteUrl(canonicalPath); + var canonicalUrl = PublicSeo.AbsoluteUrl(configuration, canonicalPath); ViewData["CanonicalUrl"] = canonicalUrl; ViewData["StructuredData"] = new[] { - JsonLd(new Dictionary + PublicSeo.JsonLd(new Dictionary { ["@context"] = "https://schema.org", ["@type"] = "CollectionPage", @@ -80,8 +79,8 @@ public sealed class HelpController(IHelpService help, IHelpMarkdownRenderer mark ["url"] = canonicalUrl, ["description"] = $"Help articles about {category} in PlotDirector." }), - BuildBreadcrumbJsonLd( - ("Help", BuildAbsoluteUrl("/help")), + PublicSeo.BreadcrumbJsonLd( + ("Help", PublicSeo.AbsoluteUrl(configuration, "/help")), (category, canonicalUrl)) }; 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) { var article = help.GetBySlugs(categorySlug, articleSlug); @@ -106,11 +105,11 @@ public sealed class HelpController(IHelpService help, IHelpMarkdownRenderer mark return RedirectPermanent(article.Url); } - var canonicalUrl = BuildAbsoluteUrl(article.Url); + var canonicalUrl = PublicSeo.AbsoluteUrl(configuration, article.Url); ViewData["CanonicalUrl"] = canonicalUrl; ViewData["StructuredData"] = new[] { - JsonLd(new Dictionary + PublicSeo.JsonLd(new Dictionary { ["@context"] = "https://schema.org", ["@type"] = "TechArticle", @@ -120,9 +119,9 @@ public sealed class HelpController(IHelpService help, IHelpMarkdownRenderer mark ["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)), + PublicSeo.BreadcrumbJsonLd( + ("Help", PublicSeo.AbsoluteUrl(configuration, "/help")), + (article.Category, PublicSeo.AbsoluteUrl(configuration, article.CategoryUrl)), (article.Title, canonicalUrl)) }; return View(new HelpCentreArticleViewModel @@ -160,34 +159,4 @@ public sealed class HelpController(IHelpService help, IHelpMarkdownRenderer mark ? 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 - { - ["@context"] = "https://schema.org", - ["@type"] = "BreadcrumbList", - ["itemListElement"] = items.Select((item, index) => new Dictionary - { - ["@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 - }); } diff --git a/PlotLine/Controllers/HomeController.cs b/PlotLine/Controllers/HomeController.cs index 8011f53..d79c5f5 100644 --- a/PlotLine/Controllers/HomeController.cs +++ b/PlotLine/Controllers/HomeController.cs @@ -1,17 +1,35 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Authorization; - using PlotLine.Models; - +using PlotLine.Services; using System.Diagnostics; namespace PlotLine.Controllers { [AllowAnonymous] - public class HomeController : Controller + public class HomeController(IConfiguration configuration) : Controller { + [HttpGet("/", Name = PublicRouteNames.Home)] 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 + { + ["@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(); } diff --git a/PlotLine/Controllers/PricingController.cs b/PlotLine/Controllers/PricingController.cs index 9998ba1..d44361a 100644 --- a/PlotLine/Controllers/PricingController.cs +++ b/PlotLine/Controllers/PricingController.cs @@ -12,11 +12,22 @@ public sealed class PricingController( ICurrentUserService currentUser, IUserRepository users, ISubscriptionService subscriptions, - IStripeBillingService stripeBilling) : Controller + IStripeBillingService stripeBilling, + IConfiguration configuration) : Controller { - [HttpGet] + [HttpGet("pricing", Name = PublicRouteNames.Pricing)] 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.WebPageJsonLd(configuration, "WebPage", "Pricing", "/pricing", description) + }; + UserSubscriptionDetail? currentSubscription = null; if (currentUser.UserId is int userId) { @@ -47,7 +58,7 @@ public sealed class PricingController( { 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 }); } diff --git a/PlotLine/Controllers/PublicController.cs b/PlotLine/Controllers/PublicController.cs index 7ce13bb..342afc2 100644 --- a/PlotLine/Controllers/PublicController.cs +++ b/PlotLine/Controllers/PublicController.cs @@ -1,27 +1,77 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using PlotLine.Services; using PlotLine.ViewModels; namespace PlotLine.Controllers; [AllowAnonymous] [Route("")] -public sealed class PublicController : Controller +public sealed class PublicController(IConfiguration configuration) : Controller { private const string SupportEmail = "hello@plotdirector.com"; 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")] - public IActionResult About() => View(CreateModel()); + [HttpGet("about", Name = PublicRouteNames.About)] + 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")] - public IActionResult Contact() => View(CreateModel()); + [HttpGet("contact", Name = PublicRouteNames.Contact)] + 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")] - public IActionResult Privacy() => View(CreateModel()); + [HttpGet("privacy", Name = PublicRouteNames.Privacy)] + 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")] - public IActionResult Terms() => View(CreateModel()); + [HttpGet("terms", Name = PublicRouteNames.Terms)] + 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() { diff --git a/PlotLine/Controllers/SitemapController.cs b/PlotLine/Controllers/SitemapController.cs index b2fed91..758433e 100644 --- a/PlotLine/Controllers/SitemapController.cs +++ b/PlotLine/Controllers/SitemapController.cs @@ -64,7 +64,7 @@ public sealed class SitemapController(IHelpService help, IConfiguration configur foreach (var url in urls) { 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) { xml.WriteElementString("lastmod", lastModifiedUtc.ToString("yyyy-MM-dd")); @@ -81,17 +81,6 @@ public sealed class SitemapController(IHelpService help, IConfiguration configur 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 class Utf8StringWriter : StringWriter diff --git a/PlotLine/Services/PublicSeo.cs b/PlotLine/Services/PublicSeo.cs new file mode 100644 index 0000000..1aed44f --- /dev/null +++ b/PlotLine/Services/PublicSeo.cs @@ -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 + { + ["@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 + { + ["@context"] = "https://schema.org", + ["@type"] = "BreadcrumbList", + ["itemListElement"] = items.Select((item, index) => new Dictionary + { + ["@type"] = "ListItem", + ["position"] = index + 1, + ["name"] = item.Name, + ["item"] = item.Url + }) + }); +} diff --git a/PlotLine/Views/Home/Index.cshtml b/PlotLine/Views/Home/Index.cshtml index 72bd8f9..e18515f 100644 --- a/PlotLine/Views/Home/Index.cshtml +++ b/PlotLine/Views/Home/Index.cshtml @@ -1,48 +1,6 @@ @{ ViewData["Title"] = "Finally finish your novel"; 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": [] - } - """ - }; }
diff --git a/PlotLine/Views/Pricing/Index.cshtml b/PlotLine/Views/Pricing/Index.cshtml index fce367a..b3d5718 100644 --- a/PlotLine/Views/Pricing/Index.cshtml +++ b/PlotLine/Views/Pricing/Index.cshtml @@ -3,29 +3,6 @@ @{ ViewData["Title"] = "Pricing"; 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 suggestedPlanId = paidPlans.Count >= 2 ? paidPlans[paidPlans.Count / 2].SubscriptionLevelID : (int?)null; } diff --git a/PlotLine/Views/Public/About.cshtml b/PlotLine/Views/Public/About.cshtml index 6a6099d..c288ba1 100644 --- a/PlotLine/Views/Public/About.cshtml +++ b/PlotLine/Views/Public/About.cshtml @@ -2,10 +2,6 @@ @{ ViewData["Title"] = "About"; 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"; }
diff --git a/PlotLine/Views/Public/Contact.cshtml b/PlotLine/Views/Public/Contact.cshtml index b3190f8..d7e7288 100644 --- a/PlotLine/Views/Public/Contact.cshtml +++ b/PlotLine/Views/Public/Contact.cshtml @@ -2,10 +2,6 @@ @{ ViewData["Title"] = "Contact"; 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"; }
diff --git a/PlotLine/Views/Public/Privacy.cshtml b/PlotLine/Views/Public/Privacy.cshtml index 966c25d..f7a3f5b 100644 --- a/PlotLine/Views/Public/Privacy.cshtml +++ b/PlotLine/Views/Public/Privacy.cshtml @@ -2,10 +2,6 @@ @{ ViewData["Title"] = "Privacy Policy"; 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"; }
diff --git a/PlotLine/Views/Public/Terms.cshtml b/PlotLine/Views/Public/Terms.cshtml index 28d1d9d..8f1e2b8 100644 --- a/PlotLine/Views/Public/Terms.cshtml +++ b/PlotLine/Views/Public/Terms.cshtml @@ -2,10 +2,6 @@ @{ ViewData["Title"] = "Terms of Service"; 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"; }
diff --git a/PlotLine/Views/Shared/_Layout.cshtml b/PlotLine/Views/Shared/_Layout.cshtml index 1ab0e77..1ef3f75 100644 --- a/PlotLine/Views/Shared/_Layout.cshtml +++ b/PlotLine/Views/Shared/_Layout.cshtml @@ -105,7 +105,7 @@