diff --git a/CatherineLynwood/CatherineLynwood.csproj b/CatherineLynwood/CatherineLynwood.csproj index c58b366..33b08e6 100644 --- a/CatherineLynwood/CatherineLynwood.csproj +++ b/CatherineLynwood/CatherineLynwood.csproj @@ -43,9 +43,96 @@ Never + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + @@ -58,6 +145,7 @@ + diff --git a/CatherineLynwood/Controllers/AudioController.cs b/CatherineLynwood/Controllers/AudioController.cs new file mode 100644 index 0000000..3725f35 --- /dev/null +++ b/CatherineLynwood/Controllers/AudioController.cs @@ -0,0 +1,123 @@ +using CatherineLynwood.Services; + +using Microsoft.AspNetCore.Mvc; + +namespace CatherineLynwood.Controllers +{ + [ApiController] + [Route("api/audio")] + public class AudioController : ControllerBase + { + private readonly IWebHostEnvironment _env; + private readonly ChapterAudioMapCache _cache; + private readonly AudioTokenService _tokenService; + + public AudioController(IWebHostEnvironment env, ChapterAudioMapCache cache, AudioTokenService tokenService) + { + _env = env; + _cache = cache; + _tokenService = tokenService; + } + + [HttpGet("secure-stream/{id}")] + public IActionResult SecureStream(string id, [FromQuery] long expires, [FromQuery] string token) + { + if (!_tokenService.IsValid(id, expires, token)) + return Unauthorized("Invalid or expired token."); + + var segment = _cache.Get(id); + if (segment == null) + return NotFound($"No audio segment found for ID: {id}"); + + var filePath = Path.Combine(_env.WebRootPath, "chapters", segment.ChapterFolder, segment.File); + if (!System.IO.File.Exists(filePath)) + return NotFound($"File not found: {filePath}"); + + var stream = System.IO.File.OpenRead(filePath); + return File(stream, "audio/mpeg"); + } + + + + [HttpGet("stream/{id}")] + public IActionResult Stream(string id) + { + var segment = _cache.Get(id); + if (segment == null) + return NotFound($"No audio segment found for ID: {id}"); + + var filePath = Path.Combine(_env.WebRootPath, "chapters", segment.ChapterFolder, segment.File); + if (!System.IO.File.Exists(filePath)) + return NotFound($"File not found: {filePath}"); + + var stream = System.IO.File.OpenRead(filePath); + return File(stream, "audio/mpeg"); + } + + [HttpGet("playlist/chapter/{chapterNumber:int}")] + public IActionResult GetChapterPlaylist(int chapterNumber) + { + var segments = _cache.GetAll() + .Where(s => s.ChapterFolder.EndsWith($"Chapter_{chapterNumber}", StringComparison.OrdinalIgnoreCase)) + .OrderBy(s => s.File) + .ToList(); + + if (!segments.Any()) + return NotFound(); + + var playlist = new List(); + var currentTime = TimeSpan.Zero; + + foreach (var segment in segments) + { + playlist.Add(new + { + id = segment.Id, + display = currentTime.ToString(@"mm\:ss") + }); + + currentTime += segment.Duration; + } + + return Ok(playlist); + } + + [HttpGet("playlist/all")] + public IActionResult GetAllChapters() + { + var segments = _cache.GetAll() + .OrderBy(s => s.ChapterFolder) + .ThenBy(s => s.File) + .ToList(); + + var grouped = segments + .GroupBy(s => s.ChapterFolder) + .Select(group => new + { + chapter = group.Key, + segments = group.Select(s => new + { + id = s.Id, + display = s.Duration.ToString(@"mm\:ss"), + text = s.Text ?? "" + }).ToList() + }); + + return Ok(grouped); + } + + + [HttpGet("token-url/{id}")] + public IActionResult GetTokenUrl(string id) + { + var segment = _cache.Get(id); + if (segment == null) + return NotFound(); + + var url = _tokenService.GenerateUrl(id); + return Content(url); + } + + + } +} diff --git a/CatherineLynwood/Controllers/DiscoveryController.cs b/CatherineLynwood/Controllers/DiscoveryController.cs index c75e261..9f47faf 100644 --- a/CatherineLynwood/Controllers/DiscoveryController.cs +++ b/CatherineLynwood/Controllers/DiscoveryController.cs @@ -2,30 +2,70 @@ namespace CatherineLynwood.Controllers { - [Route("the-alpha-flame/extras/discovery")] + [Route("the-alpha-flame/discovery")] public class DiscoveryController : Controller { - public IActionResult Index() + #region Public Methods + + [Route("chapters/chapter-1-beth")] + public IActionResult Chapter1() { return View(); } - [Route("epilogue")] + [Route("chapters/chapter-13-susie")] + public IActionResult Chapter13() + { + return View(); + } + + [Route("chapters/chapter-2-maggie")] + public IActionResult Chapter2() + { + return View(); + } + + [BookAccess(1, 1)] + [Route("extras/epilogue")] public IActionResult Epilogue() { return View(); } - [Route("scrap-book")] + [BookAccess(1, 1)] + [Route("extras")] + public IActionResult Extras() + { + return View(); + } + + [Route("")] + public IActionResult Index() + { + return View(); + } + + [BookAccess(1, 1)] + [Route("extras/listen")] + public IActionResult Listen() + { + return View(); + } + + [BookAccess(1, 1)] + [Route("extras/maggies-designs")] + public IActionResult MaggiesDesigns() + { + return View(); + } + + [BookAccess(1, 1)] + [Route("extras/scrap-book")] public IActionResult ScrapBook() { return View(); } - [Route("maggies-designs")] - public IActionResult MaggiesDesigns() - { - return View(); - } + #endregion Public Methods } -} +} \ No newline at end of file diff --git a/CatherineLynwood/Controllers/SitemapController.cs b/CatherineLynwood/Controllers/SitemapController.cs index b9707a3..f495056 100644 --- a/CatherineLynwood/Controllers/SitemapController.cs +++ b/CatherineLynwood/Controllers/SitemapController.cs @@ -40,12 +40,12 @@ namespace CatherineLynwood.Controllers new SitemapEntry { Url = Url.Action("Index", "TheAlphaFlame", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, new SitemapEntry { Url = Url.Action("Blog", "TheAlphaFlame", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, new SitemapEntry { Url = Url.Action("Characters", "TheAlphaFlame", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, - new SitemapEntry { Url = Url.Action("Discovery", "TheAlphaFlame", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, + new SitemapEntry { Url = Url.Action("Index", "Discovery", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, new SitemapEntry { Url = Url.Action("Index", "Publishing", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, new SitemapEntry { Url = Url.Action("Privacy", "Home", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, - new SitemapEntry { Url = Url.Action("Chapter1", "TheAlphaFlame", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, - new SitemapEntry { Url = Url.Action("Chapter2", "TheAlphaFlame", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, - new SitemapEntry { Url = Url.Action("Chapter13", "TheAlphaFlame", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, + new SitemapEntry { Url = Url.Action("Chapter1", "Discovery", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, + new SitemapEntry { Url = Url.Action("Chapter2", "Discovery", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, + new SitemapEntry { Url = Url.Action("Chapter13", "Discovery", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, // Additional static pages }; diff --git a/CatherineLynwood/Controllers/TheAlphaFlameController.cs b/CatherineLynwood/Controllers/TheAlphaFlameController.cs index 6dac951..0b8d5f1 100644 --- a/CatherineLynwood/Controllers/TheAlphaFlameController.cs +++ b/CatherineLynwood/Controllers/TheAlphaFlameController.cs @@ -36,6 +36,7 @@ namespace CatherineLynwood.Controllers return View(); } + [Route("blog")] public async Task Blog(BlogFilter blogFilter) { @@ -84,6 +85,12 @@ namespace CatherineLynwood.Controllers public async Task BlogItem(string slug, bool showThanks) { Blog blog = await _dataAccess.GetBlogItemAsync(slug); + + if (blog.Title == null) + { + return RedirectPermanent("/the-alpha-flame/blog"); + } + blog.ShowThanks = showThanks; if (blog.Template == "slideshow") @@ -94,22 +101,7 @@ namespace CatherineLynwood.Controllers return View("DefaultTemplate", blog); } - [Route("chapters/chapter-1-beth")] - public IActionResult Chapter1() - { - return View(); - } - [Route("chapters/chapter-2-maggie")] - public IActionResult Chapter2() - { - return View(); - } - [Route("chapters/chapter-13-susie")] - public IActionResult Chapter13() - { - return View(); - } [Route("characters")] public IActionResult Characters() @@ -166,19 +158,7 @@ namespace CatherineLynwood.Controllers return RedirectToAction("BlogItem", new { slug = blogUrl, showThanks = showThanks }); } - [Route("discovery")] - public IActionResult Discovery() - { - return View(); - } - - [BookAccess(1, 1)] - [Route("extras")] - public IActionResult Extras() - { - return View(); - } - + [Route("")] public IActionResult Index() { return View(); diff --git a/CatherineLynwood/Middleware/BotFilterMiddleware.cs b/CatherineLynwood/Middleware/BotFilterMiddleware.cs new file mode 100644 index 0000000..f62430d --- /dev/null +++ b/CatherineLynwood/Middleware/BotFilterMiddleware.cs @@ -0,0 +1,39 @@ +namespace CatherineLynwood.Middleware +{ + public class BotFilterMiddleware + { + #region Private Fields + + private static readonly List BadBots = new() + { + "AhrefsBot", "SemrushBot", "MJ12bot", "DotBot", "Baiduspider", "YandexBot" + }; + + private readonly RequestDelegate _next; + + #endregion Private Fields + + #region Public Constructors + + public BotFilterMiddleware(RequestDelegate next) => _next = next; + + #endregion Public Constructors + + #region Public Methods + + public async Task Invoke(HttpContext context) + { + var userAgent = context.Request.Headers["User-Agent"].ToString(); + if (BadBots.Any(bot => userAgent.Contains(bot, StringComparison.OrdinalIgnoreCase))) + { + context.Response.StatusCode = 403; + await context.Response.WriteAsync("Forbidden"); + return; + } + + await _next(context); + } + + #endregion Public Methods + } +} \ No newline at end of file diff --git a/CatherineLynwood/Middleware/RedirectToWwwMiddleware.cs b/CatherineLynwood/Middleware/RedirectToWwwMiddleware.cs index 31ea830..d9479fc 100644 --- a/CatherineLynwood/Middleware/RedirectToWwwMiddleware.cs +++ b/CatherineLynwood/Middleware/RedirectToWwwMiddleware.cs @@ -3,22 +3,30 @@ public class RedirectToWwwMiddleware { private readonly RequestDelegate _next; + private IWebHostEnvironment _environment; - public RedirectToWwwMiddleware(RequestDelegate next) + public RedirectToWwwMiddleware(RequestDelegate next, IWebHostEnvironment environment) { _next = next; + _environment = environment; } public async Task InvokeAsync(HttpContext context) { var host = context.Request.Host.Host; - if (host.Equals("catherinelynwood.com", System.StringComparison.OrdinalIgnoreCase)) + var schema = context.Request.Scheme; + + if (_environment.IsProduction()) { - var newUrl = $"https://www.catherinelynwood.com{context.Request.Path}{context.Request.QueryString}"; - context.Response.Redirect(newUrl, permanent: true); - return; // End the middleware pipeline. + if (host.Equals("catherinelynwood.com", StringComparison.OrdinalIgnoreCase) || schema.Equals("http", StringComparison.OrdinalIgnoreCase)) + { + var newUrl = $"https://www.catherinelynwood.com{context.Request.Path}{context.Request.QueryString}"; + context.Response.Redirect(newUrl, permanent: true); + return; // End the middleware pipeline. + } } + // Continue to the next middleware. await _next(context); } diff --git a/CatherineLynwood/Models/ChapterAudioSegment.cs b/CatherineLynwood/Models/ChapterAudioSegment.cs new file mode 100644 index 0000000..31f949e --- /dev/null +++ b/CatherineLynwood/Models/ChapterAudioSegment.cs @@ -0,0 +1,23 @@ +namespace CatherineLynwood.Models +{ + public class ChapterAudioSegment + { + #region Public Properties + + public string ChapterFolder { get; set; } = default!; + + public string Display { get; set; } = default!; + + public TimeSpan Duration { get; set; } + + public string File { get; set; } = default!; + + public string Id { get; set; } = default!; + + public string? Text { get; set; } + + public string Url { get; set; } = default!; + + #endregion Public Properties + } +} \ No newline at end of file diff --git a/CatherineLynwood/Models/ChapterPlayerViewModel.cs b/CatherineLynwood/Models/ChapterPlayerViewModel.cs new file mode 100644 index 0000000..81652b6 --- /dev/null +++ b/CatherineLynwood/Models/ChapterPlayerViewModel.cs @@ -0,0 +1,13 @@ +namespace CatherineLynwood.Models +{ + public class ChapterPlayerViewModel + { + #region Public Properties + + public int ChapterNumber { get; set; } + + public string ChapterTitle { get; set; } + + #endregion Public Properties + } +} \ No newline at end of file diff --git a/CatherineLynwood/Program.cs b/CatherineLynwood/Program.cs index 281867a..0c8f786 100644 --- a/CatherineLynwood/Program.cs +++ b/CatherineLynwood/Program.cs @@ -38,6 +38,12 @@ namespace CatherineLynwood // ✅ Register the book access code service builder.Services.AddScoped(); + builder.Services.AddSingleton(); + builder.Services.AddHostedService(); + builder.Services.AddSingleton(); + + + // Add response compression services builder.Services.AddResponseCompression(options => { @@ -76,7 +82,8 @@ namespace CatherineLynwood app.UseHsts(); // Adds the HSTS (HTTP Strict Transport Security) header } - app.UseMiddleware(); + app.UseMiddleware(); + app.UseMiddleware(); app.UseMiddleware(); app.UseMiddleware(); app.UseMiddleware(); diff --git a/CatherineLynwood/Services/AudioTokenService.cs b/CatherineLynwood/Services/AudioTokenService.cs new file mode 100644 index 0000000..4c7245f --- /dev/null +++ b/CatherineLynwood/Services/AudioTokenService.cs @@ -0,0 +1,41 @@ +using System.Security.Cryptography; +using System.Text; + +namespace CatherineLynwood.Services +{ + public class AudioTokenService + { + private readonly string _secret; + private readonly int _expirySeconds; + + public AudioTokenService(IConfiguration config) + { + _secret = config["AudioSecurity:HmacSecretKey"]; + _expirySeconds = int.Parse(config["AudioSecurity:TokenExpirySeconds"]); + } + + public string GenerateToken(string id, long expires) + { + var payload = $"{id}:{expires}"; + using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_secret)); + var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload)); + return Convert.ToBase64String(hash); + } + + public bool IsValid(string id, long expires, string token) + { + if (DateTimeOffset.UtcNow.ToUnixTimeSeconds() > expires) + return false; + + var expected = GenerateToken(id, expires); + return expected == token; + } + + public string GenerateUrl(string id) + { + var expires = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + _expirySeconds; + var token = GenerateToken(id, expires); + return $"/api/audio/stream/{id}?expires={expires}&token={Uri.EscapeDataString(token)}"; + } + } +} diff --git a/CatherineLynwood/Services/ChapterAudioMapCache.cs b/CatherineLynwood/Services/ChapterAudioMapCache.cs new file mode 100644 index 0000000..9583666 --- /dev/null +++ b/CatherineLynwood/Services/ChapterAudioMapCache.cs @@ -0,0 +1,29 @@ +using CatherineLynwood.Models; + +namespace CatherineLynwood.Services +{ + public class ChapterAudioMapCache + { + #region Private Fields + + private readonly Dictionary _idMap = new(); + + #endregion Private Fields + + #region Public Methods + + public void Add(ChapterAudioSegment segment) + { + _idMap[segment.Id] = segment; + } + + public ChapterAudioSegment? Get(string id) + { + return _idMap.TryGetValue(id, out var segment) ? segment : null; + } + + public IReadOnlyCollection GetAll() => _idMap.Values; + + #endregion Public Methods + } +} \ No newline at end of file diff --git a/CatherineLynwood/Services/ChapterAudioMapService.cs b/CatherineLynwood/Services/ChapterAudioMapService.cs new file mode 100644 index 0000000..a1486e3 --- /dev/null +++ b/CatherineLynwood/Services/ChapterAudioMapService.cs @@ -0,0 +1,130 @@ +using NAudio.Wave; +using CatherineLynwood.Models; + +using System.Text.RegularExpressions; + +namespace CatherineLynwood.Services +{ + public class ChapterAudioMapService : IHostedService + { + #region Private Fields + + private readonly AudioTokenService _audioTokenService; + private readonly ChapterAudioMapCache _cache; + private readonly IWebHostEnvironment _env; + private readonly ILogger _logger; + + #endregion Private Fields + + #region Public Constructors + + public ChapterAudioMapService( + IWebHostEnvironment env, + ILogger logger, + ChapterAudioMapCache cache, + AudioTokenService audioTokenService) + { + _env = env; + _logger = logger; + _cache = cache; + _audioTokenService = audioTokenService; + } + + #endregion Public Constructors + + #region Public Methods + + public Task StartAsync(CancellationToken cancellationToken) + { + var chaptersPath = Path.Combine(_env.WebRootPath, "chapters"); + + if (!Directory.Exists(chaptersPath)) + { + _logger.LogWarning("Chapters directory not found at {Path}", chaptersPath); + return Task.CompletedTask; + } + + foreach (var chapterDir in Directory.GetDirectories(chaptersPath)) + { + var folderName = Path.GetFileName(chapterDir); + var match = Regex.Match(folderName, @"Chapter_(\d+)", RegexOptions.IgnoreCase); + + if (!match.Success) + { + _logger.LogWarning("Skipping folder {Folder} — no chapter number found", folderName); + continue; + } + + var textFile = Directory.GetFiles(chapterDir, "Chapter*.txt").FirstOrDefault(); + List textLines = []; + + if (textFile != null) + { + textLines = File.ReadAllLines(textFile) + .Where(line => !string.IsNullOrWhiteSpace(line)) + .ToList(); + } + + else + { + _logger.LogWarning("No text file found in {Folder}", chapterDir); + } + + + var files = Directory.GetFiles(chapterDir, "*.mp3") + .OrderBy(f => f) + .ToList(); + + for (int i = 0; i < files.Count; i++) + { + var id = Guid.NewGuid().ToString("N"); + var filename = Path.GetFileName(files[i]); + + var duration = TimeSpan.Zero; + try + { + using var reader = new Mp3FileReader(files[i]); + duration = reader.TotalTime; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Could not read duration for {File}", filename); + } + + var text = i < textLines.Count ? textLines[i] : null; + + var segment = new ChapterAudioSegment + { + Id = id, + File = filename, + ChapterFolder = folderName, + Duration = duration, + Text = text + }; + + _cache.Add(segment); + } + + + _logger.LogInformation("Cached {Count} segments for {Folder}", files.Count, folderName); + } + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + #endregion Public Methods + + #region Private Methods + + private string FormatDuration(TimeSpan duration) + { + return duration.TotalHours >= 1 + ? duration.ToString(@"hh\:mm\:ss") + : duration.ToString(@"mm\:ss"); + } + + #endregion Private Methods + } +} \ No newline at end of file diff --git a/CatherineLynwood/TagHelpers/MetaTagHelper.cs b/CatherineLynwood/TagHelpers/MetaTagHelper.cs index 44e31fe..f43d4ad 100644 --- a/CatherineLynwood/TagHelpers/MetaTagHelper.cs +++ b/CatherineLynwood/TagHelpers/MetaTagHelper.cs @@ -1,36 +1,56 @@ -using System.Text.Json; - -using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.AspNetCore.Razor.TagHelpers; namespace CatherineLynwood.TagHelpers { [HtmlTargetElement("MetaTag")] public class MetaTagHelper : TagHelper { - // Shared properties - public string MetaTitle { get; set; } - public string MetaDescription { get; set; } - public string MetaKeywords { get; set; } - public string MetaAuthor { get; set; } - public string MetaUrl { get; set; } - public string MetaImage { get; set; } - public string MetaImageAlt { get; set; } + #region Public Properties - // Open Graph specific properties - public string OgSiteName { get; set; } - public string OgType { get; set; } = "article"; + public DateTime? ArticleModifiedTime { get; set; } // Article specific properties public DateTime? ArticlePublishedTime { get; set; } - public DateTime? ArticleModifiedTime { get; set; } + + public string MetaAuthor { get; set; } + + public string MetaDescription { get; set; } + + public string MetaImage { get; set; } + + public string MetaImageAlt { get; set; } + + public string MetaKeywords { get; set; } + + // Shared properties + public string MetaTitle { get; set; } + + public string MetaUrl { get; set; } + + // Open Graph specific properties + public string OgSiteName { get; set; } + + public string OgType { get; set; } = "article"; // Twitter specific properties public string TwitterCardType { get; set; } = "summary_large_image"; - public string TwitterSiteHandle { get; set; } + public string TwitterCreatorHandle { get; set; } - public int? TwitterPlayerWidth { get; set; } + public int? TwitterPlayerHeight { get; set; } + public int? TwitterPlayerWidth { get; set; } + + public string TwitterSiteHandle { get; set; } + + public string TwitterVideoUrl { get; set; } + + #endregion Public Properties + + // Optional: a full URL to a video (e.g., MP4 or embedded player) + + #region Public Methods + public override void Process(TagHelperContext context, TagHelperOutput output) { output.TagName = null; // Suppress output tag @@ -79,55 +99,29 @@ namespace CatherineLynwood.TagHelpers metaTags.AppendLine($""); if (!string.IsNullOrWhiteSpace(MetaImageAlt)) metaTags.AppendLine($""); - if (!string.IsNullOrWhiteSpace(MetaUrl)) + if (!string.IsNullOrWhiteSpace(TwitterVideoUrl)) { - metaTags.AppendLine($""); + metaTags.AppendLine(""); + metaTags.AppendLine($""); if (TwitterPlayerWidth.HasValue) metaTags.AppendLine($""); if (TwitterPlayerHeight.HasValue) metaTags.AppendLine($""); + + if (!string.IsNullOrWhiteSpace(MetaImage)) + metaTags.AppendLine($""); } - - // JSON-LD for schema.org - var jsonLd = new + else { - @context = "https://schema.org", - @type = "WebPage", - name = MetaTitle, - description = MetaDescription, - url = MetaUrl, - author = new - { - @type = "Person", - name = MetaAuthor, - url = MetaUrl - }, - publisher = new - { - @type = "Organization", - name = MetaAuthor, - url = MetaUrl - }, - mainEntityOfPage = new - { - @type = "WebPage", - @id = MetaUrl - }, - inLanguage = "en", - datePublished = ArticlePublishedTime?.ToString("yyyy-MM-dd"), - dateModified = ArticleModifiedTime?.ToString("yyyy-MM-dd"), - headline = MetaTitle - }; - - string jsonLdString = JsonSerializer.Serialize(jsonLd, new JsonSerializerOptions - { - WriteIndented = true - }); - - metaTags.AppendLine($""); + metaTags.AppendLine($""); + if (!string.IsNullOrWhiteSpace(MetaImage)) + metaTags.AppendLine($""); + } // Output all content output.Content.SetHtmlContent(metaTags.ToString()); } + + #endregion Public Methods } -} +} \ No newline at end of file diff --git a/CatherineLynwood/TagHelpers/ResponsiveImageTagHelper.cs b/CatherineLynwood/TagHelpers/ResponsiveImageTagHelper.cs index 7796b44..f88b2b7 100644 --- a/CatherineLynwood/TagHelpers/ResponsiveImageTagHelper.cs +++ b/CatherineLynwood/TagHelpers/ResponsiveImageTagHelper.cs @@ -30,6 +30,10 @@ namespace CatherineLynwood.TagHelpers [HtmlAttributeName("display-width-percentage")] public int DisplayWidthPercentage { get; set; } = 100; + [HtmlAttributeName("no-index")] + public bool NoIndex { get; set; } + + private readonly string[] _resolutions = { "1920", "1400", "1200", "992", "768", "576" }; private readonly string _wwwRoot = "wwwroot"; @@ -87,7 +91,10 @@ namespace CatherineLynwood.TagHelpers } // Add the default WebP fallback in the tag (JPEG is not included in HTML) - output.Content.AppendHtml($"{AltText}"); + string noIndexAttr = NoIndex ? " data-nosnippet" : ""; + output.Content.AppendHtml( + $"{AltText}"); + } diff --git a/CatherineLynwood/TagHelpers/SocialMediaShareTagHelper.cs b/CatherineLynwood/TagHelpers/SocialMediaShareTagHelper.cs index bc22c5f..99177c3 100644 --- a/CatherineLynwood/TagHelpers/SocialMediaShareTagHelper.cs +++ b/CatherineLynwood/TagHelpers/SocialMediaShareTagHelper.cs @@ -22,18 +22,18 @@ namespace CatherineLynwood.TagHelpers var shareHtml = $@" "; var followHtml = @" "; output.TagName = "div"; diff --git a/CatherineLynwood/Views/TheAlphaFlame/Chapter1.cshtml b/CatherineLynwood/Views/Discovery/Chapter1.cshtml similarity index 97% rename from CatherineLynwood/Views/TheAlphaFlame/Chapter1.cshtml rename to CatherineLynwood/Views/Discovery/Chapter1.cshtml index 0887905..c7c5dc9 100644 --- a/CatherineLynwood/Views/TheAlphaFlame/Chapter1.cshtml +++ b/CatherineLynwood/Views/Discovery/Chapter1.cshtml @@ -8,7 +8,7 @@ @@ -77,6 +77,12 @@ +@section Scripts{ + +} + @section Meta { - + @@ -93,6 +93,12 @@ +@section Scripts{ + +} + @section Meta { - + @@ -76,6 +76,12 @@ +@section Scripts{ + +} + @section Meta { - - + + @@ -26,12 +26,8 @@
-
- -
+ +
diff --git a/CatherineLynwood/Views/TheAlphaFlame/Extras.cshtml b/CatherineLynwood/Views/Discovery/Extras.cshtml similarity index 67% rename from CatherineLynwood/Views/TheAlphaFlame/Extras.cshtml rename to CatherineLynwood/Views/Discovery/Extras.cshtml index 501d9f4..ad44762 100644 --- a/CatherineLynwood/Views/TheAlphaFlame/Extras.cshtml +++ b/CatherineLynwood/Views/Discovery/Extras.cshtml @@ -11,6 +11,7 @@ @@ -20,9 +21,71 @@

Your Exclusive Extras

- @if (accessLevel >= 1) + @if (accessBook == 1) {
+ @if (accessLevel >= 1) + { +
+
+
Epilogue
+

Immerse yourself in the Eplilogue to The Alpha Flame: Discovery. Join the team as they relax for a quite drink at the Barnt Green Inn

+ Read or Listen +
+
+ } + @if (accessLevel >= 2) + { +
+
+
Discovery Scrap Book
+

Take a look at my image scrapbook for The Alpha Flame: Discovery. View the images I used for inspiration when writing the various scenes within the book.

+ View Scrapbook +
+
+ } + @if (accessLevel >= 3) + { +
+
+
Listen to The Alpha Flame: Discovery
+

Because you've purchased a premium physical copy of The Alpha Flame: Discovery, for a limited time this entitles you to listen to the audio version with no extra charge.

+ Listen to the Book +
+
+ } + @if (accessLevel >= 4) + { +
+
+
Scrapbook: Maggie’s Designs
+

Flip through Maggie’s sketches, fashion notes, and photos from her original designs – including the infamous red skirt.

+ View Scrapbook +
+
+ } +
+ } + else if (accessBook == 2) + { + +
+ @if (accessLevel >= 1) + { + + } + else if (accessLevel >= 2) + { + + } + else if (accessLevel >= 3) + { + + } + else if(accessLevel >= 4) + { + + }
Epilogue
@@ -57,44 +120,7 @@
} - else if (accessLevel >= 2) - { -
-
-
-
Epilogue
-

Immerse yourself in the Eplilogue to The Alpha Flame: Discovery. Join the team as they relax for a quite drink at the Barnt Green Inn

- Read or Listen -
-
- -
-
-
Discovery Scrap Book
-

Take a look at my image scrapbook for The Alpha Flame: Discovery. View the images I used for inspiration when writing the various scenes within the book.

- View Scrapbook -
-
- -
-
-
Rubery Hill Photo Archive
-

Explore historical photos and floor plans of the real Rubery Hill Hospital, the eerie inspiration behind key scenes.

- Explore -
-
- -
-
-
Scrapbook: Maggie’s Designs
-

Flip through Maggie’s sketches, fashion notes, and photos from her original designs – including the infamous red skirt.

- View Scrapbook -
-
- -
- } - else if (accessLevel >= 3) + else if (accessBook == 3) {
diff --git a/CatherineLynwood/Views/TheAlphaFlame/Discovery.cshtml b/CatherineLynwood/Views/Discovery/Index.cshtml similarity index 99% rename from CatherineLynwood/Views/TheAlphaFlame/Discovery.cshtml rename to CatherineLynwood/Views/Discovery/Index.cshtml index 24e952b..fb28242 100644 --- a/CatherineLynwood/Views/TheAlphaFlame/Discovery.cshtml +++ b/CatherineLynwood/Views/Discovery/Index.cshtml @@ -145,6 +145,11 @@ } }); + + + +} diff --git a/CatherineLynwood/Views/Discovery/MaggiesDesigns.cshtml b/CatherineLynwood/Views/Discovery/MaggiesDesigns.cshtml index 3c60609..b382be4 100644 --- a/CatherineLynwood/Views/Discovery/MaggiesDesigns.cshtml +++ b/CatherineLynwood/Views/Discovery/MaggiesDesigns.cshtml @@ -7,8 +7,8 @@ diff --git a/CatherineLynwood/Views/Discovery/ScrapBook.cshtml b/CatherineLynwood/Views/Discovery/ScrapBook.cshtml index 93daa4e..4adbd7a 100644 --- a/CatherineLynwood/Views/Discovery/ScrapBook.cshtml +++ b/CatherineLynwood/Views/Discovery/ScrapBook.cshtml @@ -7,8 +7,8 @@ diff --git a/CatherineLynwood/Views/Home/Index.cshtml b/CatherineLynwood/Views/Home/Index.cshtml index 39f3d3a..d3ef670 100644 --- a/CatherineLynwood/Views/Home/Index.cshtml +++ b/CatherineLynwood/Views/Home/Index.cshtml @@ -15,7 +15,7 @@
@@ -39,8 +39,8 @@ When Maggie Grant finds Beth, bruised, terrified, and alone, she has no idea the rescue will ignite a chain of secrets buried deep in her own past...

The Alpha Flame: Discovery, writen by Catherine Lynwood, is the first in a powerful trilogy following the tangled lives of Maggie and Beth , two women bound by fate, fire, and secrets too dangerous to stay buried. @@ -53,87 +53,95 @@ @section Meta { - - - - - - - - - + + + + + + + + +} + @section Meta { diff --git a/CatherineLynwood/Views/Shared/_Layout.cshtml b/CatherineLynwood/Views/Shared/_Layout.cshtml index 49dc52b..fc0667a 100644 --- a/CatherineLynwood/Views/Shared/_Layout.cshtml +++ b/CatherineLynwood/Views/Shared/_Layout.cshtml @@ -9,7 +9,7 @@ - +