mplemented the public navigation and SEO coverage pass.
This commit is contained in:
parent
bd5bf07906
commit
19d9bc4bb1
@ -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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
66
PlotLine/Services/PublicSeo.cs
Normal file
66
PlotLine/Services/PublicSeo.cs
Normal 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
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -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">
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -19,6 +19,9 @@
|
|||||||
"Storage": {
|
"Storage": {
|
||||||
"UploadsRoot": ""
|
"UploadsRoot": ""
|
||||||
},
|
},
|
||||||
|
"Site": {
|
||||||
|
"PublicBaseUrl": "https://plotdirector.com"
|
||||||
|
},
|
||||||
"Admin": {
|
"Admin": {
|
||||||
"AllowedEmails": ["nickbeckley5@gmail.com"]
|
"AllowedEmails": ["nickbeckley5@gmail.com"]
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user