Compare commits

...

10 Commits

Author SHA1 Message Date
Nick Beckley
85924f63d3 Added Reckoning Index pages 2026-03-30 21:51:43 +01:00
Nick Beckley
fb4e139d9f Save 2026-02-10 20:40:52 +00:00
Nick
8215d83f63 Save 2025-09-17 09:57:12 +01:00
Nick
3ce32496e8 Save 2025-09-13 21:04:48 +01:00
Nick
4759fbbd69 Refactor buy link handling and add geo-resolution features
- Refactored `BuyController` for improved click logging and centralized link management.
- Updated `DiscoveryController` to enhance user request handling and added country context dependency.
- Introduced `ClicksController` for managing click tracking and redirects.
- Added `GeoResolutionMiddleware` to resolve user locations based on IP.
- Created `BuyCatalog` for centralized management of buy links.
- Introduced view models (`BuyLinksViewModel`, `DiscoveryPageViewModel`) for better data handling in views.
- Added new files for geographical data handling (`GeoIpResult`, `HttpContextItemKeys`, `LinkChoice`, etc.).
- Updated `_BuyBox.cshtml` to render buy options based on user location.
- Modified `DataAccess` for saving and retrieving geographical data.
2025-09-12 22:01:09 +01:00
Nick
b3cc5ccedd Enhance metadata handling in MetaTagHelper and views
Updated MetaTagHelper.cs to add new metadata properties including ArticlePublishedTime, MetaDescription, and others. Refactored JavaScript in Index1.cshtml for improved readability and added functionality for video playback. Updated metadata attributes in Index.cshtml and Index1.cshtml to support new properties, ensuring better SEO and social media sharing capabilities.
2025-09-10 21:05:51 +01:00
Nick
82cb5cde02 Add new background images for synopsis sections
This commit introduces several new background images in WebP format, including `synopsis-background-288.webp`, `synopsis-background-384.webp`, `synopsis-background-400.webp`, `synopsis-background-496.webp`, `synopsis-background-600.webp`, and `synopsis-background-700.webp`. Each image includes embedded metadata for creation and modification details.

These images are intended to enhance the visual presentation of the synopsis section within the application, improving the overall aesthetic appeal and user engagement.
2025-09-10 10:38:50 +01:00
Nick
afe3c6cc78 Save 2025-09-07 21:31:16 +01:00
Nick
31ae1e80ea Save 2025-09-06 23:01:22 +01:00
Nick
f0e5052e2c Save 2025-08-11 20:19:49 +01:00
1259 changed files with 26683 additions and 1457 deletions

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
@ -153,19 +153,19 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Humanizer.Core" Version="2.14.1" /> <PackageReference Include="Humanizer.Core" Version="3.0.10" />
<PackageReference Include="MailKit" Version="4.13.0" /> <PackageReference Include="MailKit" Version="4.15.1" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.2" /> <PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.0" />
<PackageReference Include="Microsoft.Web.Administration" Version="11.1.0" /> <PackageReference Include="Microsoft.Web.Administration" Version="11.1.0" />
<PackageReference Include="NAudio" Version="2.2.1" /> <PackageReference Include="NAudio" Version="2.3.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="SixLabors.Fonts" Version="2.1.3" /> <PackageReference Include="SixLabors.Fonts" Version="2.1.3" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.8" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.6" /> <PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.7" />
<PackageReference Include="SixLabors.ImageSharp.Web" Version="3.1.5" /> <PackageReference Include="SixLabors.ImageSharp.Web" Version="3.2.0" />
<PackageReference Include="System.Drawing.Common" Version="9.0.5" /> <PackageReference Include="System.Drawing.Common" Version="10.0.5" />
<PackageReference Include="System.Net.Http" Version="4.3.4" /> <PackageReference Include="System.Net.Http" Version="4.3.4" />
<PackageReference Include="WebMarkupMin.AspNetCoreLatest" Version="2.19.0" /> <PackageReference Include="WebMarkupMin.AspNetCoreLatest" Version="2.21.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,41 @@
using CatherineLynwood.Models;
using CatherineLynwood.Services;
using Microsoft.AspNetCore.Mvc;
namespace CatherineLynwood.Components
{
public class BuyPanel : ViewComponent
{
#region Private Fields
private DataAccess _dataAccess;
#endregion Private Fields
#region Public Constructors
public BuyPanel(DataAccess dataAccess)
{
_dataAccess = dataAccess;
}
#endregion Public Constructors
#region Public Methods
public async Task<IViewComponentResult> InvokeAsync(string iso2, string src, string title)
{
BuyPanelViewModel buyPanelViewModel = await _dataAccess.GetBuyPanelViewModel(iso2, title);
buyPanelViewModel.Src = src;
if (title == "Reckoning")
{
return View("Reckoning", buyPanelViewModel);
}
return View(buyPanelViewModel);
}
#endregion Public Methods
}
}

View File

@ -0,0 +1,95 @@
using CatherineLynwood.Models;
using CatherineLynwood.Services;
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics.Metrics;
namespace CatherineLynwood.Controllers
{
[Route("")]
public class ClicksController : Controller
{
private readonly DataAccess _dataAccess;
private readonly ICountryContext _country;
public ClicksController(DataAccess dataAccess, ICountryContext country)
{
_dataAccess = dataAccess;
_country = country;
}
// ---------------------------------------------------------------------
// POST /track/click?slug=amazon-paperback-gb&src=discovery
// Receives <a ping="..."> pings and records them (204 No Content).
// ---------------------------------------------------------------------
[HttpPost("track/click")]
public async Task<IActionResult> TrackClick([FromQuery] string slug, [FromQuery] string src = "discovery")
{
bool logClick = true;
var ua = Request.Headers["User-Agent"].ToString() ?? string.Empty;
var referer = Request.Headers["Referer"].ToString() ?? string.Empty;
var ip = HttpContext.Connection.RemoteIpAddress?.ToString();
var qs = Request.QueryString.HasValue ? Request.QueryString.Value : string.Empty;
var iso2 = (_country.Iso2 ?? "UK").ToUpperInvariant();
if (iso2 == "GB") iso2 = "UK";
if (HttpMethods.IsHead(Request.Method)) logClick = false; // don't log HEAD requests
if (IsLikelyBot(ua)) logClick = false; // don't log likely bots
if (logClick)
{
EnsureSidCookie(out var sessionId);
_ = await _dataAccess.SaveBuyClick(
dateTimeUtc: DateTime.UtcNow,
slug: slug,
countryGroup: iso2,
ip: ip,
country: Request.Query["country"].ToString(),
userAgent: ua,
referer: referer,
sessionId: sessionId,
src: src
);
}
return NoContent();
}
// ---------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------
private void EnsureSidCookie(out string sessionId)
{
sessionId = Request.Cookies["sid"]!;
if (!string.IsNullOrEmpty(sessionId)) return;
sessionId = Guid.NewGuid().ToString("N");
Response.Cookies.Append("sid", sessionId, new CookieOptions
{
HttpOnly = true,
SameSite = SameSiteMode.Lax,
Secure = true,
MaxAge = TimeSpan.FromDays(365),
Path = "/"
});
}
private static bool IsLikelyBot(string ua)
{
if (string.IsNullOrWhiteSpace(ua)) return true; // empty UA, treat as bot
var needles = new[]
{
"bot","crawler","spider","preview","fetch","scan","analyzer",
"httpclient","python-requests","facebookexternalhit","slackbot",
"whatsapp","telegrambot","googlebot","adsbot","amazonbot",
"bingbot","duckduckbot","yandexbot","ahrefsbot","semrushbot",
"applebot","linkedinbot","discordbot","embedly","pinterestbot"
};
ua = ua.ToLowerInvariant();
return needles.Any(n => ua.Contains(n));
}
}
}

View File

@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Mvc;
namespace CatherineLynwood.Controllers
{
[Route("collaborations")]
public class CollaborationsController : Controller
{
public IActionResult Index()
{
return View();
}
[Route("larhysa-saddul")]
public IActionResult LarhysaSaddul()
{
return View();
}
}
}

View File

@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json; using Newtonsoft.Json;
using System.Globalization; using System.Globalization;
using System.Security.Cryptography;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace CatherineLynwood.Controllers namespace CatherineLynwood.Controllers
@ -13,97 +14,277 @@ namespace CatherineLynwood.Controllers
[Route("the-alpha-flame/discovery")] [Route("the-alpha-flame/discovery")]
public class DiscoveryController : Controller public class DiscoveryController : Controller
{ {
#region Private Fields private readonly DataAccess _dataAccess;
private readonly ICountryContext _country;
private DataAccess _dataAccess; // A/B/C constants
private const string VariantCookie = "af_disc_abc";
private const string VariantQuery = "ab"; // ?ab=A or B or C
#endregion Private Fields public DiscoveryController(DataAccess dataAccess, ICountryContext country)
#region Public Constructors
public DiscoveryController(DataAccess dataAccess)
{ {
_dataAccess = dataAccess; _dataAccess = dataAccess;
} _country = country;
#endregion Public Constructors
#region Public Methods
[Route("chapters/chapter-1-beth")]
public IActionResult Chapter1()
{
return View();
}
[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();
}
[BookAccess(1, 1)]
[Route("extras")]
public IActionResult Extras()
{
return View();
} }
[Route("")] [Route("")]
public async Task<IActionResult> Index() public async Task<IActionResult> Index()
{ {
Reviews reviews = await _dataAccess.GetReviewsAsync(); // Prevent caches or CDNs from cross-serving variants
Response.Headers["Vary"] = "Cookie, User-Agent";
// Decide device class first
var device = ResolveDeviceClass();
// Decide variant
var variant = ResolveVariant(device);
// Country ISO2
var iso2 = (_country.Iso2 ?? "GB").ToUpperInvariant();
if (iso2 == "UK") iso2 = "GB";
// Reviews
Reviews reviews = await _dataAccess.GetReviewsAsync("Discovery");
reviews.SchemaJsonLd = GenerateBookSchemaJsonLd(reviews, 3); reviews.SchemaJsonLd = GenerateBookSchemaJsonLd(reviews, 3);
return View(reviews); string src = variant switch
{
Variant.A => "DiscoveryA",
Variant.B => "DiscoveryA",
_ => "Desktop"
};
// VM
var vm = new TitlePageViewModel
{
Reviews = reviews,
UserIso2 = iso2,
Src = src,
Title = "Discovery"
};
// View mapping:
// - A and B are the two MOBILE variants
// - C is the DESKTOP variant
string viewName = variant switch
{
Variant.A => "IndexMobileA", // mobile layout A
Variant.B => "IndexMobileA", // mobile layout B
_ => "IndexDesktop" // desktop layout C
};
return View(viewName, vm);
} }
[Route("reviews")] [Route("reviews")]
public async Task<IActionResult> Reviews() public async Task<IActionResult> Reviews()
{ {
Reviews reviews = await _dataAccess.GetReviewsAsync(); Reviews reviews = await _dataAccess.GetReviewsAsync("Discovery");
reviews.SchemaJsonLd = GenerateBookSchemaJsonLd(reviews, 100); reviews.SchemaJsonLd = GenerateBookSchemaJsonLd(reviews, 100);
return View(reviews); return View(reviews);
} }
[BookAccess(1, 1)] [Route("audio-book")]
[Route("extras/listen")] public IActionResult AudioBook() => View();
public IActionResult Listen()
[Route("how-to-buy")]
public IActionResult HowToBuy() => View();
[Route("chapters/chapter-1-beth")]
public IActionResult Chapter1() => View();
[Route("chapters/chapter-2-maggie")]
public IActionResult Chapter2() => View();
[Route("chapters/chapter-13-susie")]
public IActionResult Chapter13() => View();
[Route("trailer")]
public async Task<IActionResult> Trailer()
{ {
return View(); // Country ISO2
var iso2 = (_country.Iso2 ?? "UK").ToUpperInvariant();
if (iso2 == "GB") iso2 = "UK";
FlagSupportViewModel flagSupportViewModel = await _dataAccess.GetTrailerLikes();
foreach (var flag in flagSupportViewModel.FlagCounts)
{
flag.Selected = flag.Key.ToLower() == iso2.ToLower();
}
return View(flagSupportViewModel);
} }
[BookAccess(1, 1)] [BookAccess(1, 1)]
[Route("extras/maggies-designs")] [Route("extras")]
public IActionResult MaggiesDesigns() public IActionResult Extras() => View();
{
return View(); [BookAccess(1, 1)]
} [Route("extras/epilogue")]
public IActionResult Epilogue() => View();
[BookAccess(1, 1)] [BookAccess(1, 1)]
[Route("extras/scrap-book")] [Route("extras/scrap-book")]
public IActionResult ScrapBook() public IActionResult ScrapBook() => View();
[BookAccess(1, 1)]
[Route("extras/listen")]
public IActionResult Listen() => View();
[BookAccess(1, 1)]
[Route("extras/maggies-designs")]
public IActionResult MaggiesDesigns() => View();
[BookAccess(1, 1)]
[Route("extras/soundtrack")]
public async Task<IActionResult> Soundtrack()
{ {
return View(); List<SoundtrackTrackModel> soundtrackTrackModels = await _dataAccess.GetSoundtrack();
return View(soundtrackTrackModels);
} }
#endregion Public Methods // =========================
// A/B/C selection
// =========================
#region Private Methods private enum Variant { A, B, C }
private enum DeviceClass { Mobile, Desktop }
private Variant ResolveVariant(DeviceClass device)
{
// 0) Query override: ?ab=A|B|C forces and persists
if (Request.Query.TryGetValue(VariantQuery, out var qs))
{
var parsed = ParseVariant(qs.ToString());
if (parsed.HasValue)
{
// If they force A or B but device is desktop, we still respect C logic by default.
// Allow override to win for testing convenience.
WriteVariantCookie(parsed.Value);
return parsed.Value;
}
}
// 1) Existing cookie
if (Request.Cookies.TryGetValue(VariantCookie, out var cookieVal))
{
var parsed = ParseVariant(cookieVal);
if (parsed.HasValue)
{
// If cookie says A/B but device is desktop, upgrade to C to keep desktop experience consistent.
if (device == DeviceClass.Desktop && parsed.Value != Variant.C)
{
WriteVariantCookie(Variant.C);
return Variant.C;
}
// If cookie says C but device is mobile, you can either keep C or reassign to A/B.
// We keep C only for desktops. On mobile we reassess to A/B the first time after cookie mismatch.
if (device == DeviceClass.Mobile && parsed.Value == Variant.C)
{
var reassigned = AssignMobileVariant();
WriteVariantCookie(reassigned);
return reassigned;
}
return parsed.Value;
}
}
// 2) Fresh assignment
if (device == DeviceClass.Desktop)
{
WriteVariantCookie(Variant.C);
return Variant.C;
}
else
{
var assigned = AssignMobileVariant(); // A or B
WriteVariantCookie(assigned);
return assigned;
}
}
private static Variant AssignMobileVariant(int bPercent = 50)
{
bPercent = Math.Clamp(bPercent, 0, 100);
var roll = RandomNumberGenerator.GetInt32(0, 100); // 0..99
return roll < bPercent ? Variant.B : Variant.A;
}
private static Variant? ParseVariant(string value)
{
if (string.Equals(value, "A", StringComparison.OrdinalIgnoreCase)) return Variant.A;
if (string.Equals(value, "B", StringComparison.OrdinalIgnoreCase)) return Variant.B;
if (string.Equals(value, "C", StringComparison.OrdinalIgnoreCase)) return Variant.C;
return null;
}
private void WriteVariantCookie(Variant variant)
{
var opts = new CookieOptions
{
Expires = DateTimeOffset.UtcNow.AddDays(90),
HttpOnly = false, // set true if you do not need analytics to read it
Secure = true,
SameSite = SameSiteMode.Lax,
Path = "/"
};
Response.Cookies.Append(VariantCookie, variant.ToString(), opts);
}
// =========================
// Device detection
// =========================
private DeviceClass ResolveDeviceClass()
{
// Simple and robust server-side approach using User-Agent.
// Chrome UA reduction still leaves Mobile hint for Android; iOS strings include iPhone/iPad.
var ua = Request.Headers.UserAgent.ToString();
if (IsMobileUserAgent(ua))
return DeviceClass.Mobile;
return DeviceClass.Desktop;
}
private static bool IsMobileUserAgent(string ua)
{
if (string.IsNullOrEmpty(ua)) return false;
// Common mobile indicators
// Android phones include "Android" and "Mobile"
// iPhone includes "iPhone"; iPad includes "iPad" (treat as mobile for your layouts)
// Many mobile browsers include "Mobile"
// Exclude obvious desktop platforms
ua = ua.ToLowerInvariant();
if (ua.Contains("ipad") || ua.Contains("iphone") || ua.Contains("ipod"))
return true;
if (ua.Contains("android") && ua.Contains("mobile"))
return true;
if (ua.Contains("android") && !ua.Contains("mobile"))
{
// Likely a tablet; treat as mobile for your use case
return true;
}
if (ua.Contains("mobile"))
return true;
// Desktop hints
if (ua.Contains("windows nt") || ua.Contains("macintosh") || ua.Contains("x11"))
return false;
// Fallback
return false;
}
// =========================
// Existing helpers
// =========================
private string GenerateBookSchemaJsonLd(Reviews reviews, int take) private string GenerateBookSchemaJsonLd(Reviews reviews, int take)
{ {
@ -129,22 +310,20 @@ namespace CatherineLynwood.Controllers
["name"] = "Catherine Lynwood" ["name"] = "Catherine Lynwood"
}, },
["datePublished"] = "2025-08-21", ["datePublished"] = "2025-08-21",
["description"] = "The Alpha Flame: Discovery is a powerful, character-driven novel set in 1983 Birmingham, following Maggie Grant and Bethtwo young women separated by fate, reunited by truth, and bound by secrets...", ["description"] = "The Alpha Flame: Discovery is a powerful, character-driven novel set in 1983 Birmingham, following Maggie Grant and Beth, two young women separated by fate, reunited by truth, and bound by secrets...",
["genre"] = "Women's Fiction, Mystery, Contemporary Historical", ["genre"] = "Women's Fiction, Mystery, Contemporary Historical",
["inLanguage"] = "en-GB", ["inLanguage"] = "en-GB",
["url"] = baseUrl ["url"] = baseUrl
}; };
// Add review section if there are reviews
if (reviews?.Items?.Any() == true) if (reviews?.Items?.Any() == true)
{ {
var reviewObjects = new List<Dictionary<string, object>>(); var reviewObjects = new List<Dictionary<string, object>>();
double total = 0; double total = 0;
foreach (var review in reviews.Items) total += review.RatingValue;
foreach (var review in reviews.Items.Take(take)) foreach (var review in reviews.Items.Take(take))
{ {
total += review.RatingValue;
reviewObjects.Add(new Dictionary<string, object> reviewObjects.Add(new Dictionary<string, object>
{ {
["@type"] = "Review", ["@type"] = "Review",
@ -173,7 +352,6 @@ namespace CatherineLynwood.Controllers
}; };
} }
// Add work examples
schema["workExample"] = new List<Dictionary<string, object>> schema["workExample"] = new List<Dictionary<string, object>>
{ {
new Dictionary<string, object> new Dictionary<string, object>
@ -245,9 +423,7 @@ namespace CatherineLynwood.Controllers
return JsonConvert.SerializeObject(schema, Formatting.Indented); return JsonConvert.SerializeObject(schema, Formatting.Indented);
} }
string StripHtml(string input) => string.IsNullOrWhiteSpace(input) ? string.Empty : Regex.Replace(input, "<.*?>", string.Empty); private static string StripHtml(string input) =>
string.IsNullOrWhiteSpace(input) ? string.Empty : Regex.Replace(input, "<.*?>", string.Empty);
#endregion Private Methods
} }
} }

View File

@ -1,4 +1,4 @@
using CatherineLynwood.Models; using CatherineLynwood.Models;
using CatherineLynwood.Services; using CatherineLynwood.Services;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -40,16 +40,39 @@ namespace CatherineLynwood.Controllers
return View(arcReaderApplicationModel); return View(arcReaderApplicationModel);
} }
[HttpGet("arc-reader-application/thanks")]
public IActionResult ArcThanks()
{
return View();
}
[HttpPost("arc-reader-application")] [HttpPost("arc-reader-application")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ArcReaderApplication(ArcReaderApplicationModel arcReaderApplicationModel) public async Task<IActionResult> ArcReaderApplication(ArcReaderApplicationModel arcReaderApplicationModel)
{ {
if (!ModelState.IsValid)
{
return View(arcReaderApplicationModel);
}
bool success = await _dataAccess.SaveARCReaderApplication(arcReaderApplicationModel); bool success = await _dataAccess.SaveARCReaderApplication(arcReaderApplicationModel);
if (success) if (success)
{ {
return RedirectToAction("ThankYou"); if (arcReaderApplicationModel.HasKindleAccess == "No")
{
await SendARCAltDeliveryEmailAsync(arcReaderApplicationModel);
}
else
{
await SendARCInstructionsEmailAsync(arcReaderApplicationModel);
} }
return RedirectToAction("ArcThanks");
}
// If saving fails unexpectedly
ModelState.AddModelError("", "There was an error submitting your application. Please try again later.");
return View(arcReaderApplicationModel); return View(arcReaderApplicationModel);
} }
@ -76,7 +99,7 @@ namespace CatherineLynwood.Controllers
{ {
if (ModelState.IsValid) if (ModelState.IsValid)
{ {
await _dataAccess.SaveContact(contact); await _dataAccess.SaveContact(contact, false);
var subject = "Email from Catherine Lynwood Web Site"; var subject = "Email from Catherine Lynwood Web Site";
var plainTextContent = $"Email from: {contact.Name} ({contact.EmailAddress})\r\n{contact.Message}"; var plainTextContent = $"Email from: {contact.Name} ({contact.EmailAddress})\r\n{contact.Message}";
@ -193,8 +216,13 @@ namespace CatherineLynwood.Controllers
} }
[Route("verostic-genre")] [Route("verostic-genre")]
public IActionResult VerosticGenre() public IActionResult VerosticGenre(int step)
{ {
if (step > 0)
{
ViewData["step"] = step;
}
return View(); return View();
} }
@ -231,6 +259,83 @@ namespace CatherineLynwood.Controllers
userAgent.Contains("ipad"); userAgent.Contains("ipad");
} }
public async Task SendARCInstructionsEmailAsync(ArcReaderApplicationModel arc)
{
var subject = "Your ARC Copy of *The Alpha Flame: Discovery* Kindle Instructions";
var plainTextContent = $@"Hi {arc.FullName},
Thank you so much for applying to be an ARC reader! Im thrilled to have you on board.
Since you selected Kindle delivery, heres what you need to do next:
1. Add my sender email address to your approved Kindle senders list: catherine@catherinelynwood.com
2. Make sure youve added your correct Kindle email: {arc.KindleEmail}@kindle.com
You can do this by visiting https://www.amazon.co.uk/myk → Preferences → Personal Document Settings.
Once this is done let me know and then Ill be sending your ARC shortly!
All my best,
Catherine";
var htmlContent = $@"
<p>Hi {arc.FullName},</p>
<p>Thank you so much for applying to be an ARC reader! Im thrilled to have you on board.</p>
<p>Since you selected <strong>Kindle delivery</strong>, heres what you need to do next:</p>
<ol>
<li>Add my sender email address to your approved Kindle senders list: <strong>catherine@catherinelynwood.com</strong></li>
<li>Ensure your Kindle email is correct: <strong>{arc.KindleEmail}@kindle.com</strong></li>
</ol>
<p>You can find these settings at <a href='https://www.amazon.co.uk/myk' target='_blank'>amazon.co.uk/myk</a> → Preferences → Personal Document Settings.</p>
<p>Once thats set up, let me know and Ill send your ARC soon!</p>
<p>Warmly,<br/>Catherine</p>";
Contact contact = new Contact
{
Name = arc.FullName,
EmailAddress = arc.Email
};
await _emailService.SendEmailAsync(subject, plainTextContent, htmlContent, contact, true);
}
public async Task SendARCAltDeliveryEmailAsync(ArcReaderApplicationModel arc)
{
var subject = "Your ARC Copy of *The Alpha Flame: Discovery* Next Steps";
var plainTextContent = $@"Hi {arc.FullName},
Thank you for applying to be an ARC reader I really appreciate it!
I noticed you selected an alternative to Kindle. Thats absolutely fine.
Ill be in touch shortly to offer a secure way to get your copy one thats simple and keeps the book protected from piracy.
If you have any questions or preferred reading methods (like phone, tablet, computer), feel free to reply to this email.
Thank you again,
Catherine";
var htmlContent = $@"
<p>Hi {arc.FullName},</p>
<p>Thank you for applying to be an ARC reader I really appreciate it!</p>
<p>I noticed you selected an alternative to Kindle. Thats absolutely fine.</p>
<p>Ill be in touch shortly to offer a secure way to get your copy one thats simple and keeps the book protected from piracy.</p>
<p>If you have any questions or preferred reading methods (like phone, tablet, or computer), just reply to this email.</p>
<p>Thanks again,<br/>Catherine</p>";
Contact contact = new Contact
{
Name = arc.FullName,
EmailAddress = arc.Email
};
await _emailService.SendEmailAsync(subject, plainTextContent, htmlContent, contact, true);
}
#endregion Private Methods #endregion Private Methods
} }
} }

View File

@ -0,0 +1,86 @@
using Microsoft.AspNetCore.Mvc;
namespace CatherineLynwood.Controllers
{
[Route("indie-author")]
public class IndieAuthorController : Controller
{
[HttpGet("")]
public IActionResult Index()
{
return View();
}
[HttpGet("isbns")]
public IActionResult Isbns()
{
return View();
}
[HttpGet("kdp")]
public IActionResult Kdp()
{
return View();
}
[HttpGet("audiobooks")]
public IActionResult Audiobooks()
{
return View();
}
[HttpGet("ingramspark")]
public IActionResult IngramSpark()
{
return View();
}
[HttpGet("kdp-vs-ingramspark")]
public IActionResult KdpVsIngramSpark()
{
return View();
}
[HttpGet("editing-and-proofreading")]
public IActionResult EditingAndProofreading()
{
return View();
}
[HttpGet("amazon-advertising")]
public IActionResult AmazonAdvertising()
{
return View();
}
[HttpGet("reviews-and-arc-readers")]
public IActionResult ReviewsAndArcReaders()
{
return View();
}
[HttpGet("cover-design")]
public IActionResult CoverDesign()
{
return View();
}
[HttpGet("ai-for-authors")]
public IActionResult AiForAuthors()
{
return View();
}
[HttpGet("traditional-vs-self-publishing")]
public IActionResult TraditionalVsSelfPublishing()
{
return View();
}
[HttpGet("author-finances")]
public IActionResult AuthorFinances()
{
return View();
}
}
}

View File

@ -0,0 +1,426 @@
using CatherineLynwood.Models;
using CatherineLynwood.Services;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using System.Globalization;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
namespace CatherineLynwood.Controllers
{
[Route("the-alpha-flame/reckoning")]
public class ReckoningController : Controller
{
private readonly DataAccess _dataAccess;
private readonly ICountryContext _country;
// A/B/C constants
private const string VariantCookie = "af_disc_abc";
private const string VariantQuery = "ab"; // ?ab=A or B or C
public ReckoningController(DataAccess dataAccess, ICountryContext country)
{
_dataAccess = dataAccess;
_country = country;
}
[Route("")]
public async Task<IActionResult> Index()
{
// Prevent caches or CDNs from cross-serving variants
Response.Headers["Vary"] = "Cookie, User-Agent";
// Decide device class first
var device = ResolveDeviceClass();
// Decide variant
var variant = ResolveVariant(device);
// Country ISO2
var iso2 = (_country.Iso2 ?? "GB").ToUpperInvariant();
if (iso2 == "UK") iso2 = "GB";
// Reviews
Reviews reviews = await _dataAccess.GetReviewsAsync("Reckoning");
reviews.SchemaJsonLd = GenerateBookSchemaJsonLd(reviews, 3);
string src = variant switch
{
Variant.A => "Mobile",
Variant.B => "Mobile",
_ => "Desktop"
};
// VM
var vm = new TitlePageViewModel
{
Reviews = reviews,
UserIso2 = iso2,
Src = src,
Title = "Reckoning"
};
// View mapping:
// - A and B are the two MOBILE variants
// - C is the DESKTOP variant
string viewName = variant switch
{
Variant.A => "IndexMobile", // mobile layout A
Variant.B => "IndexMobile", // mobile layout B
_ => "Index" // desktop layout C
};
return View(viewName, vm);
}
[Route("reviews")]
public async Task<IActionResult> Reviews()
{
Reviews reviews = await _dataAccess.GetReviewsAsync("Reckoning");
reviews.SchemaJsonLd = GenerateBookSchemaJsonLd(reviews, 100);
return View(reviews);
}
[Route("how-to-buy")]
public IActionResult HowToBuy() => View();
[Route("chapters/chapter-1-beth")]
public IActionResult Chapter1() => View();
[Route("chapters/chapter-2-maggie")]
public IActionResult Chapter2() => View();
[Route("chapters/chapter-13-susie")]
public IActionResult Chapter13() => View();
[Route("trailer")]
public async Task<IActionResult> Trailer()
{
// Country ISO2
var iso2 = (_country.Iso2 ?? "UK").ToUpperInvariant();
if (iso2 == "GB") iso2 = "UK";
FlagSupportViewModel flagSupportViewModel = await _dataAccess.GetTrailerLikes();
foreach (var flag in flagSupportViewModel.FlagCounts)
{
flag.Selected = flag.Key.ToLower() == iso2.ToLower();
}
return View(flagSupportViewModel);
}
[BookAccess(1, 2)]
[Route("extras")]
public IActionResult Extras() => View();
[BookAccess(1, 2)]
[Route("extras/epilogue")]
public IActionResult Epilogue() => View();
[BookAccess(1, 2)]
[Route("extras/scrap-book")]
public IActionResult ScrapBook() => View();
[BookAccess(1, 2)]
[Route("extras/listen")]
public IActionResult Listen() => View();
[BookAccess(1, 2)]
[Route("extras/maggies-designs")]
public IActionResult MaggiesDesigns() => View();
[BookAccess(1, 2)]
[Route("extras/soundtrack")]
public async Task<IActionResult> Soundtrack()
{
List<SoundtrackTrackModel> soundtrackTrackModels = await _dataAccess.GetSoundtrack();
return View(soundtrackTrackModels);
}
// =========================
// A/B/C selection
// =========================
private enum Variant { A, B, C }
private enum DeviceClass { Mobile, Desktop }
private Variant ResolveVariant(DeviceClass device)
{
// 0) Query override: ?ab=A|B|C forces and persists
if (Request.Query.TryGetValue(VariantQuery, out var qs))
{
var parsed = ParseVariant(qs.ToString());
if (parsed.HasValue)
{
// If they force A or B but device is desktop, we still respect C logic by default.
// Allow override to win for testing convenience.
WriteVariantCookie(parsed.Value);
return parsed.Value;
}
}
// 1) Existing cookie
if (Request.Cookies.TryGetValue(VariantCookie, out var cookieVal))
{
var parsed = ParseVariant(cookieVal);
if (parsed.HasValue)
{
// If cookie says A/B but device is desktop, upgrade to C to keep desktop experience consistent.
if (device == DeviceClass.Desktop && parsed.Value != Variant.C)
{
WriteVariantCookie(Variant.C);
return Variant.C;
}
// If cookie says C but device is mobile, you can either keep C or reassign to A/B.
// We keep C only for desktops. On mobile we reassess to A/B the first time after cookie mismatch.
if (device == DeviceClass.Mobile && parsed.Value == Variant.C)
{
var reassigned = AssignMobileVariant();
WriteVariantCookie(reassigned);
return reassigned;
}
return parsed.Value;
}
}
// 2) Fresh assignment
if (device == DeviceClass.Desktop)
{
WriteVariantCookie(Variant.C);
return Variant.C;
}
else
{
var assigned = AssignMobileVariant(); // A or B
WriteVariantCookie(assigned);
return assigned;
}
}
private static Variant AssignMobileVariant(int bPercent = 50)
{
bPercent = Math.Clamp(bPercent, 0, 100);
var roll = RandomNumberGenerator.GetInt32(0, 100); // 0..99
return roll < bPercent ? Variant.B : Variant.A;
}
private static Variant? ParseVariant(string value)
{
if (string.Equals(value, "A", StringComparison.OrdinalIgnoreCase)) return Variant.A;
if (string.Equals(value, "B", StringComparison.OrdinalIgnoreCase)) return Variant.B;
if (string.Equals(value, "C", StringComparison.OrdinalIgnoreCase)) return Variant.C;
return null;
}
private void WriteVariantCookie(Variant variant)
{
var opts = new CookieOptions
{
Expires = DateTimeOffset.UtcNow.AddDays(90),
HttpOnly = false, // set true if you do not need analytics to read it
Secure = true,
SameSite = SameSiteMode.Lax,
Path = "/"
};
Response.Cookies.Append(VariantCookie, variant.ToString(), opts);
}
// =========================
// Device detection
// =========================
private DeviceClass ResolveDeviceClass()
{
// Simple and robust server-side approach using User-Agent.
// Chrome UA reduction still leaves Mobile hint for Android; iOS strings include iPhone/iPad.
var ua = Request.Headers.UserAgent.ToString();
if (IsMobileUserAgent(ua))
return DeviceClass.Mobile;
return DeviceClass.Desktop;
}
private static bool IsMobileUserAgent(string ua)
{
if (string.IsNullOrEmpty(ua)) return false;
// Common mobile indicators
// Android phones include "Android" and "Mobile"
// iPhone includes "iPhone"; iPad includes "iPad" (treat as mobile for your layouts)
// Many mobile browsers include "Mobile"
// Exclude obvious desktop platforms
ua = ua.ToLowerInvariant();
if (ua.Contains("ipad") || ua.Contains("iphone") || ua.Contains("ipod"))
return true;
if (ua.Contains("android") && ua.Contains("mobile"))
return true;
if (ua.Contains("android") && !ua.Contains("mobile"))
{
// Likely a tablet; treat as mobile for your use case
return true;
}
if (ua.Contains("mobile"))
return true;
// Desktop hints
if (ua.Contains("windows nt") || ua.Contains("macintosh") || ua.Contains("x11"))
return false;
// Fallback
return false;
}
// =========================
// Existing helpers
// =========================
private string GenerateBookSchemaJsonLd(Reviews reviews, int take)
{
const string imageUrl = "https://www.catherinelynwood.com/images/webp/the-alpha-flame-discovery-cover-1200.webp";
const string baseUrl = "https://www.catherinelynwood.com/the-alpha-flame/discovery";
var schema = new Dictionary<string, object>
{
["@context"] = "https://schema.org",
["@type"] = "Book",
["name"] = "The Alpha Flame: Discovery",
["alternateName"] = "The Alpha Flame Book 1",
["image"] = imageUrl,
["author"] = new Dictionary<string, object>
{
["@type"] = "Person",
["name"] = "Catherine Lynwood",
["url"] = "https://www.catherinelynwood.com"
},
["publisher"] = new Dictionary<string, object>
{
["@type"] = "Organization",
["name"] = "Catherine Lynwood"
},
["datePublished"] = "2025-08-21",
["description"] = "The Alpha Flame: Discovery is a powerful, character-driven novel set in 1983 Birmingham, following Maggie Grant and Beth, two young women separated by fate, reunited by truth, and bound by secrets...",
["genre"] = "Women's Fiction, Mystery, Contemporary Historical",
["inLanguage"] = "en-GB",
["url"] = baseUrl
};
if (reviews?.Items?.Any() == true)
{
var reviewObjects = new List<Dictionary<string, object>>();
double total = 0;
foreach (var review in reviews.Items) total += review.RatingValue;
foreach (var review in reviews.Items.Take(take))
{
reviewObjects.Add(new Dictionary<string, object>
{
["@type"] = "Review",
["author"] = new Dictionary<string, object>
{
["@type"] = "Person",
["name"] = review.AuthorName
},
["datePublished"] = review.DatePublished.ToString("yyyy-MM-dd"),
["reviewBody"] = StripHtml(review.ReviewBody),
["reviewRating"] = new Dictionary<string, object>
{
["@type"] = "Rating",
["ratingValue"] = review.RatingValue,
["bestRating"] = "5"
}
});
}
schema["review"] = reviewObjects;
schema["aggregateRating"] = new Dictionary<string, object>
{
["@type"] = "AggregateRating",
["ratingValue"] = (total / reviews.Items.Count).ToString("0.0", CultureInfo.InvariantCulture),
["reviewCount"] = reviews.Items.Count
};
}
schema["workExample"] = new List<Dictionary<string, object>>
{
new Dictionary<string, object>
{
["@type"] = "Book",
["bookFormat"] = "https://schema.org/Hardcover",
["isbn"] = "978-1-0682258-0-2",
["name"] = "The Alpha Flame: Discovery Collector's Edition",
["image"] = imageUrl,
["offers"] = new Dictionary<string, object>
{
["@type"] = "Offer",
["price"] = "23.99",
["priceCurrency"] = "GBP",
["availability"] = "https://schema.org/InStock",
["url"] = baseUrl
}
},
new Dictionary<string, object>
{
["@type"] = "Book",
["bookFormat"] = "https://schema.org/Paperback",
["isbn"] = "978-1-0682258-1-9",
["name"] = "The Alpha Flame: Discovery Bookshop Edition",
["image"] = imageUrl,
["offers"] = new Dictionary<string, object>
{
["@type"] = "Offer",
["price"] = "17.99",
["priceCurrency"] = "GBP",
["availability"] = "https://schema.org/InStock",
["url"] = baseUrl
}
},
new Dictionary<string, object>
{
["@type"] = "Book",
["bookFormat"] = "https://schema.org/Paperback",
["isbn"] = "978-1-0682258-2-6",
["name"] = "The Alpha Flame: Discovery Amazon Edition",
["image"] = imageUrl,
["offers"] = new Dictionary<string, object>
{
["@type"] = "Offer",
["price"] = "13.99",
["priceCurrency"] = "GBP",
["availability"] = "https://schema.org/InStock",
["url"] = baseUrl
}
},
new Dictionary<string, object>
{
["@type"] = "Book",
["bookFormat"] = "https://schema.org/EBook",
["isbn"] = "978-1-0682258-3-3",
["name"] = "The Alpha Flame: Discovery eBook",
["image"] = imageUrl,
["offers"] = new Dictionary<string, object>
{
["@type"] = "Offer",
["price"] = "3.95",
["priceCurrency"] = "GBP",
["availability"] = "https://schema.org/InStock",
["url"] = baseUrl
}
}
};
return JsonConvert.SerializeObject(schema, Formatting.Indented);
}
private static string StripHtml(string input) =>
string.IsNullOrWhiteSpace(input) ? string.Empty : Regex.Replace(input, "<.*?>", string.Empty);
}
}

View File

@ -38,18 +38,37 @@ namespace CatherineLynwood.Controllers
new SitemapEntry { Url = Url.Action("Chapter1", "Discovery", 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("Chapter13", "Discovery", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, new SitemapEntry { Url = Url.Action("Chapter13", "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("Chapter2", "Discovery", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("Reviews", "Discovery", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, new SitemapEntry { Url = Url.Action("AudioBook", "Discovery", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("Trailer", "Discovery", 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("Characters", "TheAlphaFlame", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("ContactCatherine", "Home", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, new SitemapEntry { Url = Url.Action("ContactCatherine", "Home", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("Giveaways", "TheAlphaFlame", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, new SitemapEntry { Url = Url.Action("Giveaways", "TheAlphaFlame", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("StoryMap", "TheAlphaFlame", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("HowToBuy", "Discovery", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("Index", "AskAQuestion", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, new SitemapEntry { Url = Url.Action("Index", "AskAQuestion", 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", "Discovery", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("Index", "Reckoning", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("Index", "Home", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, new SitemapEntry { Url = Url.Action("Index", "Home", 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("Index", "Publishing", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("Index", "TheAlphaFlame", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, new SitemapEntry { Url = Url.Action("Index", "TheAlphaFlame", 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("Privacy", "Home", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("Reviews", "Discovery", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("SamanthaLynwood", "Home", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, new SitemapEntry { Url = Url.Action("SamanthaLynwood", "Home", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("VerosticGenre", "Home", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, new SitemapEntry { Url = Url.Action("VerosticGenre", "Home", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("LarhysaSaddul", "Collaborations", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("Index", "IndieAuthor", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("Isbn", "IndieAuthor", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("Kdp", "IndieAuthor", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("Audiobooks", "IndieAuthor", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("IngramSpark", "IndieAuthor", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("KdpVsIngramSpark", "IndieAuthor", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("EditingAndProofreading", "IndieAuthor", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("AmazonAdvertising", "IndieAuthor", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("ReviewsAndArcReaders", "IndieAuthor", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("TraditionalVsSelfPublishing", "IndieAuthor", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("AuthorFinances", "IndieAuthor", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("CoverDesign", "IndieAuthor", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("AiForAuthors", "IndieAuthor", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
// Additional static pages // Additional static pages
}; };

View File

@ -0,0 +1,68 @@
using CatherineLynwood.Models;
using CatherineLynwood.Services;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
namespace CatherineLynwood.Controllers
{
[ApiController]
[Route("api/support")]
public class SupportController : ControllerBase
{
private DataAccess _dataAccess;
public record FlagDto(string Country);
public class SubscriptionDto
{
public string Name { get; set; }
public string Email { get; set; }
}
public SupportController(DataAccess dataAccess)
{
_dataAccess = dataAccess;
}
[HttpPost("flag")]
public async Task<IActionResult> Flag([FromBody] FlagDto dto)
{
if (string.IsNullOrWhiteSpace(dto?.Country))
return BadRequest(new { ok = false, error = "Country required" });
// TODO: replace with real DB call
var total = await _dataAccess.SaveFlagClick(dto.Country);
return Ok(new { ok = true, total });
}
[HttpPost("subscribe")]
public IActionResult Subscribe([FromBody] SubscriptionDto dto)
{
if (string.IsNullOrWhiteSpace(dto?.Email))
return BadRequest(new { ok = false, error = "Email required" });
Contact contact = new Contact
{
EmailAddress = dto.Email,
Name = dto.Name
};
var ok = _dataAccess.SaveContact(contact, true);
return Ok(new { ok });
}
// ----- Dummy persistence you can swap for EF/Dapper calls -----
private static bool SaveSubscription(string email, string country)
{
return true;
}
}
}

View File

@ -1,9 +1,7 @@
using CatherineLynwood.Helpers; using CatherineLynwood.Models;
using CatherineLynwood.Models;
using CatherineLynwood.Services; using CatherineLynwood.Services;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Identity.Client;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -52,6 +50,14 @@ namespace CatherineLynwood.Controllers
blogIndex.BlogFilter.TotalPages = (int)Math.Ceiling((double)blogIndex.Blogs.Count / blogFilter.ResultsPerPage); blogIndex.BlogFilter.TotalPages = (int)Math.Ceiling((double)blogIndex.Blogs.Count / blogFilter.ResultsPerPage);
blogIndex.IsMobile = IsMobile(Request); blogIndex.IsMobile = IsMobile(Request);
// Add this check before paginating
if (blogIndex.BlogFilter.PageNumber > blogIndex.BlogFilter.TotalPages && blogIndex.BlogFilter.TotalPages > 0)
{
Response.StatusCode = 404;
return View("BlogNotFound");
}
// Apply sorting // Apply sorting
if (blogFilter.SortDirection == 1) if (blogFilter.SortDirection == 1)
blogIndex.Blogs = blogIndex.Blogs.OrderByDescending(b => b.PublishDate).ToList(); blogIndex.Blogs = blogIndex.Blogs.OrderByDescending(b => b.PublishDate).ToList();
@ -149,6 +155,30 @@ namespace CatherineLynwood.Controllers
return RedirectToAction("BlogItem", new { slug = blogUrl, showThanks = showThanks }); return RedirectToAction("BlogItem", new { slug = blogUrl, showThanks = showThanks });
} }
[BookAccess(1)]
[Route("extras")]
public IActionResult Extras()
{
var session = HttpContext.Session;
var level = session.GetInt32("BookAccessLevel");
var book = session.GetInt32("BookAccessMax");
if (book == 2)
{
return RedirectToAction("Extras", "Reckoning");
}
if (book == 3)
{
return RedirectToAction("Extras", "Redemption");
}
return RedirectToAction("Extras", "Discovery");
}
[Route("")] [Route("")]
public IActionResult Index() public IActionResult Index()
{ {
@ -206,11 +236,6 @@ namespace CatherineLynwood.Controllers
return View(); return View();
} }
[Route("reckoning")]
public IActionResult Reckoning()
{
return View();
}
[Route("redemption")] [Route("redemption")]
public IActionResult Redemption() public IActionResult Redemption()
@ -248,6 +273,12 @@ namespace CatherineLynwood.Controllers
return View(); return View();
} }
[Route("story-map")]
public IActionResult StoryMap()
{
return View();
}
#endregion Public Methods #endregion Public Methods
#region Private Methods #region Private Methods

View File

@ -6,7 +6,7 @@ public class BookAccessAttribute : Attribute, IAsyncAuthorizationFilter
private readonly int _requiredLevel; private readonly int _requiredLevel;
private readonly int _requiredBook; private readonly int _requiredBook;
public BookAccessAttribute(int requiredLevel, int requiredBook) public BookAccessAttribute(int requiredLevel, int requiredBook = 0)
{ {
_requiredLevel = requiredLevel; _requiredLevel = requiredLevel;
_requiredBook = requiredBook; _requiredBook = requiredBook;
@ -19,13 +19,28 @@ public class BookAccessAttribute : Attribute, IAsyncAuthorizationFilter
var level = session.GetInt32("BookAccessLevel"); var level = session.GetInt32("BookAccessLevel");
var book = session.GetInt32("BookAccessMax"); var book = session.GetInt32("BookAccessMax");
if (level == null || book == null || level < _requiredLevel || book < _requiredBook) if (_requiredBook == 0)
{
if (level == null || book == null || level < _requiredLevel)
{ {
var currentUrl = context.HttpContext.Request.Path + context.HttpContext.Request.QueryString; var currentUrl = context.HttpContext.Request.Path + context.HttpContext.Request.QueryString;
context.HttpContext.Items["RequestedUrl"] = currentUrl; // store temporarily context.HttpContext.Items["RequestedUrl"] = currentUrl; // store temporarily
context.Result = new RedirectToActionResult("Prompt", "Access", new { returnUrl = currentUrl }); context.Result = new RedirectToActionResult("Prompt", "Access", new { returnUrl = currentUrl });
} }
}
else
{
if (level == null || book == null || level < _requiredLevel || book != _requiredBook)
{
var currentUrl = context.HttpContext.Request.Path + context.HttpContext.Request.QueryString;
context.HttpContext.Items["RequestedUrl"] = currentUrl; // store temporarily
context.Result = new RedirectToActionResult("Prompt", "Access", new { returnUrl = currentUrl });
}
}
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@ -1,76 +0,0 @@
namespace CatherineLynwood.Middleware
{
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Web.Administration;
using System.Linq;
using System.Threading.Tasks;
public class BlockPhpRequestsMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<BlockPhpRequestsMiddleware> _logger;
private IWebHostEnvironment _environment;
public BlockPhpRequestsMiddleware(RequestDelegate next, ILogger<BlockPhpRequestsMiddleware> logger, IWebHostEnvironment environment)
{
_next = next;
_logger = logger;
_environment = environment;
}
public async Task InvokeAsync(HttpContext context)
{
var requestPath = context.Request.Path.Value;
if (requestPath != null && (requestPath.EndsWith(".php") || requestPath.EndsWith(".env")))
{
var ipAddress = context.Connection.RemoteIpAddress?.ToString();
if (ipAddress != null)
{
_logger.LogWarning($"Detected PHP request from IP {ipAddress}.");
if (!_environment.IsDevelopment())
{
// Only attempt to block IP if not in development
BlockIpAddressInIIS(ipAddress);
}
context.Response.StatusCode = StatusCodes.Status403Forbidden;
return;
}
}
await _next(context);
}
private void BlockIpAddressInIIS(string ipAddress)
{
using (var serverManager = new ServerManager())
{
// Replace "Default Web Site" with your actual site name
var site = serverManager.Sites["CatherineLynwood"];
var config = site.GetWebConfiguration();
var ipSecuritySection = config.GetSection("system.webServer/security/ipSecurity");
var ipSecurityCollection = ipSecuritySection.GetCollection();
// Check if IP already exists in the list to avoid duplicates
var existingEntry = ipSecurityCollection.FirstOrDefault(e => e.Attributes["ipAddress"]?.Value?.ToString() == ipAddress);
if (existingEntry == null)
{
// Add a new IP restriction entry with deny access
var addElement = ipSecurityCollection.CreateElement("add");
addElement.SetAttributeValue("ipAddress", ipAddress);
addElement.SetAttributeValue("allowed", false);
ipSecurityCollection.Add(addElement);
serverManager.CommitChanges();
}
}
}
}
}

View File

@ -1,39 +0,0 @@
namespace CatherineLynwood.Middleware
{
public class BotFilterMiddleware
{
#region Private Fields
private static readonly List<string> 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
}
}

View File

@ -0,0 +1,35 @@
namespace CatherineLynwood.Middleware
{
public class EnsureSidMiddleware
{
private readonly RequestDelegate _next;
public EnsureSidMiddleware(RequestDelegate next) => _next = next;
public async Task Invoke(HttpContext ctx)
{
if (!ctx.Request.Cookies.ContainsKey("sid") &&
string.Equals(ctx.Request.Method, "GET", StringComparison.OrdinalIgnoreCase))
{
var accept = ctx.Request.Headers["Accept"].ToString();
var fetchMode = ctx.Request.Headers["Sec-Fetch-Mode"].ToString();
// Only issue on real navigations to HTML
if (accept.Contains("text/html", StringComparison.OrdinalIgnoreCase) ||
string.Equals(fetchMode, "navigate", StringComparison.OrdinalIgnoreCase))
{
ctx.Response.Cookies.Append("sid", Guid.NewGuid().ToString("N"), new CookieOptions
{
HttpOnly = true,
SameSite = SameSiteMode.Lax,
Secure = true,
MaxAge = TimeSpan.FromDays(365),
Path = "/"
});
}
}
await _next(ctx);
}
}
}

View File

@ -0,0 +1,115 @@
using CatherineLynwood.Models;
using CatherineLynwood.Services;
using System.Net;
namespace CatherineLynwood.Middleware
{
public sealed class GeoResolutionMiddleware
{
private readonly RequestDelegate _next;
private readonly IGeoResolver _resolver;
private readonly ILogger<GeoResolutionMiddleware> _logger;
private readonly IHostEnvironment _env;
public GeoResolutionMiddleware(
RequestDelegate next,
IGeoResolver resolver,
ILogger<GeoResolutionMiddleware> logger,
IHostEnvironment env)
{
_next = next; _resolver = resolver; _logger = logger; _env = env;
}
public async Task Invoke(HttpContext context)
{
var path = context.Request.Path.ToString();
// Skip static files (images, css, js, fonts, ico, svg, etc.)
if (path.EndsWith(".css", StringComparison.OrdinalIgnoreCase) ||
path.EndsWith(".js", StringComparison.OrdinalIgnoreCase) ||
path.EndsWith(".png", StringComparison.OrdinalIgnoreCase) ||
path.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) ||
path.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase) ||
path.EndsWith(".webp", StringComparison.OrdinalIgnoreCase) ||
path.EndsWith(".gif", StringComparison.OrdinalIgnoreCase) ||
path.EndsWith(".svg", StringComparison.OrdinalIgnoreCase) ||
path.EndsWith(".ico", StringComparison.OrdinalIgnoreCase) ||
path.EndsWith(".woff", StringComparison.OrdinalIgnoreCase) ||
path.EndsWith(".woff2", StringComparison.OrdinalIgnoreCase) ||
path.EndsWith(".ttf", StringComparison.OrdinalIgnoreCase) ||
path.EndsWith(".mp3", StringComparison.OrdinalIgnoreCase) ||
path.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase) ||
path.EndsWith(".map", StringComparison.OrdinalIgnoreCase))
{
await _next(context);
return;
}
// Optionally also skip non-GET requests if you only care about page views
if (!HttpMethods.IsGet(context.Request.Method))
{
await _next(context);
return;
}
// Basic trace to confirm middleware is running
_logger.LogInformation("GeoMW: path={Path}", context.Request.Path);
var ip = GetClientIp(context);
var userAgent = context.Request.Headers["User-Agent"].ToString();
var queryString = context.Request.QueryString.ToString();
// In Development, replace loopback with a known public IP so ResolveAsync definitely runs
if (ip is null || IPAddress.IsLoopback(ip))
{
if (_env.IsDevelopment())
{
ip = IPAddress.Parse("90.240.25.56"); // UK
//ip = IPAddress.Parse("66.249.10.10"); // US
//ip = IPAddress.Parse("24.48.0.1"); // Canada
//ip = IPAddress.Parse("212.129.83.32"); // Ireland
//ip = IPAddress.Parse("203.109.171.243"); // New Zealand
//ip = IPAddress.Parse("1.1.1.1"); // Australia
_logger.LogInformation("GeoMW: dev override IP -> {IP}", ip);
}
else
{
_logger.LogInformation("GeoMW: loopback or null IP in non-dev; skipping");
await _next(context);
return;
}
}
_logger.LogInformation("GeoMW: calling resolver for {IP}", ip);
var geo = await _resolver.ResolveAsync(ip, path, queryString, userAgent);
if (geo is not null)
{
context.Items[HttpContextItemKeys.CountryIso2] = geo.Iso2;
context.Items[HttpContextItemKeys.CountryName] = geo.CountryName ?? "";
context.Response.Headers["X-Geo-Iso2"] = geo.Iso2; // debug aid
_logger.LogInformation("GeoMW: resolved {IP} -> {ISO2} ({Country})", ip, geo.Iso2, geo.CountryName);
}
else
{
context.Response.Headers["X-Geo-Iso2"] = "none"; // debug aid
_logger.LogWarning("GeoMW: resolver returned null for {IP}", ip);
}
await _next(context);
}
private static IPAddress? GetClientIp(HttpContext ctx)
{
// Prefer X-Forwarded-For when present
if (ctx.Request.Headers.TryGetValue("X-Forwarded-For", out var xff))
{
var first = xff.ToString().Split(',').Select(s => s.Trim()).FirstOrDefault();
if (IPAddress.TryParse(first, out var parsed)) return parsed;
}
return ctx.Connection.RemoteIpAddress;
}
}
}

View File

@ -1,82 +0,0 @@
using System.Net;
namespace CatherineLynwood.Middleware
{
public class IpqsBlockMiddleware
{
private readonly RequestDelegate _next;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<IpqsBlockMiddleware> _logger;
private const string ApiKey = "MQUwnYmhKZzHpt6FyxV97EFg8JxlByZt"; // Replace with your IPQS API key
private static readonly string[] ProtectedPaths = new[]
{
"/ask-a-question",
"/contact-catherine",
"/the-alpha-flame/blog/"
};
public IpqsBlockMiddleware(RequestDelegate next, IHttpClientFactory httpClientFactory, ILogger<IpqsBlockMiddleware> logger)
{
_next = next;
_httpClientFactory = httpClientFactory;
_logger = logger;
}
public async Task Invoke(HttpContext context)
{
var path = context.Request.Path.Value?.ToLower();
if (!ProtectedPaths.Any(p => path.StartsWith(p)))
{
await _next(context);
return;
}
var ip = context.Connection.RemoteIpAddress?.ToString();
//ip = "18.130.131.76";
// skip localhost
if (string.IsNullOrWhiteSpace(ip) || IPAddress.IsLoopback(IPAddress.Parse(ip)))
{
await _next(context);
return;
}
try
{
var client = _httpClientFactory.CreateClient();
var url = $"https://ipqualityscore.com/api/json/ip/{ApiKey}/{ip}?strictness=1&fast=true";
var response = await client.GetFromJsonAsync<IpqsResponse>(url);
if (response != null && (response.is_proxy || response.is_vpn || response.fraud_score >= 85))
{
if (context.Request.Method == HttpMethods.Post)
{
_logger.LogWarning("Blocked VPN/Proxy on POST: IP={IP}", ip);
context.Response.StatusCode = StatusCodes.Status403Forbidden;
await context.Response.WriteAsync("Access denied.");
return;
}
// Mark for GET requests so the view can show a warning
context.Items["IsVpn"] = true;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "IPQS lookup failed");
}
await _next(context);
}
public class IpqsResponse
{
public bool is_proxy { get; set; }
public bool is_vpn { get; set; }
public int fraud_score { get; set; }
}
}
}

View File

@ -1,36 +0,0 @@
namespace CatherineLynwood.Middleware
{
public class RedirectToWwwMiddleware
{
private readonly RequestDelegate _next;
private IWebHostEnvironment _environment;
public RedirectToWwwMiddleware(RequestDelegate next, IWebHostEnvironment environment)
{
_next = next;
_environment = environment;
}
public async Task InvokeAsync(HttpContext context)
{
var host = context.Request.Host.Host;
var schema = context.Request.Scheme;
if (_environment.IsProduction())
{
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.StatusCode = StatusCodes.Status308PermanentRedirect;
context.Response.Headers["Location"] = newUrl;
return;
}
}
// Continue to the next middleware.
await _next(context);
}
}
}

View File

@ -1,38 +0,0 @@
namespace CatherineLynwood.Middleware
{
public class RefererValidationMiddleware
{
private readonly RequestDelegate _next;
// Whitelist of valid referer prefixes
private static readonly string[] AllowedReferers = new[]
{
"https://www.catherinelynwood.com",
"http://localhost",
"https://localhost"
};
public RefererValidationMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
if (context.Request.Method == HttpMethods.Post)
{
var referer = context.Request.Headers["Referer"].ToString();
if (string.IsNullOrEmpty(referer) || !AllowedReferers.Any(r => referer.StartsWith(r, StringComparison.OrdinalIgnoreCase)))
{
context.Response.StatusCode = StatusCodes.Status451UnavailableForLegalReasons;
await context.Response.WriteAsync("Invalid request.");
return;
}
}
await _next(context);
}
}
}

View File

@ -61,10 +61,10 @@ namespace CatherineLynwood.Middleware
// //
if (path.EndsWith(".php", StringComparison.OrdinalIgnoreCase) || path.EndsWith(".env", StringComparison.OrdinalIgnoreCase)) if (path.EndsWith(".php", StringComparison.OrdinalIgnoreCase) || path.EndsWith(".env", StringComparison.OrdinalIgnoreCase))
{ {
//if (!_environment.IsDevelopment() && ipAddress != null) if (!_environment.IsDevelopment() && ipAddress != null)
//{ {
// TryBlockIpInIIS(ipAddress); TryBlockIpInIIS(ipAddress);
//} }
_logger.LogWarning("Blocked .php/.env probe from {IP}: {Path}", ipAddress, path); _logger.LogWarning("Blocked .php/.env probe from {IP}: {Path}", ipAddress, path);
@ -98,10 +98,10 @@ namespace CatherineLynwood.Middleware
// Only block if referer is present but NOT in the whitelist // Only block if referer is present but NOT in the whitelist
if (!string.IsNullOrEmpty(referer) && !AllowedReferers.Any(r => referer.StartsWith(r, StringComparison.Ordinal))) if (!string.IsNullOrEmpty(referer) && !AllowedReferers.Any(r => referer.StartsWith(r, StringComparison.Ordinal)))
{ {
//if (!_environment.IsDevelopment() && ipAddress != null) if (!_environment.IsDevelopment() && ipAddress != null)
//{ {
// TryBlockIpInIIS(ipAddress); TryBlockIpInIIS(ipAddress);
//} }
_logger.LogWarning("Blocked POST with invalid referer: {Referer} from IP {IP}", referer, ipAddress); _logger.LogWarning("Blocked POST with invalid referer: {Referer} from IP {IP}", referer, ipAddress);

View File

@ -6,9 +6,6 @@ namespace CatherineLynwood.Models
{ {
#region Public Properties #region Public Properties
[Required, StringLength(50)]
public string ApprovedSender { get; set; }
[Required, StringLength(50)] [Required, StringLength(50)]
public string ContentFit { get; set; } public string ContentFit { get; set; }
@ -20,6 +17,9 @@ namespace CatherineLynwood.Models
[Required, StringLength(100)] [Required, StringLength(100)]
public string FullName { get; set; } public string FullName { get; set; }
[Required, StringLength(50)]
public string HasKindleAccess { get; set; }
[Key] [Key]
public int Id { get; set; } public int Id { get; set; }

View File

@ -0,0 +1,12 @@
namespace CatherineLynwood.Models
{
public sealed class AbTestOptions
{
#region Public Properties
// Percentage of users who should see variant B; the rest see A
public int DiscoveryBPercent { get; set; } = 50;
#endregion Public Properties
}
}

View File

@ -2,10 +2,21 @@
{ {
public class AccessCode public class AccessCode
{ {
public int PageNumber { get; set; } #region Public Properties
public int WordIndex { get; set; }
public int AccessLevel { get; set; }
// 1 = basic, 2 = retail, 3 = deluxe
public int BookID { get; set; }
public string ExpectedWord { get; set; } public string ExpectedWord { get; set; }
public int AccessLevel { get; set; } // 1 = basic, 2 = retail, 3 = deluxe
public int BookNumber { get; set; } // 1, 2, or 3 public int PageNumber { get; set; }
public int WordIndex { get; set; }
#endregion Public Properties
// 1, 2, or 3
} }
} }

View File

@ -4,8 +4,8 @@ namespace CatherineLynwood.Models
{ {
public class ArcReaderApplicationModel public class ArcReaderApplicationModel
{ {
[Required(ErrorMessage = "Please enter your full name.")] [Required(ErrorMessage = "Please enter your name.")]
[Display(Name = "Your Full Name", Prompt = "e.g. Catherine Lynwood")] [Display(Name = "Your Name", Prompt = "e.g. Catherine")]
[StringLength(100, ErrorMessage = "Name must be under 100 characters.")] [StringLength(100, ErrorMessage = "Name must be under 100 characters.")]
public string FullName { get; set; } public string FullName { get; set; }
@ -15,13 +15,9 @@ namespace CatherineLynwood.Models
[DataType(DataType.EmailAddress)] [DataType(DataType.EmailAddress)]
public string Email { get; set; } public string Email { get; set; }
[Required(ErrorMessage = "Please enter your Kindle email address.")] [Display(Name = "Your Kindle Email", Prompt = "e.g. yourname@kindle.com")]
[Display(Name = "Kindle Email Address", Prompt = "e.g. yourname")] [EmailAddress(ErrorMessage = "Please enter a valid Kindle email address.")]
public string KindleEmail { get; set; } public string? KindleEmail { get; set; } // Now optional
[Required(ErrorMessage = "Please select whether you've approved the sender.")]
[Display(Name = "Have you added my sender email to your Approved Senders list?")]
public string ApprovedSender { get; set; }
[Display(Name = "Where do you plan to post your review?")] [Display(Name = "Where do you plan to post your review?")]
public List<string> Platforms { get; set; } public List<string> Platforms { get; set; }
@ -50,5 +46,10 @@ namespace CatherineLynwood.Models
[DataType(DataType.MultilineText)] [DataType(DataType.MultilineText)]
[StringLength(1000, ErrorMessage = "Please keep this under 1000 characters.")] [StringLength(1000, ErrorMessage = "Please keep this under 1000 characters.")]
public string? ExtraNotes { get; set; } public string? ExtraNotes { get; set; }
[Required(ErrorMessage = "Please indicate whether you can receive the ARC via Kindle.")]
[Display(Name = "Do you have Kindle access?")]
public string HasKindleAccess { get; set; } // Expected values: "Yes" or "No"
} }
} }

View File

@ -0,0 +1,71 @@
namespace CatherineLynwood.Models
{
public class BuyGroup
{
#region Public Properties
public int BuyGroupID { get; set; }
public int DisplayOrder { get; set; }
public string GroupName { get; set; } = string.Empty;
public List<BuyLink> Links { get; set; } = new List<BuyLink>();
public string Message { get; set; } = string.Empty;
#endregion Public Properties
}
public class BuyLink
{
#region Public Properties
public int BuyGroupID { get; set; }
public int BuyLinkID { get; set; }
public string ISO2 { get; set; } = string.Empty;
public string Icon { get; set; } = string.Empty;
public string Price { get; set; } = string.Empty;
public string Slug { get; set; } = string.Empty;
public string Target { get; set; } = string.Empty;
public string Text { get; set; } = string.Empty;
#endregion Public Properties
}
public class BuyPanelViewModel
{
#region Public Properties
private string _iso2 = "UK";
public string CountryName { get; set; } = string.Empty;
public string FlagUrl
{
get
{
return $"/images/flags/{_iso2.ToLower()}.svg";
}
}
public List<BuyGroup> Groups { get; set; } = new List<BuyGroup>();
public string ISO2
{
get { return _iso2; }
set { _iso2 = value; }
}
public string Src { get; set; } = string.Empty;
#endregion Public Properties
}
}

View File

@ -0,0 +1,17 @@
namespace CatherineLynwood.Models
{
public class FlagSupportViewModel
{
public List<FlagCount> FlagCounts { get; set; } = new();
}
public class FlagCount
{
public string Key { get; set; }
public int Value { get; set; }
public bool Selected { get; set; }
}
}

View File

@ -0,0 +1,27 @@
namespace CatherineLynwood.Models
{
public class GeoIpResult
{
#region Public Properties
public string City { get; set; } = string.Empty;
public string CountryName { get; set; } = "";
public string Ip { get; set; } = "";
public string Iso2 { get; set; } = "";
public DateTime LastSeenUtc { get; set; }
public string Path { get; set; } = string.Empty;
public string QueryString { get; set; } = string.Empty;
public string Source { get; set; } = string.Empty;
public string UserAgent { get; set; } = string.Empty;
#endregion Public Properties
}
}

View File

@ -0,0 +1,13 @@
namespace CatherineLynwood.Models
{
public static class HttpContextItemKeys
{
#region Public Fields
public const string CountryIso2 = "CountryIso2";
public const string CountryName = "CountryName";
#endregion Public Fields
}
}

View File

@ -0,0 +1,13 @@
namespace CatherineLynwood.Models
{
public sealed class LinkChoice
{
#region Public Properties
public string Slug { get; set; } = "";
public string Url { get; set; } = "";
#endregion Public Properties
}
}

View File

@ -0,0 +1,27 @@
namespace CatherineLynwood.Models
{
public class SoundtrackTrackModel
{
#region Public Properties
public string AudioUrl { get; set; }
public string Chapter { get; set; }
// e.g., "Chapter 18"
public string Description { get; set; }
// short blurb
public string ImageUrl { get; set; }
// artwork or suggestive image
// mp3/ogg
public string LyricsHtml { get; set; }
public string Title { get; set; }
#endregion Public Properties
// fallback if no HTML
}
}

View File

@ -0,0 +1,17 @@
namespace CatherineLynwood.Models
{
public sealed class TitlePageViewModel
{
#region Public Properties
public Reviews Reviews { get; set; } = new Reviews();
public string UserIso2 { get; set; } = "GB";
public string Src { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
#endregion Public Properties
}
}

View File

@ -1,10 +1,12 @@
using System.IO.Compression; using CatherineLynwood.Middleware;
using CatherineLynwood.Models;
using CatherineLynwood.Middleware;
using CatherineLynwood.Services; using CatherineLynwood.Services;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.ResponseCompression; using Microsoft.AspNetCore.ResponseCompression;
using System.IO.Compression;
using WebMarkupMin.AspNetCoreLatest; using WebMarkupMin.AspNetCoreLatest;
namespace CatherineLynwood namespace CatherineLynwood
@ -15,21 +17,44 @@ namespace CatherineLynwood
{ {
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// Retrieve the connection string from appsettings.json
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
// DAL
builder.Services.AddSingleton(new DataAccess(connectionString)); builder.Services.AddSingleton(new DataAccess(connectionString));
// MVC
builder.Services.AddControllersWithViews(); builder.Services.AddControllersWithViews();
// Add IHttpContextAccessor for accessing HTTP context in tag helpers // HttpContext accessor
builder.Services.AddHttpContextAccessor(); builder.Services.AddHttpContextAccessor();
builder.Services.AddHostedService<IndexNowBackgroundService>(); // Memory cache for IP cache
builder.Services.AddMemoryCache();
// HttpClient for general use
builder.Services.AddHttpClient(); builder.Services.AddHttpClient();
// ✅ Add session services (in-memory only) // Named HttpClient for the GeoResolver with short timeout
builder.Services.AddHttpClient(nameof(GeoResolver), c => c.Timeout = TimeSpan.FromSeconds(3));
// Geo services
builder.Services.AddSingleton<IGeoResolver, GeoResolver>();
builder.Services.AddScoped<ICountryContext, CountryContext>();
// Background and other services you already use
builder.Services.AddHostedService<IndexNowBackgroundService>();
builder.Services.AddSingleton<RedirectsStore>(sp =>
{
var logger = sp.GetRequiredService<ILogger<RedirectsStore>>();
return new RedirectsStore("redirects.json", logger);
});
builder.Services.AddScoped<IAccessCodeService, AccessCodeService>();
builder.Services.AddScoped<IEmailService, SmtpEmailService>();
builder.Services.AddSingleton<ChapterAudioMapCache>();
builder.Services.AddHostedService<ChapterAudioMapService>();
builder.Services.AddSingleton<AudioTokenService>();
// Session
builder.Services.AddSession(options => builder.Services.AddSession(options =>
{ {
options.IdleTimeout = TimeSpan.FromMinutes(30); options.IdleTimeout = TimeSpan.FromMinutes(30);
@ -37,7 +62,7 @@ namespace CatherineLynwood
options.Cookie.IsEssential = true; options.Cookie.IsEssential = true;
}); });
// Add authentication with cookie settings // Auth
builder.Services.AddAuthentication("MyCookieAuth") builder.Services.AddAuthentication("MyCookieAuth")
.AddCookie("MyCookieAuth", options => .AddCookie("MyCookieAuth", options =>
{ {
@ -48,78 +73,63 @@ namespace CatherineLynwood
options.ExpireTimeSpan = TimeSpan.FromHours(12); options.ExpireTimeSpan = TimeSpan.FromHours(12);
options.SlidingExpiration = true; options.SlidingExpiration = true;
}); });
builder.Services.AddAuthorization(); builder.Services.AddAuthorization();
// Add RedirectsStore as singleton // Response compression
builder.Services.AddSingleton<RedirectsStore>(sp =>
{
var logger = sp.GetRequiredService<ILogger<RedirectsStore>>();
return new RedirectsStore("redirects.json", logger);
});
// ✅ Register the book access code service
builder.Services.AddScoped<IAccessCodeService, AccessCodeService>();
builder.Services.AddScoped<IEmailService, SmtpEmailService>();
builder.Services.AddSingleton<ChapterAudioMapCache>();
builder.Services.AddHostedService<ChapterAudioMapService>();
builder.Services.AddSingleton<AudioTokenService>();
// Add response compression services
builder.Services.AddResponseCompression(options => builder.Services.AddResponseCompression(options =>
{ {
options.EnableForHttps = true; options.EnableForHttps = true;
options.Providers.Add<BrotliCompressionProvider>(); options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>(); options.Providers.Add<GzipCompressionProvider>();
}); });
builder.Services.Configure<BrotliCompressionProviderOptions>(o => o.Level = CompressionLevel.Fastest);
builder.Services.Configure<GzipCompressionProviderOptions>(o => o.Level = CompressionLevel.Fastest);
builder.Services.Configure<AbTestOptions>(builder.Configuration.GetSection("AbTest"));
builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Fastest;
});
builder.Services.Configure<GzipCompressionProviderOptions>(options => // HTML minification
builder.Services.AddWebMarkupMin(o =>
{ {
options.Level = CompressionLevel.Fastest; o.AllowMinificationInDevelopmentEnvironment = true;
});
// Add HTML minification
builder.Services.AddWebMarkupMin(options =>
{
options.AllowMinificationInDevelopmentEnvironment = true;
}) })
.AddHtmlMinification() .AddHtmlMinification()
.AddHttpCompression() .AddHttpCompression()
.AddXmlMinification() .AddXmlMinification()
.AddXhtmlMinification(); .AddXhtmlMinification();
builder.WebHost.ConfigureKestrel(options => // Kestrel limits
{ builder.WebHost.ConfigureKestrel(o => { o.Limits.MaxRequestBodySize = 40 * 1024 * 1024; });
options.Limits.MaxRequestBodySize = 40 * 1024 * 1024; // 40MB
});
var app = builder.Build(); var app = builder.Build();
// Configure the HTTP request pipeline. // Errors, HSTS
if (!app.Environment.IsDevelopment()) if (!app.Environment.IsDevelopment())
{ {
app.UseExceptionHandler("/Home/Error"); app.UseExceptionHandler("/Home/Error");
app.UseHsts(); // Adds the HSTS (HTTP Strict Transport Security) header app.UseHsts();
} }
// If behind a proxy or CDN, capture real client IPs
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
ForwardLimit = 2
});
// Your existing custom middleware
app.UseMiddleware<SpamAndSecurityMiddleware>(); app.UseMiddleware<SpamAndSecurityMiddleware>();
app.UseMiddleware<HoneypotLoggingMiddleware>(); app.UseMiddleware<HoneypotLoggingMiddleware>();
app.UseMiddleware<RedirectMiddleware>(); app.UseMiddleware<RedirectMiddleware>();
app.UseMiddleware<EnsureSidMiddleware>();
app.UseMiddleware<GeoResolutionMiddleware>();
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.UseResponseCompression(); app.UseResponseCompression();
app.UseStaticFiles(); app.UseStaticFiles();
app.UseWebMarkupMin(); app.UseWebMarkupMin();
app.UseRouting(); app.UseRouting();
app.UseSession();
// ✅ Authentication must come before Authorization app.UseSession();
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
@ -128,6 +138,7 @@ namespace CatherineLynwood
pattern: "{controller=Home}/{action=Index}/{id?}"); pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run(); app.Run();
} }
} }
} }

View File

@ -1,27 +1,37 @@
using CatherineLynwood.Models; using CatherineLynwood.Models;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace CatherineLynwood.Services namespace CatherineLynwood.Services
{ {
public interface IAccessCodeService public interface IAccessCodeService
{ {
Task<(int pageNumber, int wordIndex)> GetCurrentChallengeAsync(); #region Public Methods
Task<(int accessLevel, int highestBook)> ValidateWordAsync(string userWord);
}
Task<(int pageNumber, int wordIndex)> GetCurrentChallengeAsync();
Task<(int accessLevel, int highestBook)> ValidateWordAsync(string userWord);
#endregion Public Methods
}
public class AccessCodeService : IAccessCodeService public class AccessCodeService : IAccessCodeService
{ {
#region Private Fields
private readonly List<AccessCode> _accessCodes; private readonly List<AccessCode> _accessCodes;
#endregion Private Fields
#region Public Constructors
public AccessCodeService(DataAccess dataAccess) public AccessCodeService(DataAccess dataAccess)
{ {
_accessCodes = dataAccess.GetAccessCodes(); _accessCodes = dataAccess.GetAccessCodes();
} }
#endregion Public Constructors
#region Public Methods
public Task<(int pageNumber, int wordIndex)> GetCurrentChallengeAsync() public Task<(int pageNumber, int wordIndex)> GetCurrentChallengeAsync()
{ {
var challenge = _accessCodes.First(); // current prompt var challenge = _accessCodes.First(); // current prompt
@ -34,10 +44,11 @@ namespace CatherineLynwood.Services
c.ExpectedWord.Equals(userWord, StringComparison.OrdinalIgnoreCase)); c.ExpectedWord.Equals(userWord, StringComparison.OrdinalIgnoreCase));
if (match != null) if (match != null)
return Task.FromResult((match.AccessLevel, match.BookNumber)); return Task.FromResult((match.AccessLevel, match.BookID));
return Task.FromResult((0, 0)); return Task.FromResult((0, 0));
} }
}
#endregion Public Methods
}
} }

View File

@ -0,0 +1,20 @@
using CatherineLynwood.Models;
namespace CatherineLynwood.Services
{
public sealed class CountryContext : ICountryContext
{
private readonly IHttpContextAccessor _http;
public CountryContext(IHttpContextAccessor http)
{
_http = http;
}
public string? Iso2 =>
_http.HttpContext?.Items[HttpContextItemKeys.CountryIso2] as string;
public string? CountryName =>
_http.HttpContext?.Items[HttpContextItemKeys.CountryName] as string;
}
}

View File

@ -3,11 +3,6 @@
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Data.SqlClient; using Microsoft.Data.SqlClient;
using SixLabors.ImageSharp.Web.Commands.Converters;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Data; using System.Data;
namespace CatherineLynwood.Services namespace CatherineLynwood.Services
@ -227,7 +222,7 @@ namespace CatherineLynwood.Services
WordIndex = GetDataInt(rdr, "WordIndex"), WordIndex = GetDataInt(rdr, "WordIndex"),
ExpectedWord = GetDataString(rdr, "ExpectedWord"), ExpectedWord = GetDataString(rdr, "ExpectedWord"),
AccessLevel = GetDataInt(rdr, "AccessLevel"), AccessLevel = GetDataInt(rdr, "AccessLevel"),
BookNumber = GetDataInt(rdr, "BookNumber") BookID = GetDataInt(rdr, "BookID")
}); });
} }
} }
@ -241,6 +236,52 @@ namespace CatherineLynwood.Services
return accessCodes; return accessCodes;
} }
public async Task<ARCReaderList> GetAllARCReadersAsync()
{
ARCReaderList arcReaderList = new ARCReaderList();
using (SqlConnection conn = new SqlConnection(_connectionString))
{
using (SqlCommand cmd = new SqlCommand())
{
try
{
await conn.OpenAsync();
cmd.Connection = conn;
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandText = "GetAllARCReaders";
using (SqlDataReader rdr = await cmd.ExecuteReaderAsync())
{
while (await rdr.ReadAsync())
{
arcReaderList.Applications.Add(new ARCReaderApplication
{
HasKindleAccess = GetDataString(rdr, "HasKindleAccess"),
ContentFit = GetDataString(rdr, "ContentFit"),
Email = GetDataString(rdr, "Email"),
ExtraNotes = GetDataString(rdr, "ExtraNotes"),
FullName = GetDataString(rdr, "FullName"),
KindleEmail = GetDataString(rdr, "KindleEmail"),
Platforms = GetDataString(rdr, "Platforms"),
PlatformsOther = GetDataString(rdr, "PlatformsOther"),
PreviewChapters = GetDataString(rdr, "PreviewChapters"),
ReviewCommitment = GetDataString(rdr, "ReviewCommitment"),
ReviewLink = GetDataString(rdr, "ReviewLink"),
SubmittedAt = GetDataDate(rdr, "SubmittedAt"),
});
}
}
}
catch (Exception ex)
{
}
}
}
return arcReaderList;
}
public async Task<List<BlogSummaryResponse>> GetAllBlogsAsync() public async Task<List<BlogSummaryResponse>> GetAllBlogsAsync()
{ {
List<BlogSummaryResponse> list = new List<BlogSummaryResponse>(); List<BlogSummaryResponse> list = new List<BlogSummaryResponse>();
@ -282,52 +323,6 @@ namespace CatherineLynwood.Services
return list; return list;
} }
public async Task<ARCReaderList> GetAllARCReadersAsync()
{
ARCReaderList arcReaderList = new ARCReaderList();
using (SqlConnection conn = new SqlConnection(_connectionString))
{
using (SqlCommand cmd = new SqlCommand())
{
try
{
await conn.OpenAsync();
cmd.Connection = conn;
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandText = "GetAllARCReaders";
using (SqlDataReader rdr = await cmd.ExecuteReaderAsync())
{
while (await rdr.ReadAsync())
{
arcReaderList.Applications.Add(new ARCReaderApplication
{
ApprovedSender = GetDataString(rdr, "ApprovedSender"),
ContentFit = GetDataString(rdr, "ContentFit"),
Email = GetDataString(rdr, "Email"),
ExtraNotes = GetDataString(rdr, "ExtraNotes"),
FullName = GetDataString(rdr, "FullName"),
KindleEmail = GetDataString(rdr, "KindleEmail"),
Platforms = GetDataString(rdr, "Platforms"),
PlatformsOther = GetDataString(rdr, "PlatformsOther"),
PreviewChapters = GetDataString(rdr, "PreviewChapters"),
ReviewCommitment = GetDataString(rdr, "ReviewCommitment"),
ReviewLink = GetDataString(rdr, "ReviewLink"),
SubmittedAt = GetDataDate(rdr, "SubmittedAt"),
});
}
}
}
catch (Exception ex)
{
}
}
}
return arcReaderList;
}
public async Task<BlogAdminIndex> GetBlogAdminIndexAsync() public async Task<BlogAdminIndex> GetBlogAdminIndexAsync()
{ {
BlogAdminIndex blogAdminIndex = new BlogAdminIndex(); BlogAdminIndex blogAdminIndex = new BlogAdminIndex();
@ -548,6 +543,77 @@ namespace CatherineLynwood.Services
return blogIndex; return blogIndex;
} }
public async Task<BuyPanelViewModel> GetBuyPanelViewModel(string iso2, string title)
{
BuyPanelViewModel buyPanelViewModel = new BuyPanelViewModel();
using (SqlConnection conn = new SqlConnection(_connectionString))
{
using (SqlCommand cmd = new SqlCommand())
{
try
{
await conn.OpenAsync();
cmd.Connection = conn;
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandText = "GetBuyLinks";
cmd.Parameters.AddWithValue("@ISO2", iso2);
cmd.Parameters.AddWithValue("@Title", title);
using (SqlDataReader rdr = await cmd.ExecuteReaderAsync())
{
while (await rdr.ReadAsync())
{
buyPanelViewModel.ISO2 = GetDataString(rdr, "ISO2");
buyPanelViewModel.CountryName = GetDataString(rdr, "CountryName");
}
await rdr.NextResultAsync();
while (await rdr.ReadAsync())
{
BuyGroup buyGroup = new BuyGroup
{
BuyGroupID = GetDataInt(rdr, "BuyGroupID"),
Message = GetDataString(rdr, "Message"),
DisplayOrder = GetDataInt(rdr, "DisplayOrder"),
GroupName = GetDataString(rdr, "GroupName")
};
buyPanelViewModel.Groups.Add(buyGroup);
}
await rdr.NextResultAsync();
while (await rdr.ReadAsync())
{
BuyLink buyLink = new BuyLink
{
BuyGroupID = GetDataInt(rdr, "BuyGroupID"),
BuyLinkID = GetDataInt(rdr, "BuyLinkID"),
ISO2 = GetDataString(rdr, "ISO2"),
Price = GetDataString(rdr, "Price"),
Slug = GetDataString(rdr, "Slug"),
Target = GetDataString(rdr, "Target"),
Icon = GetDataString(rdr, "Icon"),
Text = GetDataString(rdr, "Text")
};
var group = buyPanelViewModel.Groups.Find(g => g.BuyGroupID == buyLink.BuyGroupID);
if (group != null)
{
group.Links.Add(buyLink);
}
}
}
}
catch (Exception ex)
{
}
}
}
return buyPanelViewModel;
}
public async Task<List<BlogPost>> GetDueBlogPostsAsync() public async Task<List<BlogPost>> GetDueBlogPostsAsync()
{ {
List<BlogPost> blogPosts = new List<BlogPost>(); List<BlogPost> blogPosts = new List<BlogPost>();
@ -584,6 +650,50 @@ namespace CatherineLynwood.Services
return blogPosts; return blogPosts;
} }
public async Task<GeoIpResult?> GetGeoIpAsync(string ip)
{
GeoIpResult? result = null;
using (SqlConnection conn = new SqlConnection(_connectionString))
{
using (SqlCommand cmd = new SqlCommand())
{
try
{
await conn.OpenAsync();
cmd.Connection = conn;
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandText = "GetGeoIp"; // your stored procedure name
cmd.Parameters.AddWithValue("@Ip", ip);
using (SqlDataReader rdr = await cmd.ExecuteReaderAsync())
{
if (await rdr.ReadAsync())
{
result = new GeoIpResult
{
Ip = GetDataString(rdr, "IP"),
Iso2 = GetDataString(rdr, "ISO2"),
CountryName = GetDataString(rdr, "CountryName"),
LastSeenUtc = GetDataDate(rdr, "LastSeenUtc"),
Source = GetDataString(rdr, "Source"),
UserAgent = GetDataString(rdr, "UserAgent"),
Path = GetDataString(rdr, "Path"),
City = GetDataString(rdr, "City")
};
}
}
}
catch (Exception ex)
{
// log if you want
}
}
}
return result;
}
public async Task<Questions> GetQuestionsAsync() public async Task<Questions> GetQuestionsAsync()
{ {
Questions questions = new Questions(); Questions questions = new Questions();
@ -660,7 +770,7 @@ namespace CatherineLynwood.Services
return selectListItems; return selectListItems;
} }
public async Task<Reviews> GetReviewsAsync() public async Task<Reviews> GetReviewsAsync(string title)
{ {
Reviews reviews = new Reviews(); Reviews reviews = new Reviews();
@ -674,6 +784,7 @@ namespace CatherineLynwood.Services
cmd.Connection = conn; cmd.Connection = conn;
cmd.CommandType = CommandType.StoredProcedure; cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandText = "GetReviews"; cmd.CommandText = "GetReviews";
cmd.Parameters.AddWithValue("@Title", title);
using (SqlDataReader rdr = await cmd.ExecuteReaderAsync()) using (SqlDataReader rdr = await cmd.ExecuteReaderAsync())
{ {
@ -700,6 +811,85 @@ namespace CatherineLynwood.Services
return reviews; return reviews;
} }
public async Task<List<SoundtrackTrackModel>> GetSoundtrack()
{
List<SoundtrackTrackModel> soundtrackTrackModels = new List<SoundtrackTrackModel>();
using (SqlConnection conn = new SqlConnection(_connectionString))
{
using (SqlCommand cmd = new SqlCommand())
{
try
{
await conn.OpenAsync();
cmd.Connection = conn;
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandText = "GetSoundtrack";
using (SqlDataReader rdr = await cmd.ExecuteReaderAsync())
{
while (await rdr.ReadAsync())
{
soundtrackTrackModels.Add(new SoundtrackTrackModel
{
AudioUrl = GetDataString(rdr, "AudioUrl"),
Chapter = GetDataString(rdr, "Chapter"),
Description = GetDataString(rdr, "Description"),
ImageUrl = GetDataString(rdr, "ImageUrl"),
LyricsHtml = GetDataString(rdr, "LyricsHtml"),
Title = GetDataString(rdr, "Title")
});
}
}
}
catch (Exception ex)
{
}
}
}
return soundtrackTrackModels;
}
public async Task<FlagSupportViewModel> GetTrailerLikes()
{
FlagSupportViewModel flagSupportViewModel = new FlagSupportViewModel();
using (SqlConnection conn = new SqlConnection(_connectionString))
{
using (SqlCommand cmd = new SqlCommand())
{
try
{
await conn.OpenAsync();
cmd.Connection = conn;
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandText = "GetTrailerLikes";
using (SqlDataReader rdr = await cmd.ExecuteReaderAsync())
{
while (await rdr.ReadAsync())
{
string country = GetDataString(rdr, "Country");
int likes = GetDataInt(rdr, "Likes");
flagSupportViewModel.FlagCounts.Add(new FlagCount
{
Key = country,
Value = likes
});
}
}
}
catch (Exception ex)
{
}
}
}
return flagSupportViewModel;
}
public async Task<bool> MarkAsNotifiedAsync(int blogID) public async Task<bool> MarkAsNotifiedAsync(int blogID)
{ {
bool success = true; bool success = true;
@ -727,6 +917,47 @@ namespace CatherineLynwood.Services
return success; return success;
} }
public async Task<bool> SaveARCReaderApplication(ArcReaderApplicationModel arcReaderApplication)
{
bool success = true;
using (SqlConnection conn = new SqlConnection(_connectionString))
{
using (SqlCommand cmd = new SqlCommand())
{
try
{
await conn.OpenAsync();
cmd.Connection = conn;
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandText = "SaveARCReader";
cmd.Parameters.AddWithValue("@FullName", arcReaderApplication.FullName);
cmd.Parameters.AddWithValue("@Email", arcReaderApplication.Email);
cmd.Parameters.AddWithValue("@KindleEmail", string.IsNullOrWhiteSpace(arcReaderApplication.KindleEmail) ? (object)DBNull.Value : arcReaderApplication.KindleEmail);
cmd.Parameters.AddWithValue("@HasKindleAccess", arcReaderApplication.HasKindleAccess);
cmd.Parameters.AddWithValue("@Platforms", arcReaderApplication.Platforms != null ? string.Join(",", arcReaderApplication.Platforms) : (object)DBNull.Value);
cmd.Parameters.AddWithValue("@PreviewChapters", arcReaderApplication.PreviewChapters != null ? string.Join(",", arcReaderApplication.PreviewChapters) : (object)DBNull.Value);
cmd.Parameters.AddWithValue("@PlatformsOther", string.IsNullOrWhiteSpace(arcReaderApplication.PlatformsOther) ? (object)DBNull.Value : arcReaderApplication.PlatformsOther);
cmd.Parameters.AddWithValue("@ReviewLink", string.IsNullOrWhiteSpace(arcReaderApplication.ReviewLink) ? (object)DBNull.Value : arcReaderApplication.ReviewLink);
cmd.Parameters.AddWithValue("@ContentFit", arcReaderApplication.ContentFit ?? (object)DBNull.Value);
cmd.Parameters.AddWithValue("@ReviewCommitment", arcReaderApplication.ReviewCommitment ?? (object)DBNull.Value);
cmd.Parameters.AddWithValue("@ExtraNotes", string.IsNullOrWhiteSpace(arcReaderApplication.ExtraNotes) ? (object)DBNull.Value : arcReaderApplication.ExtraNotes);
await cmd.ExecuteNonQueryAsync();
}
catch (Exception ex)
{
// Optionally log or rethrow
success = false;
}
}
}
return success;
}
public async Task<bool> SaveBlogToDatabase(BlogPostRequest blog) public async Task<bool> SaveBlogToDatabase(BlogPostRequest blog)
{ {
bool success = true; bool success = true;
@ -765,7 +996,56 @@ namespace CatherineLynwood.Services
return success; return success;
} }
public async Task<bool> SaveContact(Contact contact) public async Task<bool> SaveBuyClick(
DateTime dateTimeUtc,
string slug,
string countryGroup,
string ip,
string country,
string userAgent,
string referer,
string sessionId,
string src
)
{
bool success = true;
using (SqlConnection conn = new SqlConnection(_connectionString))
using (SqlCommand cmd = new SqlCommand())
{
try
{
await conn.OpenAsync();
cmd.Connection = conn;
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandText = "SaveBuyClick";
// Required
cmd.Parameters.AddWithValue("@DateTimeUtc", dateTimeUtc);
cmd.Parameters.AddWithValue("@Slug", (object?)slug ?? DBNull.Value);
cmd.Parameters.AddWithValue("@CountryGroup", (object?)countryGroup ?? DBNull.Value);
// Signals / attribution
cmd.Parameters.AddWithValue("@IP", (object?)ip ?? DBNull.Value);
cmd.Parameters.AddWithValue("@Country", (object?)country ?? DBNull.Value);
cmd.Parameters.AddWithValue("@UserAgent", (object?)userAgent ?? DBNull.Value);
cmd.Parameters.AddWithValue("@Referer", (object?)referer ?? DBNull.Value);
cmd.Parameters.AddWithValue("@SessionId", (object?)sessionId ?? DBNull.Value);
cmd.Parameters.AddWithValue("@Src", (object?)src ?? DBNull.Value);
await cmd.ExecuteNonQueryAsync();
}
catch (Exception)
{
success = false;
// optional: log the exception somewhere central
}
}
return success;
}
public async Task<bool> SaveContact(Contact contact, bool subscribe = false)
{ {
bool success = true; bool success = true;
@ -781,10 +1061,12 @@ namespace CatherineLynwood.Services
cmd.CommandText = "SaveContact"; cmd.CommandText = "SaveContact";
cmd.Parameters.AddWithValue("@Name", contact.Name); cmd.Parameters.AddWithValue("@Name", contact.Name);
cmd.Parameters.AddWithValue("@EmailAddress", contact.EmailAddress); cmd.Parameters.AddWithValue("@EmailAddress", contact.EmailAddress);
cmd.Parameters.AddWithValue("@Subscribe", subscribe);
await cmd.ExecuteNonQueryAsync(); await cmd.ExecuteNonQueryAsync();
} }
catch (Exception ex) catch (Exception ex)
{ {
success = false;
} }
} }
} }
@ -792,7 +1074,40 @@ namespace CatherineLynwood.Services
return success; return success;
} }
public async Task<bool> SaveARCReaderApplication(ArcReaderApplicationModel arcReaderApplication) public async Task<int> SaveFlagClick(string country)
{
int likes = 0;
using (SqlConnection conn = new SqlConnection(_connectionString))
{
using (SqlCommand cmd = new SqlCommand())
{
try
{
await conn.OpenAsync();
cmd.Connection = conn;
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandText = "SaveTrailerLike";
cmd.Parameters.AddWithValue("@Country", country);
using (SqlDataReader rdr = await cmd.ExecuteReaderAsync())
{
while (await rdr.ReadAsync())
{
likes = GetDataInt(rdr, "Likes");
}
}
}
catch (Exception ex)
{
}
}
}
return likes;
}
public async Task<bool> SaveGeoIpAsync(GeoIpResult geo)
{ {
bool success = true; bool success = true;
@ -805,24 +1120,22 @@ namespace CatherineLynwood.Services
await conn.OpenAsync(); await conn.OpenAsync();
cmd.Connection = conn; cmd.Connection = conn;
cmd.CommandType = CommandType.StoredProcedure; cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandText = "SaveARCReader"; cmd.CommandText = "SaveGeoIp"; // your stored procedure name
cmd.Parameters.AddWithValue("@FullName", arcReaderApplication.FullName);
cmd.Parameters.AddWithValue("@Email", arcReaderApplication.Email); cmd.Parameters.AddWithValue("@Ip", geo.Ip);
cmd.Parameters.AddWithValue("@KindleEmail", arcReaderApplication.KindleEmail); cmd.Parameters.AddWithValue("@Iso2", geo.Iso2);
cmd.Parameters.AddWithValue("@ApprovedSender", arcReaderApplication.ApprovedSender ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("@CountryName", geo.CountryName);
cmd.Parameters.AddWithValue("@Platforms", arcReaderApplication.Platforms != null ? string.Join(",", arcReaderApplication.Platforms) : (object)DBNull.Value); cmd.Parameters.AddWithValue("@LastSeenUtc", geo.LastSeenUtc);
cmd.Parameters.AddWithValue("@PreviewChapters", arcReaderApplication.PreviewChapters != null ? string.Join(",", arcReaderApplication.PreviewChapters) : (object)DBNull.Value); cmd.Parameters.AddWithValue("@Source", geo.Source ?? (object)DBNull.Value);
cmd.Parameters.AddWithValue("@PlatformsOther", string.IsNullOrWhiteSpace(arcReaderApplication.PlatformsOther) ? (object)DBNull.Value : arcReaderApplication.PlatformsOther); cmd.Parameters.AddWithValue("@UserAgent", geo.UserAgent ?? (object)DBNull.Value);
cmd.Parameters.AddWithValue("@ReviewLink", string.IsNullOrWhiteSpace(arcReaderApplication.ReviewLink) ? (object)DBNull.Value : arcReaderApplication.ReviewLink); cmd.Parameters.AddWithValue("@Path", geo.Path ?? (object)DBNull.Value);
cmd.Parameters.AddWithValue("@ContentFit", arcReaderApplication.ContentFit ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("@QueryString", geo.QueryString ?? (object)DBNull.Value);
cmd.Parameters.AddWithValue("@ReviewCommitment", arcReaderApplication.ReviewCommitment ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("@City", geo.City ?? (object)DBNull.Value);
cmd.Parameters.AddWithValue("@ExtraNotes", string.IsNullOrWhiteSpace(arcReaderApplication.ExtraNotes) ? (object)DBNull.Value : arcReaderApplication.ExtraNotes);
await cmd.ExecuteNonQueryAsync(); await cmd.ExecuteNonQueryAsync();
} }
catch (Exception ex) catch (Exception ex)
{ {
// Optionally log or rethrow
success = false; success = false;
} }
} }
@ -831,7 +1144,6 @@ namespace CatherineLynwood.Services
return success; return success;
} }
public async Task<bool> UpdateBlogAsync(Blog blog) public async Task<bool> UpdateBlogAsync(Blog blog)
{ {
bool success = true; bool success = true;

View File

@ -0,0 +1,82 @@
using CatherineLynwood.Models;
using Microsoft.Extensions.Caching.Memory;
using System.Net;
using System.Text.Json;
namespace CatherineLynwood.Services
{
public sealed class GeoResolver : IGeoResolver
{
private readonly IMemoryCache _cache;
private readonly DataAccess _dataAccess;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<GeoResolver> _logger;
private static readonly TimeSpan CacheTtl = TimeSpan.FromHours(6);
public GeoResolver(IMemoryCache cache, DataAccess dataAccess, IHttpClientFactory httpClientFactory, ILogger<GeoResolver> logger)
{
_cache = cache;
_dataAccess = dataAccess;
_httpClientFactory = httpClientFactory;
_logger = logger;
}
public async Task<GeoIpResult?> ResolveAsync(IPAddress ip, string path, string queryString, string userAgent, CancellationToken ct = default)
{
var ipStr = ip.ToString();
if (_cache.TryGetValue<GeoIpResult>(ipStr, out var cached))
return cached;
// 1) Database lookup
var db = await _dataAccess.GetGeoIpAsync(ipStr);
if (db is not null)
{
_cache.Set(ipStr, db, CacheTtl);
return db;
}
// 2) External API fallback
try
{
var client = _httpClientFactory.CreateClient(nameof(GeoResolver));
var json = await client.GetStringAsync($"http://ip-api.com/json/{ipStr}", ct);
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
if (root.GetProperty("status").GetString() == "success")
{
var iso2 = root.GetProperty("countryCode").GetString() ?? "UK";
var country = root.GetProperty("country").GetString();
var city = root.GetProperty("city").GetString() ?? "";
var geo = new GeoIpResult
{
Ip = ipStr,
Iso2 = iso2,
CountryName = country ?? "",
LastSeenUtc = DateTime.UtcNow,
Source = "ip-api",
UserAgent = userAgent,
Path = path,
QueryString = queryString,
City = city
};
await _dataAccess.SaveGeoIpAsync(geo);
_cache.Set(ipStr, geo, CacheTtl);
return geo;
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Geo lookup failed for {IP}", ipStr);
}
return null;
}
}
}

View File

@ -0,0 +1,13 @@
namespace CatherineLynwood.Services
{
public interface ICountryContext
{
#region Public Properties
string? CountryName { get; }
string? Iso2 { get; }
#endregion Public Properties
}
}

View File

@ -0,0 +1,11 @@
using CatherineLynwood.Models;
using System.Net;
namespace CatherineLynwood.Services
{
public interface IGeoResolver
{
Task<GeoIpResult?> ResolveAsync(IPAddress ip, string path, string queryString, string userAgent, CancellationToken ct = default);
}
}

View File

@ -12,7 +12,7 @@ namespace CatherineLynwood.Services
{ {
public interface IEmailService public interface IEmailService
{ {
Task SendEmailAsync( string subject, string plainText, string htmlContent, Contact contact); Task SendEmailAsync( string subject, string plainText, string htmlContent, Contact contact, bool sendToUser = false);
} }
public class SmtpEmailService : IEmailService public class SmtpEmailService : IEmailService
@ -24,12 +24,22 @@ namespace CatherineLynwood.Services
_config = config; _config = config;
} }
public async Task SendEmailAsync(string subject, string plainText, string htmlContent, Contact contact) public async Task SendEmailAsync(string subject, string plainText, string htmlContent, Contact contact, bool sendToUser = false)
{ {
var message = new MimeMessage(); var message = new MimeMessage();
message.From.Add(new MailboxAddress("Catherine Lynwood", _config["Smtp:Sender"])); message.From.Add(new MailboxAddress("Catherine Lynwood", _config["Smtp:Sender"]));
if (sendToUser)
{
message.To.Add(new MailboxAddress(contact.Name, contact.EmailAddress));
message.Bcc.Add(new MailboxAddress("Web Site", "catherine@catherinelynwood.com"));
}
else
{
message.To.Add(new MailboxAddress("Web Site", "catherine@catherinelynwood.com")); message.To.Add(new MailboxAddress("Web Site", "catherine@catherinelynwood.com"));
message.ReplyTo.Add(new MailboxAddress(contact.Name, contact.EmailAddress)); message.ReplyTo.Add(new MailboxAddress(contact.Name, contact.EmailAddress));
}
message.Subject = subject; message.Subject = subject;

View File

@ -8,27 +8,42 @@ namespace CatherineLynwood.TagHelpers
#region Public Properties #region Public Properties
public DateTime? ArticleModifiedTime { get; set; } public DateTime? ArticleModifiedTime { get; set; }
public DateTime? ArticlePublishedTime { get; set; } public DateTime? ArticlePublishedTime { get; set; }
public string MetaAuthor { get; set; } public string MetaAuthor { get; set; }
public string MetaDescription { get; set; } public string MetaDescription { get; set; }
public string MetaImage { get; set; } public string MetaImage { get; set; }
public string MetaImageAlt { get; set; } public string MetaImageAlt { get; set; }
public string MetaImagePNG { get; set; }
public string MetaKeywords { get; set; } public string MetaKeywords { get; set; }
public string MetaTitle { get; set; } public string MetaTitle { get; set; }
public string MetaUrl { get; set; } public string MetaUrl { get; set; }
public string OgSiteName { get; set; } public string OgSiteName { get; set; }
public string OgType { get; set; } = "article"; public string OgType { get; set; } = "article";
public string TwitterCardType { get; set; } = "summary_large_image"; public string TwitterCardType { get; set; } = "summary_large_image";
public string TwitterCreatorHandle { get; set; } public string TwitterCreatorHandle { get; set; }
public int? TwitterPlayerHeight { get; set; } public int? TwitterPlayerHeight { get; set; }
public int? TwitterPlayerWidth { get; set; } public int? TwitterPlayerWidth { get; set; }
public string TwitterSiteHandle { get; set; } public string TwitterSiteHandle { get; set; }
public string TwitterVideoUrl { get; set; } public string TwitterVideoUrl { get; set; }
#endregion #endregion Public Properties
#region Public Methods #region Public Methods
@ -63,6 +78,9 @@ namespace CatherineLynwood.TagHelpers
if (!string.IsNullOrWhiteSpace(MetaImage)) if (!string.IsNullOrWhiteSpace(MetaImage))
metaTags.AppendLine($"<meta property=\"og:image\" content=\"{MetaImage}\">"); metaTags.AppendLine($"<meta property=\"og:image\" content=\"{MetaImage}\">");
if (!string.IsNullOrWhiteSpace(MetaImagePNG))
metaTags.AppendLine($"<meta property=\"og:image\" content=\"{MetaImagePNG}\">");
if (!string.IsNullOrWhiteSpace(MetaImageAlt)) if (!string.IsNullOrWhiteSpace(MetaImageAlt))
metaTags.AppendLine($"<meta property=\"og:image:alt\" content=\"{MetaImageAlt}\">"); metaTags.AppendLine($"<meta property=\"og:image:alt\" content=\"{MetaImageAlt}\">");
@ -124,6 +142,6 @@ namespace CatherineLynwood.TagHelpers
output.Content.SetHtmlContent(metaTags.ToString()); output.Content.SetHtmlContent(metaTags.ToString());
} }
#endregion #endregion Public Methods
} }
} }

View File

@ -2,9 +2,11 @@
using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.AspNetCore.Razor.TagHelpers;
using SixLabors.Fonts; using SixLabors.Fonts;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Processing;
namespace CatherineLynwood.TagHelpers namespace CatherineLynwood.TagHelpers
{ {

View File

@ -35,7 +35,7 @@
<p class="card-text mb-1"><strong>Email:</strong> @item.Email</p> <p class="card-text mb-1"><strong>Email:</strong> @item.Email</p>
<p class="card-text mb-1"><strong>Kindle Email:</strong> @item.KindleEmail</p> <p class="card-text mb-1"><strong>Kindle Email:</strong> @item.KindleEmail</p>
<p class="card-text mb-1"><strong>Approved Sender:</strong> @item.ApprovedSender</p> <p class="card-text mb-1"><strong>Has Kindle Access:</strong> @item.HasKindleAccess</p>
<p class="card-text mb-1"><strong>Platforms:</strong> @item.Platforms</p> <p class="card-text mb-1"><strong>Platforms:</strong> @item.Platforms</p>
@if (!string.IsNullOrWhiteSpace(item.PlatformsOther)) @if (!string.IsNullOrWhiteSpace(item.PlatformsOther))
{ {

View File

@ -0,0 +1,134 @@
@{
ViewData["Title"] = "Larhysa Saddul Voice of The Alpha Flame: Discovery";
}
<div class="container">
<div class="row">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
<li class="breadcrumb-item active" aria-current="page">Larhysa Saddul</li>
</ol>
</nav>
</div>
</div>
<!-- Header / Hero -->
<div class="row align-items-center mb-5">
<div class="col-lg-7">
<h1 class="display-5 fw-bold mb-3">
Larhysa Saddul
</h1>
<p class="lead mb-3">
Actress, singer, and voice artist the voice of
<span class="fw-semibold">The Alpha Flame: Discovery</span>.
</p>
<p class="text-muted mb-4">
With a background on stage, on screen, and in the studio, Larhysa brings
depth, warmth, and emotional honesty to every performance, from sharp-edged
comedy to powerful, character-driven drama.
</p>
<a asp-controller="Discovery" asp-action="Index" class="btn btn-primary me-2 mb-2">
About <em>The Alpha Flame: Discovery</em>
</a>
@* <a asp-controller="Discovery" asp-action="AudioBook" class="btn btn-outline-secondary mb-2">
Audiobook information
</a> *@
<!-- Bio section -->
<h2 class="h3 mb-3">About Larhysa</h2>
<p>
Larhysa Saddul is an accomplished actress, singer, and voice artist whose career
spans more than a decade across stage, screen, and studio. With roots in theatre
and musical performance, she has built a varied body of work that showcases her
vocal range, comic timing, and emotional depth.
</p>
<p>
On stage, she is known for standout roles such as the Evil Stepmother in
<em>CinderAliyah</em>, where she performed alongside BBCs Abdullah Afzal, and for
her work in the acclaimed, five-star Edinburgh Fringe production
<em>NewsRevue</em>, contributing as both performer and writer. Her blend of sharp
character work and musical storytelling has made her a natural fit for
contemporary, fast-paced theatre.
</p>
<p>
Building on her acting experience and instinct for story, Larhysa has expanded her
creative work into voice acting, bringing characters and narratives to life with
authenticity, warmth, and emotional nuance. Her narration of Catherine Lynwoods
psychological crime novel <em>The Alpha Flame: Discovery</em> marks an exciting
new chapter in her career.
</p>
<p>
Through this performance, Larhysa seeks to honour the storys powerful themes of
justice, sisterhood, and survival, giving listeners an intimate way into Maggie
and Beths world and the complex emotional landscape they inhabit.
</p>
</div>
<div class="col-lg-5">
<responsive-image src="larhysa-saddul.png" alt="Larhysa Saddul" class="img-fluid rounded-5 border border-3 border-dark shadow-lg" display-width-percentage="100"></responsive-image>
<p class="small text-muted mt-2">
Images courtesy of Larhysa Saddul, used with permission.
</p>
</div>
</div>
<!-- Audio: Interview -->
<div class="row mb-5">
<div class="col-lg-8 mb-3">
<responsive-image src="larhysa-saddul-in-action.png" alt="Larhysa Saddul" class="img-fluid rounded-5 border border-3 border-dark shadow-lg mb-3" display-width-percentage="100"></responsive-image>
<div class="card border-0 bg-dark text-white shadow-sm">
<div class="card-body">
<h3 class="h5 mb-3">Hear Larhysa in action</h3>
<p class="mb-3">
Here are a few excerpts from Larhysa's excellent narration of The Alphe Flame: Discovery.
</p>
<audio controls class="w-100">
<source src="~/audio/excerpts-1.mp3" type="audio/mpeg" />
Your browser does not support the audio element.
</audio>
<p class="small text-white mt-2">
Excerpt taken from Chapter 25
</p>
<audio controls class="w-100">
<source src="~/audio/excerpts-3.mp3" type="audio/mpeg" />
Your browser does not support the audio element.
</audio>
<p class="small text-white mt-2">
Excerpt taken from Chapter 32
</p>
</div>
</div>
</div>
<div class="col-lg-4 mb-3">
<div class="card border-0 bg-light h-100">
<div class="card-body d-flex flex-column justify-content-center">
<h2 class="h3 mb-3">Interview: Catherine &amp; Larhysa</h2>
<p>
In this in-depth conversation, author Catherine Lynwood and Larhysa Saddul talk
about adapting <em>The Alpha Flame: Discovery</em> for audio, the emotional weight
of the story, and the craft that goes into bringing Maggie and Beth to life in
the recording booth.
</p>
<p class="fw-semibold mb-2">
Listen to the full interview
</p>
<audio controls class="w-100">
<source src="~/audio/larhysa-Interview.mp3" type="audio/mpeg" />
Your browser does not support the audio element.
</audio>
<p class="small text-muted mt-2 mb-0">
Duration: ~25 minutes.
</p>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,273 @@
@{
ViewData["Title"] = "The Alpha Flame: Discovery Audiobook";
}
<div class="container">
<div class="row">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
<li class="breadcrumb-item active" aria-current="page">Audiobook</li>
</ol>
</nav>
</div>
</div>
<!-- Hero -->
<div class="row align-items-center g-4 mb-4 mb-lg-5">
<div class="col-12 col-lg-7">
<h1 class="display-6 fw-bold mb-2">Listen to <span class="text-nowrap">The Alpha Flame: Discovery</span></h1>
<p class="lead mb-3">
A slow-burn, character-driven story that hits differently through headphones.
</p>
<div class="d-flex flex-wrap gap-2 mb-3">
<span class="badge text-bg-secondary">🎧 17.5 hours</span>
<span class="badge text-bg-secondary">🎙️ Fully narrated</span>
<span class="badge text-bg-secondary">🔒 Exclusive to Audible</span>
</div>
<div class="d-flex flex-wrap gap-2">
<a class="btn btn-primary btn-lg" target="_blank" href="https://www.audible.co.uk/pd/B0GL4NGG9F/?source_code=AUKFrDlWS02231890H6-BK-ACX0-493566&amp;ref=acx_bty_BK_ACX0_493566_rh_uk" ping="/track/click?slug=audible-uk&amp;src=AudioBook" rel="nofollow noindex">
<span><i class="fab fa-amazon me-1"></i> Listen on Audible</span>
</a>
<a class="btn btn-outline-secondary btn-lg" href="#clips">
Try the clips
</a>
</div>
<p class="text-white small mt-3 mb-0">
Tip: Try one minute. If the voice doesnt pull you in, close the page and go back to doomscrolling.
</p>
</div>
<div class="col-12 col-lg-5">
<!-- Optional cover / visual -->
<div class="card shadow-sm">
<div class="card-body">
<div class="d-flex gap-3 align-items-center">
<responsive-image src="the-alpha-flame-discovery-audiobook.png"
class="img-fluid rounded"
alt="The Alpha Flame Audiobook Cover — gritty 1980s Birmingham crime novel about twin sisters uncovering secrets and surviving abuse"
display-width-percentage="20" style="max-width: 120px;"></responsive-image>
<div>
<div class="fw-semibold">Narration</div>
<div class="small mb-2">Narrated by <strong><a asp-controller="Collaborations" asp-action="LarhysaSaddul">Lahrysa Saddul</a></strong></div>
<ul class="list-unstyled small mb-0">
<li class="mb-1">• Best for: drives, late nights, walks</li>
<li class="mb-1">• Style: intimate, story-first, no gimmicks</li>
<li class="mb-0">• No spoilers in the clips below</li>
</ul>
</div>
</div>
</div>
</div>
<div class="alert alert-dark border rounded-3 mt-3 mb-0">
<div class="small">
<strong>Already read it?</strong>
The audiobook isnt a repeat, its a different experience. Scenes land differently when you cant rush them.
</div>
</div>
</div>
</div>
<!-- Listening contexts -->
<div class="row g-3 mb-4">
<div class="col-12 col-md-4">
<div class="card h-100 shadow-sm">
<div class="card-body">
<div class="fw-semibold mb-2">🎧 Best with headphones</div>
<div class="text-muted small">
This is the kind of narration where quiet details matter.
</div>
</div>
</div>
</div>
<div class="col-12 col-md-4">
<div class="card h-100 shadow-sm">
<div class="card-body">
<div class="fw-semibold mb-2">🌙 Best when its late</div>
<div class="text-muted small">
When your brain is tired and your eyes refuse to read another page.
</div>
</div>
</div>
</div>
<div class="col-12 col-md-4">
<div class="card h-100 shadow-sm">
<div class="card-body">
<div class="fw-semibold mb-2">🚗 Best on long drives</div>
<div class="text-muted small">
Perfect for motorways, rain, and the kind of silence that gets loud.
</div>
</div>
</div>
</div>
</div>
<!-- Clips -->
<div id="clips" class="mb-4">
<div class="d-flex align-items-end justify-content-between flex-wrap gap-2 mb-2">
<h2 class="h3 mb-0">Audio clips</h2>
<span class="text-white small">Short, spoiler-safe moments to sample the voice and tone.</span>
</div>
<div class="row g-3">
<!-- Clip card 1 -->
<div class="col-12 col-lg-4">
<div class="card h-100 shadow-sm">
<div class="card-body">
<div class="text-uppercase small text-muted mb-1">Moment 1</div>
<h3 class="h5 mb-2">“She saved me.”</h3>
<p class="small text-muted mb-3">
Shock hasnt worn off yet. Voices are low. One truth slips out that changes how everyone in the room sees her.
</p>
<audio class="w-100" controls preload="none">
<source src="/audio/discovery-clip-1.mp3" type="audio/mpeg" />
Your browser does not support the audio element.
</audio>
<div class="d-flex justify-content-between mt-2 small text-muted">
<span>⏱ 0:41</span>
<span>Emotion-led, post-shock</span>
</div>
</div>
</div>
</div>
<!-- Clip card 2 -->
<div class="col-12 col-lg-4">
<div class="card h-100 shadow-sm">
<div class="card-body">
<div class="text-uppercase small text-muted mb-1">Moment 2</div>
<h3 class="h5 mb-2">Voices through a door</h3>
<p class="small text-muted mb-3">
A party outside. A quiet room inside. And words that were never meant to be overheard.
</p>
<audio class="w-100" controls preload="none">
<source src="/audio/discovery-clip-2.mp3" type="audio/mpeg" />
Your browser does not support the audio element.
</audio>
<div class="d-flex justify-content-between mt-2 small text-muted">
<span>⏱ 1:06</span>
<span>Rising unease</span>
</div>
</div>
</div>
</div>
<!-- Clip card 3 -->
<div class="col-12 col-lg-4">
<div class="card h-100 shadow-sm">
<div class="card-body">
<div class="text-uppercase small text-muted mb-1">Moment 3</div>
<h3 class="h5 mb-2">Betrayal doesnt shout</h3>
<p class="small text-muted mb-3">
Accusations come out sideways. Trust fractures in a place no one ever meant to stop.
</p>
<audio class="w-100" controls preload="none">
<source src="/audio/discovery-clip-3.mp3" type="audio/mpeg" />
Your browser does not support the audio element.
</audio>
<div class="d-flex justify-content-between mt-2 small text-muted">
<span>⏱ 0:54</span>
<span>Raw, confrontational</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- What you get (audio-specific, not plot blurb) -->
<div class="row g-3 mb-4">
<div class="col-12 col-lg-7">
<div class="card shadow-sm">
<div class="card-body">
<h2 class="h4 mb-3">What this audiobook is</h2>
<ul class="mb-0">
<li class="mb-2">A story you <strong>live inside</strong>, not one you sprint through.</li>
<li class="mb-2">Character-led tension, emotional weight, and atmosphere that builds.</li>
<li class="mb-2">Narration that gives scenes room to breathe, pauses included.</li>
<li class="mb-0">Ideal if you like long, immersive listens rather than quick hits.</li>
</ul>
</div>
</div>
</div>
<div class="col-12 col-lg-5">
<div class="card shadow-sm">
<div class="card-body">
<h2 class="h4 mb-3">Quick details</h2>
<dl class="row mb-0">
<dt class="col-5 text-muted">Length</dt>
<dd class="col-7">17.5 hours</dd>
<dt class="col-5 text-muted">Format</dt>
<dd class="col-7">Audible audiobook</dd>
<dt class="col-5 text-muted">Availability</dt>
<dd class="col-7">Exclusive to Audible</dd>
<dt class="col-5 text-muted">Best for</dt>
<dd class="col-7">Headphones, drives, late nights</dd>
</dl>
</div>
</div>
</div>
</div>
<!-- CTA Footer -->
<div class="card border-0 shadow-sm">
<div class="card-body d-flex flex-column flex-md-row align-items-start align-items-md-center justify-content-between gap-3">
<div>
<div class="h5 mb-1">Ready to listen?</div>
<div class="text-muted small mb-0">
Click through to Audible. If youre a member, a credit is usually less painful than buying hardbacks at full price.
</div>
</div>
<div class="d-flex gap-2">
<a class="btn btn-dark btn-lg" target="_blank" href="https://www.audible.co.uk/pd/B0GL4NGG9F/?source_code=AUKFrDlWS02231890H6-BK-ACX0-493566&amp;ref=acx_bty_BK_ACX0_493566_rh_uk" ping="/track/click?slug=audible-uk&amp;src=AudioBook" rel="nofollow noindex">
<span><i class="fab fa-amazon me-1"></i> Listen on Audible</span>
</a>
<a class="btn btn-outline-secondary btn-lg" href="#clips">
Play a clip
</a>
</div>
</div>
</div>
</div>
@section Scripts{
<script>
document.addEventListener('DOMContentLoaded', function () {
const audioPlayers = document.querySelectorAll('#clips audio');
audioPlayers.forEach(player => {
player.addEventListener('play', () => {
audioPlayers.forEach(otherPlayer => {
if (otherPlayer !== player && !otherPlayer.paused) {
otherPlayer.pause();
otherPlayer.currentTime = 0;
}
});
});
});
});
</script>
}

View File

@ -1,7 +1,7 @@
@{ @{
ViewData["Title"] = "The Alpha Flame: Discovery Chapter 1"; ViewData["Title"] = "The Alpha Flame: Discovery Chapter 1";
} }
<div class="container">
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
@ -76,6 +76,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
@section Scripts{ @section Scripts{
<script> <script>

View File

@ -2,6 +2,7 @@
ViewData["Title"] = "The Alpha Flame: Discovery Chapter 13"; ViewData["Title"] = "The Alpha Flame: Discovery Chapter 13";
} }
<div class="container">
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
@ -92,6 +93,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
@section Scripts{ @section Scripts{
<script> <script>

View File

@ -2,6 +2,7 @@
ViewData["Title"] = "The Alpha Flame: Discovery Chapter 2"; ViewData["Title"] = "The Alpha Flame: Discovery Chapter 2";
} }
<div class="container">
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
@ -75,6 +76,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
@section Scripts{ @section Scripts{
<script> <script>

View File

@ -9,7 +9,7 @@
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li> <li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li> <li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li> <li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Extras">Extras</a></li> <li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Extras">Extras</a></li>
<li class="breadcrumb-item active" aria-current="page">Epilogue</li> <li class="breadcrumb-item active" aria-current="page">Epilogue</li>
</ol> </ol>
</nav> </nav>

View File

@ -1,8 +1,8 @@
@{ @{
ViewData["Title"] = "Extras"; ViewData["Title"] = "The Alpha Flame: Discovery Extras";
int? accessLevel = Context.Session.GetInt32("BookAccessLevel"); int? accessLevel = Context.Session.GetInt32("BookAccessLevel");
int? accessBook = Context.Session.GetInt32("BookAccessMax"); int? accessBook = Context.Session.GetInt32("BookAccessMax");
} }
<div class="row"> <div class="row">
@ -19,10 +19,7 @@
</div> </div>
<div class="container mt-5"> <div class="container mt-5">
<h1 class="extras-header">Your Exclusive Extras</h1> <h1 class="extras-header">The Alpha Flame Discovery Exclusive Extras</h1>
@if (accessBook == 1)
{
<div class="extras-grid mt-4"> <div class="extras-grid mt-4">
@if (accessLevel >= 1) @if (accessLevel >= 1)
{ {
@ -30,7 +27,7 @@
<div class="card-body"> <div class="card-body">
<h5 class="card-title">Epilogue</h5> <h5 class="card-title">Epilogue</h5>
<p class="card-text">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</p> <p class="card-text">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</p>
<a asp-controller="Discovery" asp-action="Epilogue" class="btn btn-dark btn-sm">Read or Listen</a> <a asp-action="Epilogue" class="btn btn-dark btn-sm">Read or Listen</a>
</div> </div>
</div> </div>
} }
@ -40,7 +37,15 @@
<div class="card-body"> <div class="card-body">
<h5 class="card-title">Discovery Scrap Book</h5> <h5 class="card-title">Discovery Scrap Book</h5>
<p class="card-text">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.</p> <p class="card-text">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.</p>
<a asp-controller="Discovery" asp-action="ScrapBook" class="btn btn-dark btn-sm">View Scrapbook</a> <a asp-action="ScrapBook" class="btn btn-dark btn-sm">View Scrapbook</a>
</div>
</div>
<div class="card extra-card">
<div class="card-body">
<h5 class="card-title">Discovery Soundtrack</h5>
<p class="card-text">Have a listen to The Alpha Flame soundtrack. A selection of original songs written by me and put to music.</p>
<a asp-action="Soundtrack" class="btn btn-dark btn-sm">Soundtrack</a>
</div> </div>
</div> </div>
} }
@ -50,7 +55,7 @@
<div class="card-body"> <div class="card-body">
<h5 class="card-title">Listen to The Alpha Flame: Discovery</h5> <h5 class="card-title">Listen to The Alpha Flame: Discovery</h5>
<p class="card-text">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.</p> <p class="card-text">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.</p>
<a asp-controller="Discovery" asp-action="Listen" class="btn btn-dark btn-sm">Listen to the Book</a> <a asp-action="Listen" class="btn btn-dark btn-sm">Listen to the Book</a>
</div> </div>
</div> </div>
@ -58,107 +63,11 @@
<div class="card-body"> <div class="card-body">
<h5 class="card-title">Scrapbook: Maggies Designs</h5> <h5 class="card-title">Scrapbook: Maggies Designs</h5>
<p class="card-text">Flip through Maggies sketches, fashion notes, and photos from her original designs including the infamous red skirt.</p> <p class="card-text">Flip through Maggies sketches, fashion notes, and photos from her original designs including the infamous red skirt.</p>
<a asp-controller="Discovery" asp-action="MaggiesDesigns" class="btn btn-dark btn-sm">View Scrapbook</a> <a asp-action="MaggiesDesigns" class="btn btn-dark btn-sm">View Scrapbook</a>
</div> </div>
</div> </div>
} }
</div> </div>
}
else if (accessBook == 2)
{
<div class="extras-grid mt-4">
@if (accessLevel >= 1)
{
}
else if (accessLevel >= 2)
{
}
else if (accessLevel >= 3)
{
}
else if(accessLevel >= 4)
{
}
<div class="card extra-card">
<div class="card-body">
<h5 class="card-title">Epilogue</h5>
<p class="card-text">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</p>
<a asp-controller="Discovery" asp-action="Epilogue" class="btn btn-dark btn-sm">Read or Listen</a>
</div>
</div>
<div class="card extra-card">
<div class="card-body">
<h5 class="card-title">Discovery Scrap Book</h5>
<p class="card-text">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.</p>
<a asp-controller="Discovery" asp-action="ScrapBook" class="btn btn-dark btn-sm">View Scrapbook</a>
</div>
</div>
<div class="card extra-card">
<div class="card-body">
<h5 class="card-title">Rubery Hill Photo Archive</h5>
<p class="card-text">Explore historical photos and floor plans of the real Rubery Hill Hospital, the eerie inspiration behind key scenes.</p>
<a href="/extras/rubery-hill-photos" class="btn btn-dark btn-sm">Explore</a>
</div>
</div>
<div class="card extra-card">
<div class="card-body">
<h5 class="card-title">Scrapbook: Maggies Designs</h5>
<p class="card-text">Flip through Maggies sketches, fashion notes, and photos from her original designs including the infamous red skirt.</p>
<a asp-controller="Discovery" asp-action="MaggiesDesigns" class="btn btn-dark btn-sm">View Scrapbook</a>
</div>
</div>
</div>
}
else if (accessBook == 3)
{
<div class="extras-grid mt-4">
<div class="card extra-card">
<div class="card-body">
<h5 class="card-title">Epilogue</h5>
<p class="card-text">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</p>
<a asp-controller="Discovery" asp-action="Epilogue" class="btn btn-dark btn-sm">Read or Listen</a>
</div>
</div>
<div class="card extra-card">
<div class="card-body">
<h5 class="card-title">Discovery Scrap Book</h5>
<p class="card-text">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.</p>
<a asp-controller="Discovery" asp-action="ScrapBook" class="btn btn-dark btn-sm">View Scrapbook</a>
</div>
</div>
<div class="card extra-card">
<div class="card-body">
<h5 class="card-title">Rubery Hill Photo Archive</h5>
<p class="card-text">Explore historical photos and floor plans of the real Rubery Hill Hospital, the eerie inspiration behind key scenes.</p>
<a href="/extras/rubery-hill-photos" class="btn btn-dark btn-sm">Explore</a>
</div>
</div>
<div class="card extra-card">
<div class="card-body">
<h5 class="card-title">Scrapbook: Maggies Designs</h5>
<p class="card-text">Flip through Maggies sketches, fashion notes, and photos from her original designs including the infamous red skirt.</p>
<a asp-controller="Discovery" asp-action="MaggiesDesigns" class="btn btn-dark btn-sm">View Scrapbook</a>
</div>
</div>
</div>
}
</div> </div>
@section Meta{ @section Meta{

View File

@ -0,0 +1,165 @@
@{
ViewData["Title"] = "How to Buy The Alpha Flame: Discovery";
}
<div class="container">
<div class="row">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
<li class="breadcrumb-item active" aria-current="page">How to Buy</li>
</ol>
</nav>
</div>
</div>
<h1 class="mb-4">How to Buy <span class="fw-light">The Alpha Flame: Discovery</span></h1>
<p class="lead">There are several ways to enjoy the book — whether you prefer digital, print, or audio. If you'd like to support the author directly, the <strong>direct links</strong> below are the best way to do so.</p>
<!-- eBook -->
<div class="card mb-4">
<div class="card-header">
<i class="fad fa-tablet-alt text-primary me-2"></i> eBook (Kindle)
</div>
<div class="card-body">
<p>The Kindle edition is available via your local Amazon store:</p>
<a id="kindleLink" href="https://www.amazon.com/dp/B0FBS427VD" class="btn btn-outline-dark mb-2" target="_blank">
Buy Kindle Edition
</a>
<p class="small text-muted">Automatically redirects based on your country.</p>
</div>
</div>
<!-- Paperback -->
<div class="card mb-4">
<div class="card-header">
<i class="fad fa-book text-success me-2"></i> Paperback (Bookshop Edition)
</div>
<div class="card-body">
<p>
This version is designed for local bookstores and global retailers.
</p>
<!-- IngramSpark direct paperback link placeholder -->
<a id="paperbackLinkSelf" href="https://shop.ingramspark.com/b/084?params=6easpH54PaugzXFKdF4Tu4Izb0cvkMqbj3ZNlaYBKMJ" class="btn btn-outline-dark mb-2" target="_blank">
📦 Buy Direct (Save & Support Author)
</a>
<a id="paperbackLink" href="https://www.amazon.com/dp/1068225815" class="btn btn-outline-dark mb-2" target="_blank">
Buy on Amazon
</a>
<p class="small text-muted mb-0">ISBN 978-1-0682258-1-9</p>
<p class="small text-muted" id="extraRetailers"></p>
</div>
</div>
<!-- Hardback -->
<div class="card mb-4">
<div class="card-header">
<i class="fad fa-gem text-danger me-2"></i> Collectors Edition (Hardback)
</div>
<div class="card-body">
<p>
A premium collectors hardback edition, available via bookstores and online.
</p>
<!-- IngramSpark direct hardback link placeholder -->
<a id="hardbackLinkSelf" href="https://shop.ingramspark.com/b/084?params=GC1p1c8b66Rhfoy6Tq97SJmmhdZSEYuxBcCY5zxNstO" class="btn btn-outline-dark mb-2" target="_blank">
💎 Buy Direct (Save & Support Author)
</a>
<a id="hardbackLink" href="https://www.amazon.com/dp/1068225807" class="btn btn-outline-dark mb-2" target="_blank">
Buy on Amazon
</a>
<p class="small text-muted mb-0">ISBN 978-1-0682258-0-2</p>
<p class="small text-muted" id="extraRetailersHardback"></p>
</div>
</div>
<!-- Audiobook -->
<div class="card mb-4">
<div class="card-header">
<i class="fad fa-headphones-alt text-info me-2"></i> Audiobook (AI-Read)
</div>
<div class="card-body">
<p class="mb-2">Listen to the entire book for free on ElevenLabs (Elevenlabs subscription required):</p>
<a href="https://elevenreader.io/audiobooks/the-alpha-flame/e4Ppi7wLTLGOLrWe3Y6q?voiceId=Xb7hH8MSUJpSbSDYk0k2" class="btn btn-outline-dark mb-3" target="_blank">
🎧 Listen on ElevenLabs
</a>
<br />
</div>
</div>
</div>
@section Scripts{
<!-- Geo-based link adjustment -->
<script>
fetch('https://ipapi.co/json/')
.then(response => response.json())
.then(data => {
const country = data.country_code;
let kindleLink = "https://www.amazon.com/dp/B0FBS427VD";
let paperbackLink = "https://www.amazon.com/dp/1068225815";
let hardbackLink = "https://www.amazon.com/dp/1068225807";
let extraRetailers = "";
let extraRetailersHardback = "";
switch (country) {
case "GB":
kindleLink = "https://www.amazon.co.uk/dp/B0FBS427VD";
paperbackLink = "https://www.amazon.co.uk/dp/1068225815";
hardbackLink = "https://www.amazon.co.uk/dp/1068225807";
extraRetailers = 'Also available at <a href="https://www.waterstones.com/book/the-alpha-flame/catherine-lynwood/9781068225819" target="_blank">Waterstones</a>';
extraRetailersHardback = 'Also available at <a href="https://www.waterstones.com/book/the-alpha-flame/catherine-lynwood/9781068225802" target="_blank">Waterstones</a>';
break;
case "US":
kindleLink = "https://www.amazon.com/dp/B0FBS427VD";
paperbackLink = "https://www.amazon.com/dp/1068225815";
hardbackLink = "https://www.amazon.com/dp/1068225807";
extraRetailers = 'Also available at <a href="https://www.barnesandnoble.com/s/9781068225819" target="_blank">Barnes & Noble</a>';
extraRetailersHardback = 'Also available at <a href="https://www.barnesandnoble.com/s/9781068225802" target="_blank">Barnes & Noble</a>';
break;
case "CA":
kindleLink = "https://www.amazon.ca/dp/B0FBS427VD";
paperbackLink = "https://www.amazon.ca/dp/1068225815";
hardbackLink = "https://www.amazon.ca/dp/1068225807";
break;
case "AU":
kindleLink = "https://www.amazon.com.au/dp/B0FBS427VD";
paperbackLink = "https://www.amazon.com.au/dp/1068225815";
hardbackLink = "https://www.amazon.com.au/dp/1068225807";
break;
}
// Set Amazon + retailer content
const elKindle = document.getElementById("kindleLink");
const elPbAmazon = document.getElementById("paperbackLink");
const elHbAmazon = document.getElementById("hardbackLink");
const elExtra = document.getElementById("extraRetailers");
const elExtraHb = document.getElementById("extraRetailersHardback");
if (elKindle) elKindle.setAttribute("href", kindleLink);
if (elPbAmazon) elPbAmazon.setAttribute("href", paperbackLink);
if (elHbAmazon) elHbAmazon.setAttribute("href", hardbackLink);
if (elExtra) elExtra.innerHTML = extraRetailers;
if (elExtraHb) elExtraHb.innerHTML = extraRetailersHardback;
// Show IngramSpark only in GB/US; hide elsewhere
const showIngram = country === "GB" || country === "US";
["paperbackLinkSelf", "hardbackLinkSelf"].forEach(id => {
const el = document.getElementById(id);
if (!el) return;
el.classList.toggle("d-none", !showIngram); // add when false, remove when true
});
})
.catch(() => {
// If the geo lookup fails, leave links as-is
});
</script>
}

View File

@ -1,401 +0,0 @@
@model CatherineLynwood.Models.Reviews
@{
ViewData["Title"] = "The Alpha Flame: A Gritty 1980s Birmingham Crime Novel about Twin Sisters";
bool showReviews = Model.Items.Any();
}
<div class="row">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
<li class="breadcrumb-item active" aria-current="page">Discovery</li>
</ol>
</nav>
</div>
</div>
<!-- Specific Row for Book Cover and Synopsis -->
<div class="row">
<!-- Book Cover Section -->
<div class="col-md-4">
<section id="book-cover">
<div class="card character-card" id="cover-card">
<responsive-image src="the-alpha-flame-discovery-cover.png" class="card-img-top" alt="The Alpha Flame book cover — gritty 1980s Birmingham crime novel about twin sisters uncovering secrets and surviving abuse" display-width-percentage="50"></responsive-image>
<div class="card-body border-top border-3 border-dark">
<h3 class="card-title">The Front Cover</h3>
<p class="card-text">This is the final front cover of The Alpha Flame: Discovery. It features Maggie stood outside the derelict Rubery Hill Hospital.</p>
</div>
</div>
</section>
</div>
@if (showReviews)
{
<!-- Buy Section -->
<div class="col-md-8">
<section id="purchase-and-reviews">
<div class="card character-card" id="companion-card">
<div class="card-header">
<h1>The Alpha Flame: <span class="fw-light">Discovery</span><br /><span class="h2">A Gritty 1980s Birmingham Crime Novel</span></h1>
<h2 class="h3">Survival, secrets, and sisters in 1980s Birmingham.</h2>
</div>
<div class="card-body" id="companion-body">
<div class="p-2">
<h2 class="h5">Buy the Book</h2>
</div>
<!-- Buy Now Section -->
<div id="buy-now" class="mb-4">
<a id="kindleLink" href="https://www.amazon.com/dp/B0FBS427VD" target="_blank" class="btn btn-dark mb-2">
Buy Kindle Edition
</a>
<a id="paperbackLink" href="https://www.amazon.co.uk/dp/1068225815" target="_blank" class="btn btn-dark mb-2">
Buy Paperback (Bookshop Edition)
</a>
<a id="hardbackLink" href="https://www.amazon.co.uk/dp/1068225807" target="_blank" class="btn btn-dark mb-2">
Buy Hardback (Collector's Edition)
</a>
<p id="geoNote" class="text-muted small mt-2">
Available from your local Amazon store.<br />
Or order from your local bookshop using:
<ul class="small text-muted pl-3 mb-1">
<li>ISBN 978-1-0682258-1-9 Bookshop Edition (Paperback)</li>
<li>ISBN 978-1-0682258-0-2 Collector's Edition (Hardback)</li>
</ul>
<span id="extraRetailers"></span>
</p>
</div>
<!-- Reader Reviews -->
<div class="reader-reviews">
<h3 class="h6 text-uppercase text-muted">★ Reader Praise ★</h3>
@foreach (var review in Model.Items.Take(3))
{
var fullStars = (int)Math.Floor(review.RatingValue);
var hasHalfStar = review.RatingValue - fullStars >= 0.5;
var emptyStars = 5 - fullStars - (hasHalfStar ? 1 : 0);
var reviewDate = review.DatePublished.ToString("d MMMM yyyy");
<blockquote class="blockquote mb-4">
<span class="mb-2 text-warning">
@for (int i = 0; i < fullStars; i++)
{
<i class="fad fa-star"></i>
}
@if (hasHalfStar)
{
<i class="fad fa-star-half-alt"></i>
}
@for (int i = 0; i < emptyStars; i++)
{
<i class="fad fa-star" style="--fa-primary-opacity: 0.2; --fa-secondary-opacity: 0.2;"></i>
}
</span>
@Html.Raw(review.ReviewBody)
<footer>
@review.AuthorName on <cite title="@review.SiteName">
@if (string.IsNullOrEmpty(review.URL))
{
@review.SiteName
}
else
{
<a href="@review.URL" target="_blank">@review.SiteName</a>
}
</cite> &mdash; <span class="text-muted smaller">@reviewDate</span>
</footer>
</blockquote>
}
@if (Model.Items.Count > 3)
{
<div class="text-end">
<a asp-action="Reviews" class="btn btn-outline-secondary btn-sm">
Read More Reviews
</a>
</div>
}
</div>
</div>
</div>
</section>
</div>
<!-- Synopsis Section -->
<div class="col-md-12">
<section id="synopsis">
<div class="card character-card">
<div class="card-header">
<h2 class="card-title h1">The Alpha Flame: <span class="fw-light">Discovery:</span> Synopsis</h2>
</div>
<div class="card-body" id="synopsis-body">
<div class="row align-items-center">
<div class="col-2">
<responsive-image src="catherine-lynwood-16.png" class="img-fluid rounded-circle border border-2 border-dark" alt="Catherine Lynwood" display-width-percentage="20"></responsive-image>
</div>
<div class="col-10">
<!-- Audio Section -->
<div class="audio-player text-center">
<audio id="player">
<source src="/audio/the-alpha-flame-discovery-synopsis.mp3" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
</div>
<p class="text-center text-muted small">
Listen to Catherine telling you about The Alpha Flame: Discovery
</p>
</div>
</div>
<!-- Synopsis Content -->
<h3 class="card-title">Synopsis</h3>
<p class="card-text">Set in 1983 Birmingham, The Alpha Flame: Discovery is a gritty crime novel following twin sisters Beth and Maggie as they uncover dark family secrets and fight to survive abuse in a harsh, realistic world. With unflinching honesty, it explores the bonds of family, the scars of the past, and the resilience needed to endure. This powerful first instalment in the trilogy immerses readers in the grim realities of 1980s Britain while celebrating hope in the face of darkness.</p>
<p class="card-text">For Beth, the world is a cold and unforgiving place. Devastation strikes in a single moment, leaving her isolated, shattered, and vulnerable. Alone in the bleak shadows of a city that offers neither refuge nor redemption, she is forced to navigate a relentless cycle of desperation and despair. Every step of her journey tests the limits of her endurance, pushing her into harrowing situations where survival feels like a hollow victory. Beths existence is marked by loss, betrayal, and an almost suffocating loneliness that threatens to consume her entirely. Yet, even in the darkest corners of her ordeal, a fragile ember of defiance smoulders within her, a quiet, stubborn refusal to let the world destroy her completely.</p>
<p class="card-text">Maggie, by contrast, is a force of nature, a woman who thrives on her unshakable drive and an unrelenting belief in her own power. Behind her fiery red hair and disarming charm lies a storm of determination and ferocity. Maggie doesnt just live; she races through life, fuelled by a need for speed and the thrill of freedom. Her Triumph TR6 isnt just a car; its an extension of her spirit, sleek, powerful, and unapologetically bold. On the open road, with the engine roaring and the world blurring past her, she feels invincible. But Maggies intensity doesnt stop at the wheel. Her relationships burn just as brightly. As a lover, she is dominant, passionate, and unafraid to embrace her darker desires. While fiercely loving and loyal, Maggie is also formidable; crossing her isnt a mistake anyone makes twice.</p>
<p class="card-text">When fate brings Beth and Maggie together, their connection is explosive, a union of two polar opposites that burns with both tenderness and raw power. For Beth, Maggie represents a lifeline, a reminder that love and trust still exist, even in a world that has betrayed her at every turn. For Maggie, Beth awakens a fierce protectiveness and vulnerability shes rarely allowed herself to feel. Together, they ignite a flame that challenges them to confront their own fears, desires, and limitations.</p>
<p class="card-text">Set against the kaleidoscope of 1983, where synthesised anthems provide a pulsing soundtrack and the streets are alive with the bold styles and rebellious energy of the decade, their story unfolds in a city teeming with danger and intrigue. From high-speed chases along winding roads to dimly lit clubs and desolate alleyways, the heroines journey is a visceral exploration of survival and freedom. The neon haze of the era contrasts sharply with the stark realities they face, painting a vivid picture of a world where strength and vulnerability coexist.</p>
<p class="card-text">As secrets surface and danger tightens its grip, Beth and Maggie must confront not only the challenges around them but the truths within themselves. Their bond is tested by betrayal, desire, and the shadows of their pasts, but through it all, their flame burns brighter, illuminating their courage and the unbreakable spirit of two heroines determined to rewrite their fates.</p>
<p class="card-text">At its heart, The Alpha Flame is a story of survival, passion, and empowerment. It explores the devastating lows and triumphant highs of life with unflinching honesty, capturing the raw power of human connection against the gritty, vibrant backdrop of an unforgettable era. With its blend of drama, intensity, and unapologetic emotion, this is a story that will leave its mark long after the final frame.</p>
</div>
</div>
</section>
</div>
}
else
{
<!-- Synopsis Section -->
<div class="col-md-8">
<section id="synopsis">
<div class="card character-card" id="companion-card">
<div class="card-header">
<h1>The Alpha Flame: <span class="fw-light">Discovery</span><br /><span class="h2">A Gritty 1980s Birmingham Crime Novel</span></h1>
<h2 class="h3">Survival, secrets, and sisters in 1980s Birmingham.</h2>
</div>
<div class="card-body" id="companion-body">
<div class="row align-items-center">
<div class="col-2">
<responsive-image src="catherine-lynwood-16.png" class="img-fluid rounded-circle border border-2 border-dark" alt="Catherine Lynwood" display-width-percentage="20"></responsive-image>
</div>
<div class="col-10">
<!-- Audio Section -->
<div class="audio-player text-center">
<audio id="player">
<source src="/audio/the-alpha-flame-discovery-synopsis.mp3" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
</div>
<p class="text-center text-muted small">
Listen to Catherine telling you about The Alpha Flame: Discovery
</p>
</div>
</div>
<div class="row">
<div class="col-12">
<div id="buy-now" class="my-4">
<a id="kindleLink" href="https://www.amazon.co.uk/dp/B0FBS427VD" target="_blank" class="btn btn-dark mb-2">
Buy Kindle Edition
</a>
<a id="paperbackLink" href="https://www.amazon.co.uk/dp/1068225815" target="_blank" class="btn btn-dark mb-2">
Buy Paperback (Bookshop Edition)
</a>
<a id="hardbackLink" href="https://www.amazon.co.uk/dp/1068225807" target="_blank" class="btn btn-dark mb-2">
Buy Hardback (Collector's Edition)
</a>
<p id="geoNote" class="text-muted small mt-2">
Available from your local Amazon store.<br />
Or order from your local bookshop using:
<ul class="small text-muted">
<li>
ISBN 978-1-0682258-1-9 - Bookshop Edition (Paperback)
</li>
<li>
ISBN 978-1-0682258-0-2 - Collector's Eidtion (Hardback)
</li>
</ul>
<span id="extraRetailers"></span>
</p>
</div>
</div>
</div>
<!-- Synopsis Content -->
<h3 class="card-title">Synopsis</h3>
<p class="card-text">Set in 1983 Birmingham, The Alpha Flame: Discovery is a gritty crime novel following twin sisters Beth and Maggie as they uncover dark family secrets and fight to survive abuse in a harsh, realistic world. With unflinching honesty, it explores the bonds of family, the scars of the past, and the resilience needed to endure. This powerful first instalment in the trilogy immerses readers in the grim realities of 1980s Britain while celebrating hope in the face of darkness.</p>
<p class="card-text">For Beth, the world is a cold and unforgiving place. Devastation strikes in a single moment, leaving her isolated, shattered, and vulnerable. Alone in the bleak shadows of a city that offers neither refuge nor redemption, she is forced to navigate a relentless cycle of desperation and despair. Every step of her journey tests the limits of her endurance, pushing her into harrowing situations where survival feels like a hollow victory. Beths existence is marked by loss, betrayal, and an almost suffocating loneliness that threatens to consume her entirely. Yet, even in the darkest corners of her ordeal, a fragile ember of defiance smoulders within her, a quiet, stubborn refusal to let the world destroy her completely.</p>
<p class="card-text">Maggie, by contrast, is a force of nature, a woman who thrives on her unshakable drive and an unrelenting belief in her own power. Behind her fiery red hair and disarming charm lies a storm of determination and ferocity. Maggie doesnt just live; she races through life, fuelled by a need for speed and the thrill of freedom. Her Triumph TR6 isnt just a car; its an extension of her spirit, sleek, powerful, and unapologetically bold. On the open road, with the engine roaring and the world blurring past her, she feels invincible. But Maggies intensity doesnt stop at the wheel. Her relationships burn just as brightly. As a lover, she is dominant, passionate, and unafraid to embrace her darker desires. While fiercely loving and loyal, Maggie is also formidable; crossing her isnt a mistake anyone makes twice.</p>
<p class="card-text">When fate brings Beth and Maggie together, their connection is explosive, a union of two polar opposites that burns with both tenderness and raw power. For Beth, Maggie represents a lifeline, a reminder that love and trust still exist, even in a world that has betrayed her at every turn. For Maggie, Beth awakens a fierce protectiveness and vulnerability shes rarely allowed herself to feel. Together, they ignite a flame that challenges them to confront their own fears, desires, and limitations.</p>
<p class="card-text">Set against the kaleidoscope of 1983, where synthesised anthems provide a pulsing soundtrack and the streets are alive with the bold styles and rebellious energy of the decade, their story unfolds in a city teeming with danger and intrigue. From high-speed chases along winding roads to dimly lit clubs and desolate alleyways, the heroines journey is a visceral exploration of survival and freedom. The neon haze of the era contrasts sharply with the stark realities they face, painting a vivid picture of a world where strength and vulnerability coexist.</p>
<p class="card-text">As secrets surface and danger tightens its grip, Beth and Maggie must confront not only the challenges around them but the truths within themselves. Their bond is tested by betrayal, desire, and the shadows of their pasts, but through it all, their flame burns brighter, illuminating their courage and the unbreakable spirit of two heroines determined to rewrite their fates.</p>
<p class="card-text">At its heart, The Alpha Flame is a story of survival, passion, and empowerment. It explores the devastating lows and triumphant highs of life with unflinching honesty, capturing the raw power of human connection against the gritty, vibrant backdrop of an unforgettable era. With its blend of drama, intensity, and unapologetic emotion, this is a story that will leave its mark long after the final frame.</p>
</div>
</div>
</section>
</div>
}
<!-- Giveway Section -->
@if (DateTime.Now < new DateTime(2025, 9, 1))
{
<div class="col-md-12 mt-4">
<div class="card">
<div class="card-body text-center">
<h2 class="display-6 fw-bold">Win: <span class="fw-light">a Collectors Edition of The Alpha Flame: Discovery</span></h2>
<p class="mb-2">
Enter my giveaway for your chance to own this special edition. Signed in the UK, or delivered as a premium collectors copy worldwide.
</p>
<em>Exclusive, limited, beautiful.</em>
<div class="row justify-content-center">
<div class="col-8 col-md-4 mt-4">
<a asp-controller="TheAlphaFlame" asp-action="Giveaways" class="btn btn-dark">Enter now for your chance.</a>
</div>
</div>
</div>
</div>
</div>
}
</div>
<!-- Chapter Previews Section -->
<section id="chapters" class="mt-4">
<h2>Chapter Previews</h2>
<div class="row">
<div class="col-md-4 mb-4">
<div class="card h-100 character-card">
<a asp-action="Chapter1">
<responsive-image src="beth-stood-in-bathroom.png" class="card-img-top" alt="Beth's Bathroom" display-width-percentage="50"></responsive-image>
</a>
<div class="card-body border-top border-3 border-dark">
<h3 class="card-title">Chapter 1: Drowning in Silence - Beth</h3>
<p class="card-text">Beth returns home to find her mother lifeless in the bath...</p>
<div class="text-end"><a asp-action="Chapter1" class="btn btn-dark">Read More</a></div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card h-100 character-card">
<a asp-action="Chapter2">
<responsive-image src="maggie-with-her-tr6-2.png" class="fit-image" alt="Maggie With Her TR6" display-width-percentage="50"></responsive-image>
</a>
<div class="card-body border-top border-3 border-dark">
<h3 class="card-title">Chapter 2: The Last Lesson - Maggie</h3>
<p class="card-text">On Christmas Eve, Maggie nervously heads out for her driving test...</p>
<div class="text-end"><a asp-action="Chapter2" class="btn btn-dark">Read More</a></div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card h-100 character-card">
<a asp-action="Chapter13">
<responsive-image src="pub-from-chapter-13.png" class="fit-image" alt="Pub from Chapter 13" display-width-percentage="50"></responsive-image>
</a>
<div class="card-body border-top border-3 border-dark">
<h3 class="card-title">Chapter 13: A Name She Never Owned - Susie</h3>
<p class="card-text">Susie goes out for a drink with a punter. What on earth could go wrong...</p>
<div class="text-end"><a asp-action="Chapter13" class="btn btn-dark">Read More</a></div>
</div>
</div>
</div>
</div>
</section>
@section Scripts {
<script>
window.addEventListener("load", function () {
const coverCard = document.getElementById("cover-card");
const companionCard = document.getElementById("companion-card");
const companionBody = document.getElementById("companion-body");
if (coverCard && companionCard && companionBody) {
// Match the height of the synopsis card to the cover card
const coverHeight = coverCard.offsetHeight;
companionCard.style.height = `${coverHeight}px`;
// Adjust the synopsis body to scroll within the matched height
const headerHeight = companionCard.querySelector(".card-header").offsetHeight;
companionBody.style.maxHeight = `${coverHeight - headerHeight}px`;
}
});
</script>
<script>
const player = new Plyr('audio');
</script>
<script>
fetch('https://ipapi.co/json/')
.then(response => response.json())
.then(data => {
const country = data.country_code;
let kindleLink = "https://www.amazon.com/dp/B0FBS427VD";
let paperbackLink = "https://www.amazon.com/dp/1068225815";
let hardbackLink = "https://www.amazon.com/dp/1068225807";
let extraRetailers = "";
switch (country) {
case "GB":
kindleLink = "https://www.amazon.co.uk/dp/B0FBS427VD";
paperbackLink = "https://www.amazon.co.uk/dp/1068225815";
hardbackLink = "https://www.amazon.co.uk/dp/1068225807";
extraRetailers = 'Also available at <a href="https://www.waterstones.com/book/the-alpha-flame/catherine-lynwood/9781068225819" target="_blank">Waterstons</a>';
break;
case "US":
kindleLink = "https://www.amazon.com/dp/B0FBS427VD";
paperbackLink = "https://www.amazon.com/dp/1068225815";
hardbackLink = "https://www.amazon.com/dp/1068225807";
extraRetailers = 'Also available at <a href="https://www.barnesandnoble.com/s/9781068225810" target="_blank">Barnes & Noble</a>';
break;
case "CA":
kindleLink = "https://www.amazon.ca/dp/B0FBS427VD";
paperbackLink = "https://www.amazon.ca/dp/1068225815";
hardbackLink = "https://www.amazon.ca/dp/1068225807";
break;
case "AU":
kindleLink = "https://www.amazon.com.au/dp/B0FBS427VD";
paperbackLink = "https://www.amazon.com.au/dp/1068225815";
hardbackLink = "https://www.amazon.com.au/dp/1068225807";
break;
}
document.getElementById("kindleLink").setAttribute("href", kindleLink);
document.getElementById("paperbackLink").setAttribute("href", paperbackLink);
document.getElementById("hardbackLink").setAttribute("href", hardbackLink);
document.getElementById("extraRetailers").innerHTML = extraRetailers;
});
</script>
}
@section Meta{
<MetaTag meta-title="The Alpha Flame: Discovery by Catherine Lynwood"
meta-description="A gritty 1980s Birmingham crime novel about twin sisters uncovering dark family secrets and surviving abuse. Realistic, powerful, and unflinching — discover The Alpha Flame today."
meta-keywords="The Alpha Flame Discovery, Catherine Lynwood, 1983 novel, twin sisters, suspense fiction, Rubery, Birmingham fiction, historical drama, family secrets"
meta-author="Catherine Lynwood"
meta-url="https://www.catherinelynwood.com/the-alpha-flame/discovery"
meta-image="https://www.catherinelynwood.com/images/webp/the-alpha-flame-discovery-cover-1200.webp"
meta-image-alt="Maggie from 'The Alpha Flame: Discovery' by Catherine Lynwood"
og-site-name="Catherine Lynwood - The Alpha Flame: Discovery"
article-published-time="@new DateTime(2024, 11, 20)"
article-modified-time="@new DateTime(2025, 06, 07)"
twitter-card-type="summary_large_image"
twitter-site-handle="@@CathLynwood"
twitter-creator-handle="@@CathLynwood" />
<script type="application/ld+json">
@Html.Raw(Model.SchemaJsonLd)
</script>
}

View File

@ -0,0 +1,355 @@
@model CatherineLynwood.Models.TitlePageViewModel
@{
ViewData["Title"] = "The Alpha Flame: A Gritty 1980s Birmingham Crime Novel about Twin Sisters";
bool showReviews = Model.Reviews.Items.Any();
}
<div class="row">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
<li class="breadcrumb-item active" aria-current="page">Discovery</li>
</ol>
</nav>
</div>
</div>
<!-- HERO: Cover + Trailer + Buy Box -->
<section class="mb-4">
<div class="row g-3 align-items-stretch">
<!-- Book Cover -->
<div class="col-lg-5 d-flex d-none d-lg-block">
<div class="card character-card h-100 flex-fill" id="cover-card">
<responsive-image src="the-alpha-flame-discovery-cover.png"
class="card-img-top"
alt="The Alpha Flame book cover — gritty 1980s Birmingham crime novel about twin sisters uncovering secrets and surviving abuse"
display-width-percentage="50"></responsive-image>
<div class="card-body border-top border-3 border-dark">
<h3 class="card-title h5 mb-1">The Alpha Flame: <span class="fw-light">Discovery</span></h3>
<p class="card-text mb-0">It's 1983 Birmingham. Maggie, my fiery heroine, standing outside the derelict Rubery Hill Hospital. A story of survival, sisters, and fire.</p>
</div>
</div>
</div>
<!-- Trailer + Buy Box -->
<div class="col-lg-7 d-flex">
<div class="card character-card h-100 flex-fill" id="hero-media-card">
<div class="card-body d-flex flex-column">
<!-- Trailer -->
<div class="trailer-wrapper mb-3">
<!-- Desktop: LANDSCAPE -->
<video id="trailerLandscape"
class="w-100"
playsinline
preload="none"
poster="/images/webp/the-alpha-flame-discovery-trailer-landscape-1400.webp"
controls>
<source src="/videos/the-alpha-flame-discovery-trailer-landscape.mp4" type="video/mp4">
Sorry, your browser doesn't support embedded video.
</video>
<button id="trailerPlayBtn" class="trailer-play-btn">
<i class="fad fa-play"></i>
</button>
</div>
<!-- If JS is off, hide the custom play button so native controls are used -->
<noscript>
<style>
#trailerPlayBtn {
display: none !important
}</style>
</noscript>
@await Component.InvokeAsync("BuyPanel", new { ISO2 = Model.UserIso2, Src = Model.Src, Title = Model.Title })
</div>
</div>
</div>
</div>
</section>
<section class="mb-4">
<div class="card character-card">
<div class="card-body">
<h3 class="h6 text-uppercase text-muted mb-2">★ Audiobook Out Now ★</h3>
<p class="mb-3">
<em>The Alpha Flame: Discovery</em> is now available as a full-length audiobook, narrated to be lived in rather than rushed through.
Its a slow-burn, character-led story where quiet moments matter, tension builds gradually, and certain scenes land harder when you hear them spoken.
</p>
<p class="mb-3">
Ideal for long drives, late nights, or anyone who prefers to sink into a story through headphones rather than skim it on a screen.
</p>
<a asp-controller="Discovery" asp-action="AudioBook" class="btn btn-dark">
🎧 Explore the audiobook
</a>
</div>
</div>
</section>
<!-- Social proof: show one standout review near the top -->
@if (showReviews)
{
var top = Model.Reviews.Items.Where(x => x.RatingValue == 5).OrderByDescending(y => y.DatePublished).First();
var fullStars = (int)Math.Floor(top.RatingValue);
var hasHalfStar = top.RatingValue - fullStars >= 0.5;
var emptyStars = 5 - fullStars - (hasHalfStar ? 1 : 0);
var reviewDate = top.DatePublished.ToString("d MMMM yyyy");
<section class="mb-4">
<div class="card character-card">
<div class="card-body">
<h3 class="h6 text-uppercase text-muted mb-2">★ Reader Praise ★</h3>
<blockquote class="blockquote mb-2">
<span class="mb-2 text-warning d-inline-block">
@for (int i = 0; i < fullStars; i++)
{
<i class="fad fa-star"></i>
}
@if (hasHalfStar)
{
<i class="fad fa-star-half-alt"></i>
}
@for (int i = 0; i < emptyStars; i++)
{
<i class="fad fa-star" style="--fa-primary-opacity:0.2;--fa-secondary-opacity:0.2;"></i>
}
</span>
@Html.Raw(top.ReviewBody)
<footer>
@top.AuthorName on
<cite title="@top.SiteName">
@if (string.IsNullOrEmpty(top.URL))
{
@top.SiteName
}
else
{
<a href="@top.URL" target="_blank">@top.SiteName</a>
}
</cite>
<span class="text-muted smaller">, @reviewDate</span>
</footer>
</blockquote>
@if (Model.Reviews.Items.Count > 1)
{
<div class="text-end">
<a asp-action="Reviews" class="btn btn-outline-secondary btn-sm">Read more reviews</a>
</div>
}
</div>
</div>
</section>
}
<!-- Synopsis: open on mobile, collapsible on desktop -->
<section id="synopsis" class="mb-4">
<div class="card character-card text-white" style="background: url('/images/webp/synopsis-background-960.webp'); background-position: center; background-size: cover;">
<div class="card-header">
<h2 class="card-title h1 mb-0">The Alpha Flame: <span class="fw-light">Discovery</span></h2>
<p class="mb-0">Survival, secrets, and shadows in 1983 Birmingham.</p>
</div>
<div class="card-body" id="synopsis-body">
<!-- Audio blurb -->
<div class="row align-items-center mb-3">
<div class="col-2">
<responsive-image src="catherine-lynwood-16.png" class="img-fluid rounded-circle border border-2 border-dark" alt="Catherine Lynwood" display-width-percentage="20"></responsive-image>
</div>
<div class="col-10">
<div class="audio-player text-center">
<audio id="player">
<source src="/audio/the-alpha-flame-discovery-catherine.mp3" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
</div>
<p class="text-center text-white small mb-0">Listen to Catherine talking about the book</p>
</div>
</div>
<!-- Teaser -->
<p class="card-text">
She didnt go looking for trouble. But when she found Beth, bruised, broken, and terrified, Maggie couldnt walk away.
</p>
<p class="card-text">
But nothing prepares her for Beth. As she digs deeper into Beths world, Maggie finds herself pulled into the shadows, a seedy underworld of secrets, survival, and control, where loyalty is rare and nothing is guaranteed. The more she uncovers, the more she realises this isnt someone elses nightmare. Its her own.
</p>
<p class="card-text">
Set in 1983 Birmingham, nearby Redditch, and Barmouth in Wales, The Alpha Flame: Discovery follows the lives of two young women, Beth and Maggie, as they uncover dark family secrets and fight to survive. Gritty and emotionally charged, it explores the bond between two women who refuse to be broken.
</p>
<!-- Desktop-only: collapse trigger -->
<p class="mb-2 d-none d-md-block">
<a class="btn btn-outline-light btn-sm"
data-bs-toggle="collapse"
href="#fullSynopsis"
role="button"
aria-expanded="false"
aria-controls="fullSynopsis">
Read full synopsis
</a>
</p>
<!-- One copy of the full synopsis -->
<div class="collapse" id="fullSynopsis">
<div class="mt-2">
<p class="card-text">For Beth, the world is a cold and unforgiving place. Devastation strikes in a single moment, leaving her isolated, shattered, and vulnerable. Alone in the bleak shadows of a city that offers neither refuge nor redemption, she is forced to navigate a relentless cycle of desperation and despair. Every step of her journey tests the limits of her endurance, pushing her into harrowing situations where survival feels like a hollow victory. Beths existence is marked by loss, betrayal, and an almost suffocating loneliness that threatens to consume her entirely. Yet, even in the darkest corners of her ordeal, a fragile ember of defiance smoulders within her, a quiet, stubborn refusal to let the world destroy her completely.</p>
<p class="card-text">Maggie, by contrast, is a force of nature, a woman who thrives on her unshakable drive and an unrelenting belief in her own power. Behind her fiery red hair and disarming charm lies a storm of determination and ferocity. Maggie doesnt just live; she races through life, fuelled by a need for speed and the thrill of freedom. Her Triumph TR6 isnt just a car; its an extension of her spirit, sleek, powerful, and unapologetically bold. On the open road, with the engine roaring and the world blurring past her, she feels invincible. But Maggies intensity doesnt stop at the wheel. Her relationships burn just as brightly. As a lover, she is dominant, passionate, and unafraid to embrace her darker desires. While fiercely loving and loyal, Maggie is also formidable; crossing her isnt a mistake anyone makes twice.</p>
<p class="card-text">When fate brings Beth and Maggie together, their connection is explosive, a union of two polar opposites that burns with both tenderness and raw power. For Beth, Maggie represents a lifeline, a reminder that love and trust still exist, even in a world that has betrayed her at every turn. For Maggie, Beth awakens a fierce protectiveness and vulnerability shes rarely allowed herself to feel. Together, they ignite a flame that challenges them to confront their own fears, desires, and limitations.</p>
<p class="card-text">Set against the kaleidoscope of 1983, where synthesised anthems provide a pulsing soundtrack and the streets are alive with the bold styles and rebellious energy of the decade, their story unfolds in a city teeming with danger and intrigue. From high-speed chases along winding roads to dimly lit clubs and desolate alleyways, the heroines journey is a visceral exploration of survival and freedom. The neon haze of the era contrasts sharply with the stark realities they face, painting a vivid picture of a world where strength and vulnerability coexist.</p>
<p class="card-text">As secrets surface and danger tightens its grip, Beth and Maggie must confront not only the challenges around them but the truths within themselves. Their bond is tested by betrayal, desire, and the shadows of their pasts, but through it all, their flame burns brighter, illuminating their courage and the unbreakable spirit of two heroines determined to rewrite their fates.</p>
<p class="card-text">At its heart, The Alpha Flame is a story of survival, passion, and empowerment. It captures the raw power of human connection against the gritty, vibrant backdrop of an unforgettable era. With its blend of drama, intensity, and unapologetic emotion, this is a story that will leave its mark long after the final page.</p>
</div>
</div>
</div>
</div>
</section>
<!-- Chapter Previews -->
<section id="chapters" class="mt-4">
<h2>Chapter Previews</h2>
<div class="row">
<div class="col-md-4 mb-4">
<div class="card h-100 character-card">
<a asp-action="Chapter1">
<responsive-image src="beth-stood-in-bathroom.png" class="card-img-top" alt="Beth's Bathroom" display-width-percentage="50"></responsive-image>
</a>
<div class="card-body border-top border-3 border-dark">
<h3 class="card-title">Chapter 1: Drowning in Silence — Beth</h3>
<p class="card-text">Beth returns home to find her mother lifeless in the bath...</p>
<div class="text-end"><a asp-action="Chapter1" class="btn btn-dark">Read More</a></div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card h-100 character-card">
<a asp-action="Chapter2">
<responsive-image src="maggie-with-her-tr6-2.png" class="fit-image" alt="Maggie With Her TR6" display-width-percentage="50"></responsive-image>
</a>
<div class="card-body border-top border-3 border-dark">
<h3 class="card-title">Chapter 2: The Last Lesson — Maggie</h3>
<p class="card-text">On Christmas Eve, Maggie nervously heads out for her driving test not knowing the story that will pan out before her...</p>
<div class="text-end"><a asp-action="Chapter2" class="btn btn-dark">Read More</a></div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card h-100 character-card">
<a asp-action="Chapter13">
<responsive-image src="pub-from-chapter-13.png" class="fit-image" alt="Pub from Chapter 13" display-width-percentage="50"></responsive-image>
</a>
<div class="card-body border-top border-3 border-dark">
<h3 class="card-title">Chapter 13: A Name She Never Owned — Susie</h3>
<p class="card-text">Susie goes out for a drink with a punter. What on earth could go wrong...</p>
<div class="text-end"><a asp-action="Chapter13" class="btn btn-dark">Read More</a></div>
</div>
</div>
</div>
</div>
</section>
@section Scripts {
<!-- Plyr for audio -->
<script>
const player = new Plyr('audio');
</script>
<!-- Trailer play/pause via custom button or video click (desktop only) -->
<script>
document.addEventListener("DOMContentLoaded", () => {
const v = document.getElementById("trailerLandscape");
const playBtn = document.getElementById("trailerPlayBtn");
if (!v || !playBtn) return;
// Hide native controls when JS is active; use custom button instead
v.controls = false;
// Start playback
const startPlayback = () => {
v.muted = false;
v.volume = 1.0;
v.play()
.then(() => { playBtn.style.display = "none"; })
.catch(err => console.warn("Video play failed:", err));
};
// Toggle on video click
const togglePlayback = () => {
if (v.paused) {
startPlayback();
} else {
v.pause();
playBtn.style.display = "block";
}
};
// Events
playBtn.addEventListener("click", (e) => { e.preventDefault(); startPlayback(); });
v.addEventListener("click", togglePlayback);
// Keep button state in sync with native events
v.addEventListener("play", () => { playBtn.style.display = "none"; });
v.addEventListener("pause", () => { playBtn.style.display = "block"; });
v.addEventListener("ended", () => { playBtn.style.display = "block"; });
});
</script>
<script>
document.addEventListener('DOMContentLoaded', function () {
const el = document.getElementById('fullSynopsis');
if (!el || !window.bootstrap?.Collapse) return;
// Initialise without auto-toggling
const c = new bootstrap.Collapse(el, { toggle: false });
const mq = window.matchMedia('(min-width: 768px)'); // Bootstrap md
function setInitial() {
if (mq.matches) {
// Desktop: keep collapsed
c.hide();
} else {
// Mobile: open by default
c.show();
}
}
setInitial();
// Optional: if user resizes across the breakpoint, adjust state
mq.addEventListener?.('change', setInitial);
});
</script>
}
@section Meta {
<MetaTag meta-title="The Alpha Flame: Discovery by Catherine Lynwood"
meta-description="A gritty 1980s Birmingham psycological crime novel about two girls uncovering dark family secrets and surviving abuse. Realistic, powerful, and unflinching... discover The Alpha Flame today."
meta-keywords="The Alpha Flame Discovery, Catherine Lynwood, 1983 novel, twin sisters, suspense fiction, Rubery, Birmingham fiction, historical drama, family secrets"
meta-author="Catherine Lynwood"
meta-url="https://www.catherinelynwood.com/the-alpha-flame/discovery"
meta-image="https://www.catherinelynwood.com/images/webp/the-alpha-flame-discovery-cover-1200.webp"
meta-image-png="https://www.catherinelynwood.com/images/the-alpha-flame-discovery-cover.png"
meta-image-alt="Maggie from 'The Alpha Flame: Discovery' by Catherine Lynwood"
og-site-name="Catherine Lynwood - The Alpha Flame: Discovery"
article-published-time="@new DateTime(2024, 11, 20)"
article-modified-time="@new DateTime(2025, 09, 10)"
twitter-card-type="summary_large_image"
twitter-site-handle="@@CathLynwood"
twitter-creator-handle="@@CathLynwood" />
<script type="application/ld+json">
@Html.Raw(Model.Reviews.SchemaJsonLd)
</script>
}

View File

@ -0,0 +1,308 @@
@model CatherineLynwood.Models.TitlePageViewModel
@{
ViewData["Title"] = "The Alpha Flame: A Gritty 1980s Birmingham Crime Novel about Twin Sisters";
bool showReviews = Model.Reviews.Items.Any();
}
<div class="row">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
<li class="breadcrumb-item active" aria-current="page">Discovery</li>
</ol>
</nav>
</div>
</div>
<!-- HERO: Cover + Trailer + Buy Box -->
<section class="mb-4">
<div class="card mb-4">
<div class="card-body">
<h2 class="card-title h1 mb-0">The Alpha Flame: <span class="fw-light">Discovery</span></h2>
<p class="mb-0">Birmingham 1983 - A tale of survival, secrets, and sisterhood.</p>
</div>
</div>
<div class="row g-3 align-items-stretch">
<!-- Trailer + Buy Box -->
<div class="col-12">
<div class="card character-card" id="hero-media-card">
<div class="card-body">
<!-- Trailer -->
<div class="trailer-wrapper mb-3">
<!-- Mobile / tablet: PORTRAIT -->
<video id="trailerPortrait"
class="w-100"
playsinline
preload="none"
poster="/images/webp/the-alpha-flame-discovery-trailer-portrait-400.webp"
controls>
<source src="/videos/the-alpha-flame-discovery-trailer-portrait.mp4" type="video/mp4">
Sorry, your browser doesn't support embedded video.
</video>
<button id="trailerPlayBtn" class="trailer-play-btn">
<i class="fad fa-play"></i>
</button>
</div>
<!-- If JS is off, hide the custom play button so native controls are used -->
<noscript>
<style>
#trailerPlayBtn {
display: none !important
}</style>
</noscript>
@await Component.InvokeAsync("BuyPanel", new { ISO2 = Model.UserIso2, Src = Model.Src })
</div>
</div>
<!-- Sticky mobile buy bar -->
<div id="mobileBuyBar" class="d-md-none fixed-bottom bg-dark text-white py-2 border-top border-3 border-light" style="z-index:1030;">
<div class="container d-flex justify-content-between align-items-center">
<span class="small">The Alpha Flame: Discovery</span>
<a href="#buyBox" class="btn btn-light btn-sm">Buy now</a>
</div>
</div>
</div>
</div>
</section>
<section class="mb-4">
<div class="card character-card">
<div class="card-body">
<h3 class="h6 text-uppercase text-muted mb-2">★ Audiobook Out Now ★</h3>
<p class="mb-3">
<em>The Alpha Flame: Discovery</em> is now available as a full-length audiobook, narrated to be lived in rather than rushed through.
Its a slow-burn, character-led story where quiet moments matter, tension builds gradually, and certain scenes land harder when you hear them spoken.
</p>
<p class="mb-3">
Ideal for long drives, late nights, or anyone who prefers to sink into a story through headphones rather than skim it on a screen.
</p>
<a asp-controller="Discovery" asp-action="AudioBook" class="btn btn-dark">
🎧 Explore the audiobook
</a>
</div>
</div>
</section>
<!-- Social proof: show one standout review near the top -->
@if (showReviews)
{
var top = Model.Reviews.Items.Where(x => x.RatingValue == 5).OrderByDescending(y => y.DatePublished).First();
var fullStars = (int)Math.Floor(top.RatingValue);
var hasHalfStar = top.RatingValue - fullStars >= 0.5;
var emptyStars = 5 - fullStars - (hasHalfStar ? 1 : 0);
var reviewDate = top.DatePublished.ToString("d MMMM yyyy");
<section class="mb-4">
<div class="card character-card">
<div class="card-body">
<h3 class="h6 text-uppercase text-muted mb-2">★ Reader Praise ★</h3>
<blockquote class="blockquote mb-2">
<span class="mb-2 text-warning d-inline-block">
@for (int i = 0; i < fullStars; i++)
{
<i class="fad fa-star"></i>
}
@if (hasHalfStar)
{
<i class="fad fa-star-half-alt"></i>
}
@for (int i = 0; i < emptyStars; i++)
{
<i class="fad fa-star" style="--fa-primary-opacity:0.2;--fa-secondary-opacity:0.2;"></i>
}
</span>
@Html.Raw(top.ReviewBody)
<footer>
@top.AuthorName on
<cite title="@top.SiteName">
@if (string.IsNullOrEmpty(top.URL))
{
@top.SiteName
}
else
{
<a href="@top.URL" target="_blank">@top.SiteName</a>
}
</cite>
<span class="text-muted smaller">, @reviewDate</span>
</footer>
</blockquote>
@if (Model.Reviews.Items.Count > 1)
{
<div class="text-end">
<a asp-action="Reviews" class="btn btn-outline-secondary btn-sm">Read more reviews</a>
</div>
}
</div>
</div>
</section>
}
<!-- Synopsis: open on mobile, collapsible on desktop -->
<section id="synopsis" class="mb-4">
<div class="card character-card text-white" style="background: url('/images/webp/synopsis-background-960.webp'); background-position: center; background-size: cover;">
<div class="card-header">
<h2 class="card-title h1 mb-0">The Alpha Flame: <span class="fw-light">Discovery</span></h2>
<p class="mb-0">Birmingham 1983 - A tale of survival, secrets, and sisterhood.</p>
</div>
<div class="card-body" id="synopsis-body">
<!-- Audio blurb -->
<div class="row align-items-center mb-3">
<div class="col-2">
<responsive-image src="catherine-lynwood-16.png" class="img-fluid rounded-circle border border-2 border-dark" alt="Catherine Lynwood" display-width-percentage="20"></responsive-image>
</div>
<div class="col-10">
<div class="audio-player text-center">
<audio id="player">
<source src="/audio/the-alpha-flame-discovery-catherine.mp3" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
</div>
<p class="text-center text-white small mb-0">Listen to Catherine talking about the book</p>
</div>
</div>
<!-- Full synopsis -->
<div>
<div class="mt-2">
<p class="card-text">She didnt go looking for trouble. But when she found Beth, bruised, broken, and terrified, Maggie couldnt walk away.</p>
<p class="card-text">But nothing prepares her for Beth. As she digs deeper into Beths world, Maggie finds herself pulled into the shadows, a seedy underworld of secrets, survival, and control, where loyalty is rare and nothing is guaranteed. The more she uncovers, the more she realises this isnt someone elses nightmare. Its her own.</p>
<p class="card-text">Set in 1983 Birmingham, nearby Redditch, and Barmouth in Wales, The Alpha Flame: Discovery follows the lives of two young women, Beth and Maggie, as they uncover dark family secrets and fight to survive. Gritty and emotionally charged, it explores the bond between two women who refuse to be broken.</p>
<p class="card-text">For Beth, the world is a cold and unforgiving place. Devastation strikes in a single moment, leaving her isolated, shattered, and vulnerable. Alone in the bleak shadows of a city that offers neither refuge nor redemption, she is forced to navigate a relentless cycle of desperation and despair. Every step of her journey tests the limits of her endurance, pushing her into harrowing situations where survival feels like a hollow victory. Beths existence is marked by loss, betrayal, and an almost suffocating loneliness that threatens to consume her entirely. Yet, even in the darkest corners of her ordeal, a fragile ember of defiance smoulders within her, a quiet, stubborn refusal to let the world destroy her completely.</p>
<p class="card-text">Maggie, by contrast, is a force of nature, a woman who thrives on her unshakable drive and an unrelenting belief in her own power. Behind her fiery red hair and disarming charm lies a storm of determination and ferocity. Maggie doesnt just live; she races through life, fuelled by a need for speed and the thrill of freedom. Her Triumph TR6 isnt just a car; its an extension of her spirit, sleek, powerful, and unapologetically bold. On the open road, with the engine roaring and the world blurring past her, she feels invincible. But Maggies intensity doesnt stop at the wheel. Her relationships burn just as brightly. As a lover, she is dominant, passionate, and unafraid to embrace her darker desires. While fiercely loving and loyal, Maggie is also formidable; crossing her isnt a mistake anyone makes twice.</p>
<p class="card-text">When fate brings Beth and Maggie together, their connection is explosive, a union of two polar opposites that burns with both tenderness and raw power. For Beth, Maggie represents a lifeline, a reminder that love and trust still exist, even in a world that has betrayed her at every turn. For Maggie, Beth awakens a fierce protectiveness and vulnerability shes rarely allowed herself to feel. Together, they ignite a flame that challenges them to confront their own fears, desires, and limitations.</p>
<p class="card-text">Set against the kaleidoscope of 1983, where synthesised anthems provide a pulsing soundtrack and the streets are alive with the bold styles and rebellious energy of the decade, their story unfolds in a city teeming with danger and intrigue. From high speed chases along winding roads to dimly lit clubs and desolate alleyways, the heroines journey is a visceral exploration of survival and freedom. The neon haze of the era contrasts sharply with the stark realities they face, painting a vivid picture of a world where strength and vulnerability coexist.</p>
<p class="card-text">As secrets surface and danger tightens its grip, Beth and Maggie must confront not only the challenges around them but the truths within themselves. Their bond is tested by betrayal, desire, and the shadows of their pasts, but through it all, their flame burns brighter, illuminating their courage and the unbreakable spirit of two heroines determined to rewrite their fates.</p>
<p class="card-text">At its heart, The Alpha Flame is a story of survival, passion, and empowerment. It captures the raw power of human connection against the gritty, vibrant backdrop of an unforgettable era. With its blend of drama, intensity, and unapologetic emotion, this is a story that will leave its mark long after the final page.</p>
</div>
</div>
</div>
</div>
</section>
<!-- Chapter Previews -->
<section id="chapters" class="mt-4">
<h2>Chapter Previews</h2>
<div class="row">
<div class="col-md-4 mb-4">
<div class="card h-100 character-card">
<a asp-action="Chapter1">
<responsive-image src="beth-stood-in-bathroom.png" class="card-img-top" alt="Beth's Bathroom" display-width-percentage="50"></responsive-image>
</a>
<div class="card-body border-top border-3 border-dark">
<h3 class="card-title">Chapter 1: Drowning in Silence — Beth</h3>
<p class="card-text">Beth returns home to find her mother lifeless in the bath...</p>
<div class="text-end"><a asp-action="Chapter1" class="btn btn-dark">Read More</a></div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card h-100 character-card">
<a asp-action="Chapter2">
<responsive-image src="maggie-with-her-tr6-2.png" class="fit-image" alt="Maggie With Her TR6" display-width-percentage="50"></responsive-image>
</a>
<div class="card-body border-top border-3 border-dark">
<h3 class="card-title">Chapter 2: The Last Lesson — Maggie</h3>
<p class="card-text">On Christmas Eve, Maggie nervously heads out for her driving test not knowing the story that will pan out before her...</p>
<div class="text-end"><a asp-action="Chapter2" class="btn btn-dark">Read More</a></div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card h-100 character-card">
<a asp-action="Chapter13">
<responsive-image src="pub-from-chapter-13.png" class="fit-image" alt="Pub from Chapter 13" display-width-percentage="50"></responsive-image>
</a>
<div class="card-body border-top border-3 border-dark">
<h3 class="card-title">Chapter 13: A Name She Never Owned — Susie</h3>
<p class="card-text">Susie goes out for a drink with a punter. What on earth could go wrong...</p>
<div class="text-end"><a asp-action="Chapter13" class="btn btn-dark">Read More</a></div>
</div>
</div>
</div>
</div>
</section>
@section Scripts {
<!-- Plyr for audio -->
<script>
const player = new Plyr('audio');
</script>
<!-- Trailer play/pause via custom button or video click (single video) -->
<script>
document.addEventListener("DOMContentLoaded", () => {
const v = document.getElementById("trailerPortrait");
const playBtn = document.getElementById("trailerPlayBtn");
if (!v || !playBtn) return;
// Hide native controls when JS is active; use custom button instead
v.controls = false;
// Start playback
const startPlayback = () => {
v.muted = false;
v.volume = 1.0;
v.play()
.then(() => { playBtn.style.display = "none"; })
.catch(err => console.warn("Video play failed:", err));
};
// Toggle on video click
const togglePlayback = () => {
if (v.paused) {
startPlayback();
} else {
v.pause();
playBtn.style.display = "block";
}
};
// Events
playBtn.addEventListener("click", (e) => { e.preventDefault(); startPlayback(); });
v.addEventListener("click", togglePlayback);
// Keep button state in sync with native events
v.addEventListener("play", () => { playBtn.style.display = "none"; });
v.addEventListener("pause", () => { playBtn.style.display = "block"; });
v.addEventListener("ended", () => { playBtn.style.display = "block"; });
});
</script>
}
@section Meta {
<MetaTag meta-title="The Alpha Flame: Discovery by Catherine Lynwood"
meta-description="A gritty 1980s Birmingham psycological crime novel about two girls uncovering dark family secrets and surviving abuse. Realistic, powerful, and unflinching... discover The Alpha Flame today."
meta-keywords="The Alpha Flame Discovery, Catherine Lynwood, 1983 novel, twin sisters, suspense fiction, Rubery, Birmingham fiction, historical drama, family secrets"
meta-author="Catherine Lynwood"
meta-url="https://www.catherinelynwood.com/the-alpha-flame/discovery"
meta-image="https://www.catherinelynwood.com/images/webp/the-alpha-flame-discovery-cover-1200.webp"
meta-image-png="https://www.catherinelynwood.com/images/the-alpha-flame-discovery-cover.png"
meta-image-alt="Maggie from 'The Alpha Flame: Discovery' by Catherine Lynwood"
og-site-name="Catherine Lynwood - The Alpha Flame: Discovery"
article-published-time="@new DateTime(2024, 11, 20)"
article-modified-time="@new DateTime(2025, 09, 10)"
twitter-card-type="summary_large_image"
twitter-site-handle="@@CathLynwood"
twitter-creator-handle="@@CathLynwood" />
<script type="application/ld+json">
@Html.Raw(Model.Reviews.SchemaJsonLd)
</script>
}

View File

@ -0,0 +1,309 @@
@model CatherineLynwood.Models.TitlePageViewModel
@{
ViewData["Title"] = "The Alpha Flame: A Gritty 1980s Birmingham Crime Novel about Twin Sisters";
bool showReviews = Model.Reviews.Items.Any();
}
<div class="row">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
<li class="breadcrumb-item active" aria-current="page">Discovery</li>
</ol>
</nav>
</div>
</div>
<!-- HERO: Cover + Trailer (no Buy Box here in Version B) -->
<section class="mb-4">
<div class="row g-3 align-items-stretch">
<!-- Trailer only -->
<div class="col-12">
<div class="card character-card" id="hero-media-card">
<div class="card-body">
<!-- Trailer -->
<div class="trailer-wrapper mb-3">
<!-- Mobile / tablet: PORTRAIT -->
<video id="trailerPortrait"
class="w-100"
playsinline
preload="none"
poster="/images/webp/the-alpha-flame-discovery-trailer-portrait-400.webp"
controls>
<source src="/videos/the-alpha-flame-discovery-trailer-portrait.mp4" type="video/mp4">
Sorry, your browser doesn't support embedded video.
</video>
<button id="trailerPlayBtn" class="trailer-play-btn">
<i class="fad fa-play"></i>
</button>
</div>
<!-- If JS is off, hide the custom play button so native controls are used -->
<noscript>
<style>
#trailerPlayBtn {
display: none !important
}</style>
</noscript>
</div>
</div>
</div>
</div>
</section>
<section class="mb-4">
<div class="card character-card">
<div class="card-body">
<h3 class="h6 text-uppercase text-muted mb-2">★ Audiobook Out Now ★</h3>
<p class="mb-3">
<em>The Alpha Flame: Discovery</em> is now available as a full-length audiobook, narrated to be lived in rather than rushed through.
Its a slow-burn, character-led story where quiet moments matter, tension builds gradually, and certain scenes land harder when you hear them spoken.
</p>
<p class="mb-3">
Ideal for long drives, late nights, or anyone who prefers to sink into a story through headphones rather than skim it on a screen.
</p>
<a asp-controller="Discovery" asp-action="AudioBook" class="btn btn-dark">
🎧 Explore the audiobook
</a>
</div>
</div>
</section>
<!-- Synopsis first (story-first layout) -->
<section id="synopsis" class="mb-4">
<div class="card character-card text-white" style="background: url('/images/webp/synopsis-background-960.webp'); background-position: center; background-size: cover;">
<div class="card-header">
<h2 class="card-title h1 mb-0">The Alpha Flame: <span class="fw-light">Discovery</span></h2>
<p class="mb-0">Survival, secrets, and sisters in 1983 Birmingham.</p>
</div>
<div class="card-body" id="synopsis-body">
<!-- Audio blurb -->
<div class="row align-items-center mb-3">
<div class="col-2">
<responsive-image src="catherine-lynwood-16.png" class="img-fluid rounded-circle border border-2 border-dark" alt="Catherine Lynwood" display-width-percentage="20"></responsive-image>
</div>
<div class="col-10">
<div class="audio-player text-center">
<audio id="player">
<source src="/audio/the-alpha-flame-discovery-catherine.mp3" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
</div>
<p class="text-center text-white small mb-0">Listen to Catherine talking about the book</p>
</div>
</div>
<!-- Full synopsis -->
<div>
<div class="mt-2">
<p class="card-text">She didnt go looking for trouble. But when she found Beth, bruised, broken, and terrified, Maggie couldnt walk away.</p>
<p class="card-text">But nothing prepares her for Beth. As she digs deeper into Beths world, Maggie finds herself pulled into the shadows, a seedy underworld of secrets, survival, and control, where loyalty is rare and nothing is guaranteed. The more she uncovers, the more she realises this isnt someone elses nightmare. Its her own.</p>
<p class="card-text">Set in 1983 Birmingham, nearby Redditch, and Barmouth in Wales, The Alpha Flame: Discovery follows the lives of two young women, Beth and Maggie, as they uncover dark family secrets and fight to survive. Gritty and emotionally charged, it explores the bond between two women who refuse to be broken.</p>
<p class="card-text">For Beth, the world is a cold and unforgiving place. Devastation strikes in a single moment, leaving her isolated, shattered, and vulnerable. Alone in the bleak shadows of a city that offers neither refuge nor redemption, she is forced to navigate a relentless cycle of desperation and despair. Every step of her journey tests the limits of her endurance, pushing her into harrowing situations where survival feels like a hollow victory. Beths existence is marked by loss, betrayal, and an almost suffocating loneliness that threatens to consume her entirely. Yet, even in the darkest corners of her ordeal, a fragile ember of defiance smoulders within her, a quiet, stubborn refusal to let the world destroy her completely.</p>
<p class="card-text">Maggie, by contrast, is a force of nature, a woman who thrives on her unshakable drive and an unrelenting belief in her own power. Behind her fiery red hair and disarming charm lies a storm of determination and ferocity. Maggie doesnt just live; she races through life, fuelled by a need for speed and the thrill of freedom. Her Triumph TR6 isnt just a car; its an extension of her spirit, sleek, powerful, and unapologetically bold. On the open road, with the engine roaring and the world blurring past her, she feels invincible. But Maggies intensity doesnt stop at the wheel. Her relationships burn just as brightly. As a lover, she is dominant, passionate, and unafraid to embrace her darker desires. While fiercely loving and loyal, Maggie is also formidable; crossing her isnt a mistake anyone makes twice.</p>
<p class="card-text">When fate brings Beth and Maggie together, their connection is explosive, a union of two polar opposites that burns with both tenderness and raw power. For Beth, Maggie represents a lifeline, a reminder that love and trust still exist, even in a world that has betrayed her at every turn. For Maggie, Beth awakens a fierce protectiveness and vulnerability shes rarely allowed herself to feel. Together, they ignite a flame that challenges them to confront their own fears, desires, and limitations.</p>
<p class="card-text">Set against the kaleidoscope of 1983, where synthesised anthems provide a pulsing soundtrack and the streets are alive with the bold styles and rebellious energy of the decade, their story unfolds in a city teeming with danger and intrigue. From high speed chases along winding roads to dimly lit clubs and desolate alleyways, the heroines journey is a visceral exploration of survival and freedom. The neon haze of the era contrasts sharply with the stark realities they face, painting a vivid picture of a world where strength and vulnerability coexist.</p>
<p class="card-text">As secrets surface and danger tightens its grip, Beth and Maggie must confront not only the challenges around them but the truths within themselves. Their bond is tested by betrayal, desire, and the shadows of their pasts, but through it all, their flame burns brighter, illuminating their courage and the unbreakable spirit of two heroines determined to rewrite their fates.</p>
<p class="card-text">At its heart, The Alpha Flame is a story of survival, passion, and empowerment. It captures the raw power of human connection against the gritty, vibrant backdrop of an unforgettable era. With its blend of drama, intensity, and unapologetic emotion, this is a story that will leave its mark long after the final page.</p>
</div>
</div>
</div>
</div>
</section>
<!-- Social proof near top -->
@if (showReviews)
{
var top = Model.Reviews.Items.Where(x => x.RatingValue == 5).OrderByDescending(y => y.DatePublished).First();
var fullStars = (int)Math.Floor(top.RatingValue);
var hasHalfStar = top.RatingValue - fullStars >= 0.5;
var emptyStars = 5 - fullStars - (hasHalfStar ? 1 : 0);
var reviewDate = top.DatePublished.ToString("d MMMM yyyy");
<section class="mb-4">
<div class="card character-card">
<div class="card-body">
<h3 class="h6 text-uppercase text-muted mb-2">★ Reader Praise ★</h3>
<blockquote class="blockquote mb-2">
<span class="mb-2 text-warning d-inline-block">
@for (int i = 0; i < fullStars; i++)
{
<i class="fad fa-star"></i>
}
@if (hasHalfStar)
{
<i class="fad fa-star-half-alt"></i>
}
@for (int i = 0; i < emptyStars; i++)
{
<i class="fad fa-star" style="--fa-primary-opacity:0.2;--fa-secondary-opacity:0.2;"></i>
}
</span>
@Html.Raw(top.ReviewBody)
<footer>
@top.AuthorName on
<cite title="@top.SiteName">
@if (string.IsNullOrEmpty(top.URL))
{
@top.SiteName
}
else
{
<a href="@top.URL" target="_blank">@top.SiteName</a>
}
</cite>
<span class="text-muted smaller">, @reviewDate</span>
</footer>
</blockquote>
@if (Model.Reviews.Items.Count > 1)
{
<div class="text-end">
<a asp-action="Reviews" class="btn btn-outline-secondary btn-sm">Read more reviews</a>
</div>
}
</div>
</div>
</section>
}
<!-- Buy Box now AFTER synopsis/review -->
<section class="mb-4" id="buy">
<div class="row">
<div class="col-12 d-flex">
<div class="card character-card h-100 flex-fill" id="buy-card">
<div class="card-body d-flex flex-column">
@await Component.InvokeAsync("BuyPanel", new { ISO2 = Model.UserIso2, Src = Model.Src })
</div>
</div>
</div>
</div>
</section>
<!-- Sticky mobile buy bar (global, still points to #buyBox inside the partial) -->
<div id="mobileBuyBar" class="d-md-none fixed-bottom bg-dark text-white py-2 border-top border-3 border-light" style="z-index:1030;">
<div class="container d-flex justify-content-between align-items-center">
<span class="small">The Alpha Flame: Discovery</span>
<a href="#buyBox" class="btn btn-light btn-sm">Buy now</a>
</div>
</div>
<!-- Chapter Previews -->
<section id="chapters" class="mt-4">
<h2>Chapter Previews</h2>
<div class="row">
<div class="col-md-4 mb-4">
<div class="card h-100 character-card">
<a asp-action="Chapter1">
<responsive-image src="beth-stood-in-bathroom.png" class="card-img-top" alt="Beth's Bathroom" display-width-percentage="50"></responsive-image>
</a>
<div class="card-body border-top border-3 border-dark">
<h3 class="card-title">Chapter 1, Drowning in Silence, Beth</h3>
<p class="card-text">Beth returns home to find her mother lifeless in the bath...</p>
<div class="text-end"><a asp-action="Chapter1" class="btn btn-dark">Read More</a></div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card h-100 character-card">
<a asp-action="Chapter2">
<responsive-image src="maggie-with-her-tr6-2.png" class="fit-image" alt="Maggie With Her TR6" display-width-percentage="50"></responsive-image>
</a>
<div class="card-body border-top border-3 border-dark">
<h3 class="card-title">Chapter 2, The Last Lesson, Maggie</h3>
<p class="card-text">On Christmas Eve, Maggie nervously heads out for her driving test not knowing the story that will pan out before her...</p>
<div class="text-end"><a asp-action="Chapter2" class="btn btn-dark">Read More</a></div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card h-100 character-card">
<a asp-action="Chapter13">
<responsive-image src="pub-from-chapter-13.png" class="fit-image" alt="Pub from Chapter 13" display-width-percentage="50"></responsive-image>
</a>
<div class="card-body border-top border-3 border-dark">
<h3 class="card-title">Chapter 13, A Name She Never Owned, Susie</h3>
<p class="card-text">Susie goes out for a drink with a punter. What on earth could go wrong...</p>
<div class="text-end"><a asp-action="Chapter13" class="btn btn-dark">Read More</a></div>
</div>
</div>
</div>
</div>
</section>
@section Scripts {
<!-- Plyr for audio -->
<script>
const player = new Plyr('audio');
</script>
<!-- Trailer play/pause via custom button or video click (single video) -->
<script>
document.addEventListener("DOMContentLoaded", () => {
const v = document.getElementById("trailerPortrait");
const playBtn = document.getElementById("trailerPlayBtn");
if (!v || !playBtn) return;
// Hide native controls when JS is active; use custom button instead
v.controls = false;
// Start playback
const startPlayback = () => {
v.muted = false;
v.volume = 1.0;
v.play()
.then(() => { playBtn.style.display = "none"; })
.catch(err => console.warn("Video play failed:", err));
};
// Toggle on video click
const togglePlayback = () => {
if (v.paused) {
startPlayback();
} else {
v.pause();
playBtn.style.display = "block";
}
};
// Events
playBtn.addEventListener("click", (e) => { e.preventDefault(); startPlayback(); });
v.addEventListener("click", togglePlayback);
// Keep button state in sync with native events
v.addEventListener("play", () => { playBtn.style.display = "none"; });
v.addEventListener("pause", () => { playBtn.style.display = "block"; });
v.addEventListener("ended", () => { playBtn.style.display = "block"; });
});
</script>
}
@section Meta {
<MetaTag meta-title="The Alpha Flame: Discovery by Catherine Lynwood"
meta-description="A gritty 1980s Birmingham psycological crime novel about two girls uncovering dark family secrets and surviving abuse. Realistic, powerful, and unflinching... discover The Alpha Flame today."
meta-keywords="The Alpha Flame Discovery, Catherine Lynwood, 1983 novel, twin sisters, suspense fiction, Rubery, Birmingham fiction, historical drama, family secrets"
meta-author="Catherine Lynwood"
meta-url="https://www.catherinelynwood.com/the-alpha-flame/discovery"
meta-image="https://www.catherinelynwood.com/images/webp/the-alpha-flame-discovery-cover-1200.webp"
meta-image-png="https://www.catherinelynwood.com/images/the-alpha-flame-discovery-cover.png"
meta-image-alt="Maggie from 'The Alpha Flame: Discovery' by Catherine Lynwood"
og-site-name="Catherine Lynwood - The Alpha Flame: Discovery"
article-published-time="@new DateTime(2024, 11, 20)"
article-modified-time="@new DateTime(2025, 09, 10)"
twitter-card-type="summary_large_image"
twitter-site-handle="@@CathLynwood"
twitter-creator-handle="@@CathLynwood" />
<script type="application/ld+json">
@Html.Raw(Model.Reviews.SchemaJsonLd)
</script>
}

View File

@ -9,7 +9,7 @@
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li> <li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li> <li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li> <li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Extras">Extras</a></li> <li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Extras">Extras</a></li>
<li class="breadcrumb-item active" aria-current="page">Listen</li> <li class="breadcrumb-item active" aria-current="page">Listen</li>
</ol> </ol>
</nav> </nav>

View File

@ -8,7 +8,7 @@
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li> <li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li> <li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li> <li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Extras">Extras</a></li> <li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Extras">Extras</a></li>
<li class="breadcrumb-item active" aria-current="page">Maggie's Designs</li> <li class="breadcrumb-item active" aria-current="page">Maggie's Designs</li>
</ol> </ol>
</nav> </nav>

View File

@ -8,7 +8,7 @@
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li> <li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li> <li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li> <li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Extras">Extras</a></li> <li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Extras">Extras</a></li>
<li class="breadcrumb-item active" aria-current="page">Discovery Scrap Book</li> <li class="breadcrumb-item active" aria-current="page">Discovery Scrap Book</li>
</ol> </ol>
</nav> </nav>

View File

@ -0,0 +1,198 @@
@model List<CatherineLynwood.Models.SoundtrackTrackModel>
@{
ViewData["Title"] = "Alpha Flame • Soundtrack";
}
<section class="container my-4" id="soundtrack">
<header class="mb-4">
<h1 class="h2">The Alpha Flame • Soundtrack</h1>
<p class="text-muted mb-0">Eight original tracks inspired by key chapters; listen while you read…</p>
</header>
<div class="row gy-4">
@if (Model != null && Model.Any())
{
var index = 0;
foreach (var track in Model)
{
var id = $"track-{index++}";
<div class="col-12">
<article class="card shadow-sm h-100">
<div class="row g-0 align-items-stretch">
<!-- Image + Play/Pause -->
<div class="col-12 col-md-5 col-lg-3">
<div class="position-relative h-100">
<responsive-image src="@track.ImageUrl" class="img-fluid w-100 h-100 object-fit-cover rounded-start" alt="@track.Title image" display-width-percentage="50"></responsive-image>
<button type="button"
class="btn btn-light btn-lg rounded-circle position-absolute top-50 start-50 translate-middle track-toggle"
aria-label="Play @track.Title"
data-audio-id="@id">
<i class="fad fa-play"></i>
</button>
</div>
</div>
<!-- Text -->
<div class="col-12 col-md-7 col-lg-9">
<div class="card-body d-flex flex-column">
<h2 class="h4 mb-2">@track.Title</h2>
@if (!string.IsNullOrWhiteSpace(track.Chapter) || !string.IsNullOrWhiteSpace(track.Description))
{
<p class="text-muted small mb-3">
@if (!string.IsNullOrWhiteSpace(track.Chapter))
{
<span><strong>Chapter:</strong> @track.Chapter</span>
}
@if (!string.IsNullOrWhiteSpace(track.Chapter) && !string.IsNullOrWhiteSpace(track.Description))
{
<span> • </span>
}
@if (!string.IsNullOrWhiteSpace(track.Description))
{
<span>@track.Description</span>
}
</p>
}
<div class="lyrics border rounded p-3 mb-3 overflow-auto"
style="max-height: 300px;">
@if (!string.IsNullOrWhiteSpace(track.LyricsHtml))
{
@Html.Raw(track.LyricsHtml)
}
</div>
<div class="mt-auto">
<button type="button"
class="btn btn-outline-dark me-2 track-toggle"
aria-label="Play @track.Title"
data-audio-id="@id">
<i class="fad fa-play me-1"></i> <span>Play</span>
</button>
<span class="text-muted small" data-duration-for="@id"></span>
</div>
<!-- Hidden audio element -->
<audio id="@id"
preload="metadata"
src="\audio\soundtrack\@track.AudioUrl"
data-title="@track.Title"></audio>
</div>
</div>
</div>
</article>
</div>
}
}
else
{
<div class="col-12">
<div class="alert alert-info">
Tracks will appear here soon.
</div>
</div>
}
</div>
<noscript>
<div class="alert alert-warning mt-4">Enable JavaScript to play the soundtrack.</div>
</noscript>
</section>
<style>
/* Keep images nicely cropped */
.object-fit-cover { object-fit: cover; }
/* Make the overlay button stand out on varied artwork */
.track-toggle.btn-light {
--bs-btn-bg: rgba(255,255,255,.9);
--bs-btn-border-color: rgba(0,0,0,.05);
box-shadow: 0 0.5rem 1rem rgba(0,0,0,.15);
width: 3.25rem;
height: 3.25rem;
display: grid;
place-items: center;
}
.track-toggle .fa-play, .track-toggle .fa-pause { font-size: 1.25rem; }
</style>
@section Scripts {
<script>
(function () {
const cards = document.querySelectorAll('#soundtrack article.card');
const toggles = document.querySelectorAll('.track-toggle');
const audios = Array.from(document.querySelectorAll('#soundtrack audio'));
function setAllToStopped(exceptId) {
audios.forEach(a => {
if (a.id !== exceptId) {
a.pause();
a.currentTime = a.currentTime; // stop updating without resetting
updateUI(a, false);
}
});
}
function formatTime(seconds) {
const s = Math.round(seconds);
const m = Math.floor(s / 60);
const r = s % 60;
return `${m}:${r.toString().padStart(2, '0')}`;
}
function updateUI(audio, isPlaying) {
// Update both toggles for this track
const buttons = document.querySelectorAll(`.track-toggle[data-audio-id="${audio.id}"]`);
buttons.forEach(btn => {
const icon = btn.querySelector('i');
const labelSpan = btn.querySelector('span');
if (isPlaying) {
btn.setAttribute('aria-label', `Pause ${audio.dataset.title}`);
if (icon) { icon.classList.remove('fa-play'); icon.classList.add('fa-pause'); }
if (labelSpan) { labelSpan.textContent = 'Pause'; }
} else {
btn.setAttribute('aria-label', `Play ${audio.dataset.title}`);
if (icon) { icon.classList.remove('fa-pause'); icon.classList.add('fa-play'); }
if (labelSpan) { labelSpan.textContent = 'Play'; }
}
});
}
// Wire up buttons
toggles.forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.getAttribute('data-audio-id');
const audio = document.getElementById(id);
if (!audio) return;
if (audio.paused) {
setAllToStopped(id);
audio.play().then(() => updateUI(audio, true)).catch(() => { /* ignore */ });
} else {
audio.pause();
updateUI(audio, false);
}
});
});
// Keep UI in sync with media events
audios.forEach(audio => {
audio.addEventListener('play', () => {
setAllToStopped(audio.id);
updateUI(audio, true);
});
audio.addEventListener('pause', () => updateUI(audio, false));
audio.addEventListener('ended', () => {
audio.currentTime = 0;
updateUI(audio, false);
});
audio.addEventListener('loadedmetadata', () => {
const slot = document.querySelector(`[data-duration-for="${audio.id}"]`);
if (slot && isFinite(audio.duration)) {
slot.textContent = `Length: ${formatTime(audio.duration)}`;
}
});
});
})();
</script>
}

View File

@ -0,0 +1,488 @@
@model CatherineLynwood.Models.FlagSupportViewModel
@{
ViewData["Title"] = "The Alpha Flame - Coming Soon";
}
<!-- Your existing video container: unchanged -->
<div class="container">
<div class="row">
<div class="col-md-12">
<!-- H1 for SEO and accessibility -->
<header class="mb-2 text-center">
<h1 class="h3 mb-1">The Alpha Flame: <span class="fw-light">Discovery</span></h1>
<p class="mb-1">A gritty Birmingham crime novel set in 1983</p>
</header>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="trailer-wrapper">
<video id="trailerVideo" playsinline preload="none"></video>
<button id="trailerPlayBtn" class="trailer-play-btn">
<i class="fad fa-play"></i>
</button>
</div>
</div>
</div>
</div>
@if (DateTime.Now < new DateTime(2025, 8, 21))
{
<section>
<div class="row justify-content-center">
<div class="col-10 col-md-4 text-center bg-white text-dark border border-dark border-3 rounded-5 mt-3 p-3">
<h3 class="h4">Released in:</h3>
<div class="release-countdown mb-2" data-release="2025-08-21T00:00:00+01:00" data-out-text="Out now">
<span class="rcd d"><span class="num">0</span><span class="label">d</span></span>
<span class="rcd h"><span class="num">00</span><span class="label">h</span></span>
<span class="rcd m"><span class="num">00</span><span class="label">m</span></span>
<span class="rcd s"><span class="num">00</span><span class="label">s</span></span>
</div>
<div class="d-grid px-3"><a asp-controller="Discovery" asp-action="Index" class="btn btn-dark btn-pulse">Pre-order Now!</a></div>
<noscript>Releases on 21 Aug 2025</noscript>
</div>
</div>
</section>
}
<!-- Quick interaction: flags -->
<section class="container py-3">
<div class="row">
<div class="col-12 text-center">
<h2 class="h4 mb-2">Please show your support</h2>
<p class="mb-3">Tap your flag to show your support.</p>
</div>
<div class="col-12">
<div class="flag-grid" role="group" aria-label="Choose your country">
@foreach (var kv in Model.FlagCounts)
{
string pulse = "";
var code = kv.Key;
var count = kv.Value;
var name = code switch
{
"UK" => "UK",
"US" => "US",
"CA" => "Canada",
"AU" => "Australia",
"IE" => "Ireland",
"NZ" => "New&nbsp;Zealand",
_ => code
};
var flagFile = code.ToLower() switch
{
"uk" => "gb",
_ => code.ToLower()
};
if (kv.Selected)
{
pulse = "btn-pulse";
}
<button class="flag-btn @pulse" data-country="@code">
<img src="/images/flags/@($"{flagFile}.svg")" alt="@name flag">
<span class="flag-name">@Html.Raw(name)</span>
<span class="flag-count">(@count)</span>
</button>
}
</div>
<!-- Toast/message area -->
<div id="flagToast" class="flag-toast text-center" role="status" aria-live="polite" style="display:none;">
<p id="flagMessage" class="mb-2"></p>
<!-- Hidden release notification form -->
<div id="releaseForm" style="display:none;">
<p>Want me to let you know when the book is released?</p>
<form id="notifyForm" class="mt-2">
<input type="text" id="notifyName" name="name" placeholder="Your name (optional)" class="form-control form-control-sm mb-2">
<input type="email" id="notifyEmail" name="email" placeholder="Enter your email" class="form-control form-control-sm mb-2" required>
<button type="submit" class="btn btn-sm btn-primary">Notify Me</button>
</form>
</div>
</div>
</div>
</div>
</section>
<section class="container py-3">
<div class="row justify-content-center">
<div class="col-md-6 text-dark border border-dark border-3 rounded-5 p-4" style="background-image:url('/images/sheet-music.png'); background-position: center; background-size: cover;">
<h3 class="h5 text-center text-dark pb-3">Listen to The Alpha Flame theme tune<br />The Flame We Found</h3>
<audio controls="controls">
<source src="~/audio/the-flame-we-found-original-song-inspired-by-alpha-flame_teaser.mp3" type="audio/mpeg" />
</audio>
</div>
</div>
</section>
<!-- Teaser reel -->
<section class="container py-3">
<div class="row">
<div class="col-12">
<h2 class="h4 mb-3 text-center">A glimpse inside</h2>
</div>
<div class="col-md-4">
<article class="teaser-card border border-3 border-dark mb-3">
<div class="teaser-bg" style="background-image:url('/images/webp/the-alpha-flame-discovery-back-cover-400.webp');"></div>
<div class="teaser-copy">
<div>
<p class="h1 text-warning">
The Alpha Flame: Discovery
</p>
<p>
Some girls survive. Others set the world on fire.
</p>
<p>
She didnt go looking for trouble. But when she found Beth, bruised, broken, and terrified, Maggie couldnt walk away.
</p>
<p>
But nothing prepares her for Beth.
</p>
<p>
As she digs deeper into Beths world, Maggie finds herself pulled into the shadows, a seedy underworld of secrets, survival, and control, where loyalty is rare and nothing is guaranteed. The more she uncovers, the more she realises this isnt someone elses nightmare. Its her own.
</p>
<p>
The Alpha Flame: Discovery is a gritty, emotionally charged thriller that pulls no punches. Raw, real, and anything but a fairy tale, its a story of survival, sisterhood, and fire.
</p>
<div class="teaser-actions">
<button class="btn btn-sm btn-light" data-audio="#aud4"><i class="fad fa-play"></i> Listen 58s</button>
</div>
</div>
</div>
<audio id="aud4" preload="none" src="/audio/book-synopsis.mp3"></audio>
</article>
</div>
<div class="col-md-4">
<article class="teaser-card border border-3 border-dark mb-3">
<div class="teaser-bg" style="background-image:url('/images/webp/teaser-city-400.webp');"></div>
<div class="teaser-copy">
<div>
<p>
I eased the TR6 down a side street, the headlights sweeping over a figure shifting in the shadows. A movement to my left. A woman, young, her face pale beneath the heavy makeup, stepped forward as I slowed at the junction. She leaned down to my passenger window, so close I could see the faint smudge of lipstick at the corner of her mouth.
</p>
<p>
A loud knock on the glass made me jump.
</p>
<p>
“You looking for something, love?” she asked, her voice soft but direct. Her lips were parted just slightly, her breath misting against the cold window.
</p>
<p>
My stomach tightened.
</p>
<p>
I wasnt looking for anything. Not really. But I didnt drive away either.
</p>
<p>
She was close now, close enough that I could see the dark liner smudged beneath her eyes, the glint of something unreadable in her gaze. Not quite curiosity. Not quite suspicion. Just a quiet knowing.
</p>
<div class="teaser-actions">
<button class="btn btn-sm btn-light" data-audio="#aud1"><i class="fad fa-play"></i> Listen 50s</button>
</div>
</div>
</div>
<audio id="aud1" preload="none" src="/audio/snippets/clip-1.mp3"></audio>
</article>
</div>
<div class="col-md-4">
<article class="teaser-card border border-3 border-dark mb-3">
<div class="teaser-bg" style="background-image:url('/images/webp/teaser-hospital-400.webp');"></div>
<div class="teaser-copy">
<div>
<p>
“Maggie… wait.”
</p>
<p>
She turned as I crouched down. My stomach dropped.
</p>
<p>
It was a sweatshirt. Pink. Faded. Cartoon print on the front, cracked with age and wear. Garfield, grinning.
</p>
<p>
I reached out slowly, fingertips brushing the fabric. The left sleeve was soaked, stiff with something dark.
</p>
<p>
Blood.
</p>
<p>
“Maggie…” My voice broke. “Its hers. She used to wear this all the time. She was wearing it the last time I saw her.”
</p>
<p>
Maggie dropped to her knees beside me, torch trembling in her grip. “Bloody hell. Youre right.”
</p>
<p>
For a second neither of us moved. The building suddenly felt tighter, like it was watching us.
</p>
<div class="teaser-actions">
<button class="btn btn-sm btn-light" data-audio="#aud2"><i class="fad fa-play"></i> Listen 28s</button>
</div>
</div>
</div>
<audio id="aud2" preload="none" src="/audio/snippets/clip-2.mp3"></audio>
</article>
</div>
<div class="col-md-4 d-md-none">
<article class="teaser-card border border-3 border-dark mb-2">
<div class="teaser-bg" style="background-image:url('/images/webp/teaser-beach-400.webp');"></div>
<div class="teaser-copy">
<div>
<p>
She turned in the water, soaked to the waist, flinging droplets everywhere.
</p>
<p>
“Maggie! Come on!” she shouted, laughing. “Youve got to feel this!”
</p>
<p>
I didnt hesitate.
</p>
<p>
I peeled off my hoody and shorts, left them in a heap on the rocks, and sprinted after her, my bikini clinging tight to my skin in the salty breeze. The sand stung slightly as I ran, then came the cold slap of the sea, wrapping around my legs and dragging a breathless laugh out of me.
</p>
<p>
Beth was already dancing through the waves like a lunatic.
</p>
<p>
We collided mid-splash, both of us soaked, screaming and laughing like we were eight years old again, like wed somehow got all those childhood summers back in one moment.
The sea was freezing, but we didnt care.
</p>
<div class="teaser-actions">
<button class="btn btn-sm btn-light" data-audio="#aud3"><i class="fad fa-play"></i> Listen 37s</button>
</div>
</div>
</div>
<audio id="aud3" preload="none" src="/audio/snippets/clip-3.mp3"></audio>
</article>
</div>
</div>
</section>
<!-- Footer note -->
<section class="container pb-4">
<div class="row justify-content-center">
<div class="col-md-6 text-center">
<h3 class="h4 mb-3"><strong>Coming 21st August 2025</strong> to major retailers.</h3>
<div class="d-grid"><a asp-controller="Discovery" asp-action="Index" class="btn btn-dark btn-pulse">Find Out More</a></div>
</div>
</div>
</section>
@*
<responsive-image src="the-alpha-flame-discovery-trailer-landscape.png" class="img-fluid" alt="The Alpha Flame book cover — gritty 1980s Birmingham crime novel about twin sisters uncovering secrets and surviving abuse" display-width-percentage="100"></responsive-image>
<responsive-image src="the-alpha-flame-discovery-trailer-portrait.png" class="img-fluid" alt="The Alpha Flame book cover — gritty 1980s Birmingham crime novel about twin sisters uncovering secrets and surviving abuse" display-width-percentage="50"></responsive-image>
<responsive-image src="the-alpha-flame-discovery-back-cover.png" class="img-fluid" alt="The Alpha Flame book cover — gritty 1980s Birmingham crime novel about twin sisters uncovering secrets and surviving abuse" display-width-percentage="50"></responsive-image>
*@
@section Scripts {
<script>
const player = new Plyr('audio');
</script>
<script>
document.addEventListener("DOMContentLoaded", () => {
const video = document.getElementById("trailerVideo");
const playBtn = document.getElementById("trailerPlayBtn");
// Pick correct source and poster before loading
const isDesktop = window.matchMedia("(min-width: 400px)").matches;
video.poster = isDesktop
? "/images/webp/the-alpha-flame-discovery-trailer-landscape-1400.webp"
: "/images/webp/the-alpha-flame-discovery-trailer-portrait-400.webp";
const src = isDesktop
? "/videos/the-alpha-flame-discovery-trailer-landscape.mp4"
: "/videos/the-alpha-flame-discovery-trailer-portrait.mp4";
const sourceEl = document.createElement("source");
sourceEl.src = src;
sourceEl.type = "video/mp4";
video.appendChild(sourceEl);
// Play button click handler
playBtn.addEventListener("click", () => {
video.muted = false;
video.volume = 1.0;
video.play().then(() => {
playBtn.style.display = "none"; // hide button once playing
}).catch(err => {
console.warn("Video play failed:", err);
});
});
// --- Teaser audio logic ---
const btnForAudio = new Map();
function anyTeaserPlaying() {
return Array.from(btnForAudio.keys()).some(a => !a.paused && !a.ended);
}
document.querySelectorAll("[data-audio]").forEach(btn => {
const aud = document.querySelector(btn.getAttribute("data-audio"));
if (!aud) return;
btn.dataset.orig = btn.innerHTML;
btnForAudio.set(aud, btn);
aud.addEventListener("ended", () => {
btn.innerHTML = btn.dataset.orig;
if (!anyTeaserPlaying()) {
video.play().catch(() => {});
}
});
});
document.addEventListener("click", e => {
const btn = e.target.closest("[data-audio]");
if (!btn) return;
const aud = document.querySelector(btn.getAttribute("data-audio"));
if (!aud) return;
// Stop others
btnForAudio.forEach((b, a) => {
if (a !== aud) {
a.pause();
a.currentTime = 0;
b.innerHTML = b.dataset.orig;
}
});
if (aud.paused) {
video.pause();
aud.currentTime = 0;
aud.play().then(() => {
btn.innerHTML = '<i class="fad fa-pause"></i> Pause';
});
} else {
aud.pause();
btn.innerHTML = btn.dataset.orig;
if (!anyTeaserPlaying()) {
video.play().catch(() => {});
}
}
});
});
</script>
<script>
let selectedCountry = null;
window.addEventListener("click", function (e) {
const flag = e.target.closest('.flag-btn');
if (!flag) return;
selectedCountry = flag.getAttribute("data-country") || "Your country";
const key = "taf_support_" + selectedCountry;
if (!localStorage.getItem(key)) {
localStorage.setItem(key, "1");
}
// Send click to server and update count
fetch("/api/support/flag", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ country: selectedCountry })
})
.then(res => res.json())
.then(data => {
if (data.ok) {
// Update the flag's count
const countEl = flag.querySelector(".flag-count");
if (countEl) countEl.textContent = `(${data.total})`;
}
});
// Show thank-you + form
document.getElementById("flagMessage").textContent = `Thanks for the love, ${selectedCountry}!`;
document.getElementById("flagToast").style.display = "block";
document.getElementById("releaseForm").style.display = "block";
// Tap animation
if (flag.animate) {
flag.animate(
[{ transform: 'scale(1)' }, { transform: 'scale(1.06)' }, { transform: 'scale(1)' }],
{ duration: 260, easing: 'ease-out' }
);
}
});
document.addEventListener("submit", function (e) {
if (e.target && e.target.id === "notifyForm") {
e.preventDefault();
const emailInput = document.getElementById("notifyEmail");
if (!emailInput.value) return;
fetch("/api/support/subscribe", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
country: selectedCountry,
name: document.getElementById("notifyName").value || null,
email: document.getElementById("notifyEmail").value
})
});
document.getElementById("releaseForm").innerHTML = "<p>Thanks! We'll email you when the book is released.</p>";
}
});
</script>
@if (DateTime.Now < new DateTime(2025, 8, 21))
{
<script>
document.addEventListener('DOMContentLoaded', function () {
function initCountdown(el) {
if (!el) return;
var iso = el.getAttribute('data-release');
var ts = Date.parse(iso);
if (isNaN(ts)) return;
var dEl = el.querySelector('.rcd.d .num');
var hEl = el.querySelector('.rcd.h .num');
var mEl = el.querySelector('.rcd.m .num');
var sEl = el.querySelector('.rcd.s .num');
var outText = el.getAttribute('data-out-text') || 'Out now';
function pad(n) { return n < 10 ? '0' + n : '' + n; }
function tick() {
var now = Date.now();
var diff = ts - now;
if (diff <= 0) {
el.textContent = outText;
clearInterval(timer);
return;
}
var secs = Math.floor(diff / 1000);
var days = Math.floor(secs / 86400); secs -= days * 86400;
var hrs = Math.floor(secs / 3600); secs -= hrs * 3600;
var mins = Math.floor(secs / 60); secs -= mins * 60;
if (dEl) dEl.textContent = days;
if (hEl) hEl.textContent = pad(hrs);
if (mEl) mEl.textContent = pad(mins);
if (sEl) sEl.textContent = pad(secs);
}
tick();
var timer = setInterval(tick, 1000);
}
// Support multiple countdowns on a page
var timers = document.querySelectorAll('.release-countdown');
for (var i = 0; i < timers.length; i++) {
initCountdown(timers[i]);
}
});
</script>
}
}

View File

@ -0,0 +1,39 @@
@{
Layout = "/Views/Shared/_Layout.cshtml";
}
@section CSS {
<style>
.video-overlay {
background-color: rgba(13, 202, 240, 0.2);
}
</style>
}
@section Meta {
@RenderSection("Meta", required: false)
}
@section BackgroundVideo {
<div id="background-wrapper">
<div class="video-background">
<video id="siteBackgroundVideo"
autoplay
muted
loop
playsinline
preload="none">
<!-- Source will be injected by JS -->
</video>
<div class="video-overlay"></div>
</div>
</div>
}
@RenderBody()
@section Scripts {
@RenderSection("Scripts", required: false)
}

View File

@ -0,0 +1,28 @@
@{
ViewData["Title"] = "Thank You";
}
<div class="container my-5">
<div class="row justify-content-center">
<div class="col-md-10 col-lg-8 text-center">
<h1 class="display-5 fw-bold mb-4">Thank You for Applying!</h1>
<p class="lead">I'm so grateful you've offered to read <em>The Alpha Flame: Discovery</em> and consider reviewing it. It really means a lot.</p>
<div class="alert alert-info mt-4 text-start">
<h5 class="fw-semibold">📩 Please check your inbox</h5>
<p class="mb-2">I've just sent you an email with setup instructions based on how you said you'd like to receive the book — whether via Kindle or an alternative method.</p>
<p class="mb-2">If it doesn't appear soon, please check your spam or junk folder. If its not there either, feel free to contact me directly.</p>
</div>
<div class="alert alert-warning text-start">
<h5 class="fw-semibold">✅ One last step...</h5>
<p>Once you've completed the setup (or if you need help), please reply to the email and let me know. That way I can get the ARC sent out to you right away.</p>
</div>
<p class="mt-4">Thanks again for your support — it genuinely makes a difference.</p>
<p class="fst-italic">Warmest wishes,<br>Catherine</p>
<a asp-action="Index" class="btn btn-primary mt-3">Return to Home Page</a>
</div>
</div>
</div>

View File

@ -10,7 +10,7 @@
<p class="mb-0"><em>For <strong>The Alpha Flame: Discovery</strong> Advance Reader Copy (ARC)</em></p> <p class="mb-0"><em>For <strong>The Alpha Flame: Discovery</strong> Advance Reader Copy (ARC)</em></p>
</div> </div>
<div class="card-body"> <div class="card-body">
<form asp-action="ArcReaderApplication" id="arcWizardForm" novalidate method="post"> <form asp-action="ArcReaderApplication" id="arcWizardForm" novalidate method="post" onsubmit="disableSubmit(this)">
<div class="progress mb-4" style="height: 1.5rem;"> <div class="progress mb-4" style="height: 1.5rem;">
<div id="arcWizardProgress" class="progress-bar bg-success" style="width: 20%;"> <div id="arcWizardProgress" class="progress-bar bg-success" style="width: 20%;">
Step 1 of 5 Step 1 of 5
@ -23,34 +23,31 @@
<section class="arc-intro"> <section class="arc-intro">
<h1 class="display-5"><i class="fad fa-book-reader me-2 text-primary"></i>Become an ARC Reader</h1> <h1 class="display-5"><i class="fad fa-book-reader me-2 text-primary"></i>Become an ARC Reader</h1>
<p class="lead">Fancy reading <strong>The Alpha Flame: Discovery</strong> before anyone else? I'm looking for passionate early readers to receive a free Kindle copy in exchange for an honest review.</p> <p class="lead">Fancy reading <strong>The Alpha Flame: Discovery</strong> before anyone else? I'm looking for passionate early readers to receive a free Kindle copy in exchange for an honest review.</p>
<button type="button" class="btn btn-dark arc-next mb-3">Start Application</button>
<div class="alert alert-warning mt-4"> <p>
<i class="fad fa-exclamation-triangle me-2"></i> The Alpha Flame: Discovery is a gritty, character-driven crime novel set in 1983 Birmingham. Beth and Maggie are thrown together by fate, each carrying trauma, secrets, and fire. Beth is a survivor — wounded, wary, and haunted by the past. Maggie is bold, passionate, and dangerous to underestimate. Their bond is raw, explosive, and deeply human. Together, they must face a world that wants to break them — and fight back harder.
<strong>Note:</strong> This novel is raw and emotional. Please check the themes below. </p>
</div> <p>
<h2 class="h5 mt-4"><i class="fad fa-tags me-2 text-secondary"></i>Major Themes and Topics</h2> This is a story of survival, sisterhood, and power. Unflinching in its honesty, The Alpha Flame explores abuse, love, identity, and the fragile strength that carries us through the darkest nights. With fast cars, dangerous men, and high-stakes emotion set against the electric backdrop of 1980s Britain, this is not a soft read — but it burns with hope, truth, and fierce female energy.
<ul class="fa-ul"> </p>
<li><span class="fa-li"><i class="fad fa-skull-crossbones text-danger"></i></span> Death, trauma, and grief through the eyes of a teenage girl</li> <p>
<li><span class="fa-li"><i class="fad fa-hand-rock text-danger"></i></span> Physical and sexual abuse (non-graphic, but deeply affecting)</li> If you'd like to know more then take a look at the main book page.
<li><span class="fa-li"><i class="fad fa-procedures text-muted"></i></span> Mental health, including suicidal thoughts and PTSD</li> </p>
<li><span class="fa-li"><i class="fad fa-money-bill-wave text-warning"></i></span> Prostitution, exploitation, and coercion</li> <p>
<li><span class="fa-li"><i class="fad fa-bomb text-danger"></i></span> Violence against women (threats, assaults, murder)</li> <a asp-controller="Discovery" asp-action="Index" class="btn btn-dark">Explore the Book</a>
<li><span class="fa-li"><i class="fad fa-hand-holding-heart text-success"></i></span> Love, trust, tenderness, and emotional recovery</li> </p>
<li><span class="fa-li"><i class="fad fa-transgender text-info"></i></span> Sexuality, orientation, and identity discovery</li>
<li><span class="fa-li"><i class="fad fa-user-friends text-success"></i></span> Found family, sisterhood, and female empowerment</li>
<li><span class="fa-li"><i class="fad fa-mask text-dark"></i></span> Corruption, manipulation, and cover-ups</li>
<li><span class="fa-li"><i class="fad fa-gavel text-dark"></i></span> Justice, revenge, and difficult moral choices</li>
<li><span class="fa-li"><i class="fad fa-music text-secondary"></i></span> Music and fashion as creative expression and survival</li>
</ul>
<p class="mt-4">If youre still interested, Id love to have you on board. ARC readers help spread the word and offer early feedback that matters.</p> <p class="mt-4">If youre still interested, Id love to have you on board. ARC readers help spread the word and offer early feedback that matters.</p>
<p><strong>All I ask:</strong> read the book and leave a review on Goodreads, Amazon, or wherever you normally post.</p> <p><strong>All I ask:</strong> read the book and leave a review on Goodreads, Amazon, or wherever you normally post.</p>
</section> </section>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<button type="button" class="btn btn-link arc-next">
<responsive-image src="the-alpha-flame-discovery-cover.png" class="img-fluid rounded-5 border border-3 border-dark" alt="The Alpha Flame book cover — gritty 1980s Birmingham crime novel about twin sisters uncovering secrets and surviving abuse" display-width-percentage="50"></responsive-image> <responsive-image src="the-alpha-flame-discovery-cover.png" class="img-fluid rounded-5 border border-3 border-dark" alt="The Alpha Flame book cover — gritty 1980s Birmingham crime novel about twin sisters uncovering secrets and surviving abuse" display-width-percentage="50"></responsive-image>
</button>
</div> </div>
</div> </div>
<button type="button" class="btn btn-primary arc-next">Continue</button> <button type="button" class="btn btn-dark arc-next">Start Application</button>
</div> </div>
<div class="arc-step d-none" style="min-height: 65vh;" data-step="2"> <div class="arc-step d-none" style="min-height: 65vh;" data-step="2">
<div class="row"> <div class="row">
@ -66,61 +63,56 @@
<div class="form-group"> <div class="form-group">
<label asp-for="Email"></label> <label asp-for="Email"></label>
<input asp-for="Email" class="form-control" /> <input asp-for="Email" class="form-control" />
<small class="form-text text-muted">This is where Ill send updates and reminders.</small> <small class="form-text text-muted">This is where Ill send updates and reminders, and generally keep in touch.</small>
<span asp-validation-for="Email" class="text-danger"></span> <span asp-validation-for="Email" class="text-danger"></span>
</div> </div>
</div> </div>
<div class="col-12 mt-3"> <div class="col-12 mt-3">
<button type="button" class="btn btn-secondary arc-back">Back</button> <button type="button" class="btn btn-secondary arc-back">Back</button>
<button type="button" class="btn btn-primary arc-next">Continue</button> <button type="button" class="btn btn-dark arc-next">Continue</button>
</div> </div>
</div> </div>
</div> </div>
<div class="arc-step d-none" style="min-height: 65vh;" data-step="3"> <div class="arc-step d-none" style="min-height: 65vh;" data-step="3">
<div class="row"> <div class="row">
<div class="col-9"> <div class="col-md-12">
<div class="alert alert-info"> <div class="alert alert-info">
<p>ARCs are sent via Kindle only. You can use the free Kindle app or a Kindle device.</p> <p><strong>How will you receive the ARC?</strong></p>
<p>Add the following sender to your Amazon Kindle Approved Senders list: <strong id="arc-email">[enable JavaScript to view]</strong></p> <p>To protect the unpublished book from piracy, I send all ARC copies securely via Kindle. This sends the book directly to your Kindle app or device — no files to download, nothing to forward.</p>
<p> <p><strong>You dont need a physical Kindle.</strong> The free Kindle app works on phones, tablets, and desktops.</p>
To find your kindle email follow this link <a href="https://www.amazon.co.uk/myk" target="_blank">amazon.co.uk/myk</a>. <p class="mt-3">Just let me know below if you have Kindle access. Ill email you simple step-by-step instructions to get everything set up.</p>
Once there click on Preferences and then scroll down to Personal Document Settings, as shown in the screen shot.
</p>
</div>
<div class="form-group">
<label asp-for="KindleEmail"></label>
<div class="input-group mb-3">
<input asp-for="KindleEmail" class="form-control" />
<span class="input-group-text">@@kindle.com</span>
</div>
<span asp-validation-for="KindleEmail" class="text-danger"></span>
</div> </div>
<!-- Kindle Access -->
<div class="form-group"> <div class="form-group">
<label asp-for="ApprovedSender"></label> <label asp-for="HasKindleAccess" class="form-label"></label>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" asp-for="ApprovedSender" value="Yes" id="ApprovedYes" /> <input class="form-check-input" type="radio" asp-for="HasKindleAccess" value="Yes" id="HasKindleYes" />
<label class="form-check-label" for="ApprovedYes">Yes — Ive added your email</label> <label class="form-check-label" for="HasKindleYes">Yes — I can receive it by Kindle</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" asp-for="ApprovedSender" value="NotYet" id="ApprovedNotYet" /> <input class="form-check-input" type="radio" asp-for="HasKindleAccess" value="No" id="HasKindleNo" />
<label class="form-check-label" for="ApprovedNotYet">Not yet — but I will</label> <label class="form-check-label" for="HasKindleNo">No — Ill need an alternative method</label>
</div> </div>
<div class="form-check"> <span asp-validation-for="HasKindleAccess" class="text-danger"></span>
<input class="form-check-input" type="radio" asp-for="ApprovedSender" value="NeedHelp" id="ApprovedNeedHelp" />
<label class="form-check-label" for="ApprovedNeedHelp">I need help</label>
</div> </div>
<span asp-validation-for="ApprovedSender" class="text-danger"></span>
<!-- Optional Kindle Email -->
<div class="form-group mt-3">
<label asp-for="KindleEmail">Your Kindle Email (optional)</label>
<input asp-for="KindleEmail" class="form-control" placeholder="e.g. yourname@kindle.com" />
<span asp-validation-for="KindleEmail" class="text-danger"></span>
<small class="form-text text-muted">If you know your Kindle email address already, pop it in here. Otherwise Ill help you via email.</small>
</div> </div>
</div> </div>
<div class="col-md-3">
<responsive-image src="kindle-setup.png" class="img-fluid rounded-5 border border-3 border-dark" alt="Kindle setup screen shot" display-width-percentage="50"></responsive-image>
</div>
<div class="col-12 mt-3"> <div class="col-12 mt-3">
<button type="button" class="btn btn-secondary arc-back">Back</button> <button type="button" class="btn btn-secondary arc-back">Back</button>
<button type="button" class="btn btn-primary arc-next">Continue</button> <button type="button" class="btn btn-dark arc-next">Continue</button>
</div> </div>
</div> </div>
</div> </div>
<div class="arc-step d-none" style="min-height: 65vh;" data-step="4"> <div class="arc-step d-none" style="min-height: 65vh;" data-step="4">
<div class="row"> <div class="row">
@ -168,7 +160,7 @@
</div> </div>
<div class="col-12 mt-3"> <div class="col-12 mt-3">
<button type="button" class="btn btn-secondary arc-back">Back</button> <button type="button" class="btn btn-secondary arc-back">Back</button>
<button type="button" class="btn btn-primary arc-next">Continue</button> <button type="button" class="btn btn-dark arc-next">Continue</button>
</div> </div>
</div> </div>
</div> </div>
@ -177,7 +169,10 @@
<div class="col-12"> <div class="col-12">
<!-- Content Fit --> <!-- Content Fit -->
<div class="alert alert-info mt-3"> <div class="alert alert-info mt-3">
I'm interested in the type of fiction you enjoy reading. I write what I describe as <a asp-controller="Home" asp-action="VerosticGenre" class="alert-link">verostic fiction</a>, I've even got a page on this website describing it. I'm interested in the type of fiction you enjoy reading.
I write what I describe as <a asp-controller="Home" asp-action="VerosticGenre" asp-route-step="5" class="alert-link">verostic fiction</a>,
it's A literary genre or descriptive tone characterised by raw emotional realism, unflinching psychological depth, and grounded human truth.
Often gritty, sometimes painful, but always sincere.
</div> </div>
<div class="form-group"> <div class="form-group">
<label asp-for="ContentFit"></label> <label asp-for="ContentFit"></label>
@ -263,6 +258,16 @@
}); });
</script> </script>
<script>
function disableSubmit(form) {
const button = form.querySelector('button[type="submit"]');
if (button) {
button.disabled = true;
button.innerText = 'Sending...'; // Optional
}
}
</script>
<script> <script>
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
const steps = document.querySelectorAll('.arc-step'); const steps = document.querySelectorAll('.arc-step');
@ -288,7 +293,6 @@
let isValid = true; let isValid = true;
inputs.forEach(input => { inputs.forEach(input => {
// This uses jQuery Unobtrusive Validation to validate Razor-bound fields
if (!$(input).valid()) { if (!$(input).valid()) {
isValid = false; isValid = false;
} }
@ -297,6 +301,57 @@
return isValid; return isValid;
} }
// Restore values
document.querySelectorAll('input, textarea, select').forEach(el => {
const name = el.name;
if (el.type === 'checkbox') {
const stored = localStorage.getItem(name);
if (stored) {
try {
const values = JSON.parse(stored);
if (Array.isArray(values)) {
el.checked = values.includes(el.value);
} else {
el.checked = stored === 'true';
}
} catch {
el.checked = false;
}
}
} else if (el.type === 'radio') {
const stored = localStorage.getItem(name);
if (stored && el.value === stored) {
el.checked = true;
}
} else {
const stored = localStorage.getItem(name);
if (stored !== null) {
el.value = stored;
}
}
});
// Save values
document.querySelectorAll('input, textarea, select').forEach(el => {
el.addEventListener('change', () => {
const name = el.name;
if (el.type === 'checkbox') {
const checkboxes = document.querySelectorAll(`input[type="checkbox"][name="${name}"]`);
const checkedValues = Array.from(checkboxes)
.filter(cb => cb.checked)
.map(cb => cb.value);
localStorage.setItem(name, JSON.stringify(checkedValues));
} else if (el.type === 'radio') {
if (el.checked) {
localStorage.setItem(name, el.value);
}
} else {
localStorage.setItem(name, el.value);
}
});
});
document.querySelectorAll('.arc-next').forEach(btn => { document.querySelectorAll('.arc-next').forEach(btn => {
btn.addEventListener('click', function () { btn.addEventListener('click', function () {
@ -321,7 +376,6 @@
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
// Try to find first step with invalid field
for (let i = 0; i < steps.length; i++) { for (let i = 0; i < steps.length; i++) {
const stepFields = steps[i].querySelectorAll('input, select, textarea'); const stepFields = steps[i].querySelectorAll('input, select, textarea');
for (const field of stepFields) { for (const field of stepFields) {
@ -336,9 +390,16 @@
} }
}); });
// 🌟 NEW: Handle "step" in URL query string
const urlParams = new URLSearchParams(window.location.search);
const stepFromUrl = parseInt(urlParams.get('step'));
if (stepFromUrl && stepFromUrl > 0 && stepFromUrl <= steps.length) {
currentStep = stepFromUrl;
}
// Initial step display
showStep(currentStep); showStep(currentStep);
}); });
</script> </script>
} }

View File

@ -12,17 +12,16 @@
</div> </div>
</div> </div>
<div class="row justify-content-center align-items-center mb-5">
<div class="row align-items-center mb-5">
<div class="col-md-5 text-center"> <div class="col-md-5 text-center">
<a asp-controller="Discovery" asp-action="Index"> <a asp-controller="Reckoning" asp-action="Index">
<div class="hero-video-container"> <div class="hero-video-container">
<video id="heroVideo" autoplay muted loop playsinline preload="none" poster="/images/webp/the-alpha-flame-discovery-blank-400.webp"> <video id="heroVideo" autoplay muted loop playsinline preload="none" poster="/images/webp/the-alpha-flame-reckoning-400.webp">
<!-- Source will be injected later --> <!-- Source will be injected later -->
</video> </video>
<!-- Overlay image remains untouched --> <!-- Overlay image remains untouched -->
<responsive-image src="the-alpha-flame-discovery-overlay.png" <responsive-image src="the-alpha-flame-reckoning-overlay.png"
class="hero-overlay-image" class="hero-overlay-image"
alt="The Alpha Flame: Discovery by Catherine Lynwood" alt="The Alpha Flame: Discovery by Catherine Lynwood"
display-width-percentage="50" display-width-percentage="50"
@ -33,34 +32,88 @@
</div> </div>
<div class="col-md-7 pt-2 pt-md-0"> <div class="col-md-7 pt-2 pt-md-0">
<h1 class="display-5 fw-bold">The Alpha Flame: <span class="fw-light">Discovery</span></h1>
<p class="lead fst-italic tagline-shadow">Some girls survive. Others set the world on fire.</p> <h1 class="display-5 fw-bold">
The Alpha Flame
<span class="fw-light d-block fs-4">A Trilogy by Catherine Lynwood</span>
</h1>
<p class="lead fst-italic tagline-shadow">
Some girls survive. Others set the world on fire.
</p>
<p> <p>
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... Maggie Grant never meant to save anyone. But when she finds Beth bruised, terrified, and running from something unspeakable, walking away is no longer an option.
What begins as a single act of kindness pulls them into a world of buried secrets, quiet violence, and truths that refuse to stay hidden.
</p> </p>
<div class="d-flex gap-3 flex-wrap">
<a asp-controller="Discovery" asp-action="Index" class="btn btn-dark">Explore the Book</a> <hr class="my-4" />
<a asp-controller="Discovery" asp-action="Extras" class="btn btn-outline-dark">Unlock Extras</a>
<h2 class="h4 fw-bold">
The Alpha Flame: <span class="fw-light">Discovery</span>
</h2>
<p>
The first novel introduces Maggie and Beth and the fragile, dangerous bond that forms between them.
Set against 1980s Birmingham, <em>Discovery</em> is a story of survival, loyalty, and the moment a life quietly changes forever.
</p>
<div class="d-flex gap-3 flex-wrap mb-4">
<a asp-controller="Discovery" asp-action="Index" class="btn btn-dark">Explore Discovery</a>
<a asp-controller="TheAlphaFlame" asp-action="Extras" class="btn btn-outline-dark">Unlock Extras</a>
</div> </div>
<p class="mt-4">
<h2 class="h6 d-inline"><em>The Alpha Flame: Discovery</em></h2>, writen by <a asp-controller="Home" asp-action="AboutCatherineLynwood" class="link-dark fw-semibold">Catherine Lynwood</a>, 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. <h2 class="h4 fw-bold">
The journey continues in <strong>Reckoning</strong> (Spring 2026) and concludes with <strong>Redemption</strong> (Autumn 2026). The Alpha Flame: <span class="fw-light">Reckoning</span>
Learn more about the full trilogy on the <a asp-controller="TheAlphaFlame" asp-action="Index" class="link-dark fw-semibold">Alpha Flame series page</a>. </h2>
<p>
As pasts collide and consequences close in, Maggie and Beth are forced to confront the truth of who they are, and what they are willing to become.
<em>Reckoning</em> deepens the danger, raises the stakes, and proves that survival always comes at a cost.
</p> </p>
<div class="d-flex gap-3 flex-wrap mb-4">
<a asp-controller="Reckoning" asp-action="Index" class="btn btn-dark">Discover Reckoning</a>
<a asp-controller="TheAlphaFlame" asp-action="Index" class="btn btn-outline-dark">View the Full Trilogy</a>
</div>
<p class="mt-4">
<em>The Alpha Flame</em>, written by
<a asp-controller="Home" asp-action="AboutCatherineLynwood" class="link-dark fw-semibold">Catherine Lynwood</a>,
is a powerful trilogy of UK historical fiction following the intertwined lives of Maggie and Beth.
The story begins with <strong>Discovery</strong>, intensifies in <strong>Reckoning</strong> (Spring 2026),
and concludes with <strong>Redemption</strong> (Autumn 2026).
</p>
@if (DateTime.Now < new DateTime(2025, 9, 1)) @if (DateTime.Now < new DateTime(2025, 9, 1))
{ {
<p class="mt-4"> <hr class="my-4" />
<h2 class="display-6 fw-bold">Win: <span class="fw-light">a Collectors Edition of The Alpha Flame: Discovery</span></h2>
<em>Exclusive, limited, beautiful.</em> <p>
<h2 class="display-6 fw-bold">
Win:
<span class="fw-light">A Collectors Edition of The Alpha Flame: Discovery</span>
</h2>
<em>Exclusive. Limited. Beautiful.</em>
</p> </p>
<div class="d-flex gap-3 flex-wrap mt-4"> <div class="d-flex gap-3 flex-wrap mt-4">
<a asp-controller="TheAlphaFlame" asp-action="Giveaways" class="btn btn-dark">Enter now for your chance.</a> <a asp-controller="TheAlphaFlame" asp-action="Giveaways" class="btn btn-dark">
Enter now for your chance
</a>
</div> </div>
} }
<p class="mt-4"> <p class="mt-4">
<h3 class="h6 d-inline"><em>About The Alpha Flame Trilogy</em></h3>, is gripping UK historical fiction set in 1980s Birmingham. These novels combine family drama, dark secrets, and emotional suspense as two sisters fight for truth and redemption. Perfect for readers who enjoy tense, character-driven stories and literary suspense in a richly described real-world setting. <h3 class="h6 d-inline"><em>About the Trilogy</em></h3>
Set in 1980s Birmingham, <em>The Alpha Flame</em> blends family drama, emotional suspense,
and dark secrets rooted in real places and lived experience.
Ideal for readers who value character-driven storytelling, moral complexity,
and stories that linger long after the final page.
</p> </p>
</div> </div>
</div> </div>
@ -72,6 +125,7 @@
meta-author="Catherine Lynwood" meta-author="Catherine Lynwood"
meta-url="https://www.catherinelynwood.com/" meta-url="https://www.catherinelynwood.com/"
meta-image="https://www.catherinelynwood.com/images/webp/the-alpha-flame-discovery-cover-600.webp" meta-image="https://www.catherinelynwood.com/images/webp/the-alpha-flame-discovery-cover-600.webp"
meta-image-png="https://www.catherinelynwood.com/images/the-alpha-flame-discovery-cover.png"
meta-image-alt="Cover artwork from 'The Alpha Flame: Discovery' by Catherine Lynwood" meta-image-alt="Cover artwork from 'The Alpha Flame: Discovery' by Catherine Lynwood"
og-site-name="Catherine Lynwood The Alpha Flame" og-site-name="Catherine Lynwood The Alpha Flame"
article-published-time="@new DateTime(2024, 11, 20)" article-published-time="@new DateTime(2024, 11, 20)"
@ -191,7 +245,7 @@
setTimeout(() => { setTimeout(() => {
const video = document.getElementById("heroVideo"); const video = document.getElementById("heroVideo");
const source = document.createElement("source"); const source = document.createElement("source");
source.src = "/videos/background-5.mp4"; source.src = "/videos/the-alpha-flame-reckoning.mp4";
source.type = "video/mp4"; source.type = "video/mp4";
video.appendChild(source); video.appendChild(source);
video.load(); // Triggers actual file load video.load(); // Triggers actual file load

View File

@ -13,6 +13,18 @@
</div> </div>
</div> </div>
@if (ViewData["step"] != null)
{
var step = ViewData["step"];
<div class="row">
<div class="col-12">
<a class="btn btn-dark mt-4" asp-controller="Home" asp-action="ArcReaderApplication" asp-route-step="@step">
<i class="fad fa-arrow-left me-1"></i> Back to Application
</a>
</div>
</div>
}
<section class="container py-2"> <section class="container py-2">
<h1 class="display-4 mb-4">The Verostic Genre</h1> <h1 class="display-4 mb-4">The Verostic Genre</h1>

View File

@ -0,0 +1,335 @@
@{
ViewData["Title"] = "AI for Authors";
}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-12 col-xl-10">
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb small">
<li class="breadcrumb-item">
<a asp-controller="IndieAuthor" asp-action="Index">Indie Author Guide</a>
</li>
<li class="breadcrumb-item active" aria-current="page">AI for Authors</li>
</ol>
</nav>
<header class="mb-5">
<span class="badge text-bg-dark mb-3">Indie Author Guide</span>
<h1 class="display-5 fw-bold mb-3">AI for authors, practical use without losing your soul</h1>
<p class="lead mb-3">
AI has become one of the biggest talking points in writing and publishing, which is hardly surprising.
It can generate text, create images, read words aloud, suggest ideas, and generally poke its nose into
almost every creative workflow going. That does not mean authors need to hand over the keys to the kingdom.
</p>
<p class="mb-0">
This page looks at how AI can be used as a practical tool for indie authors, from brainstorming and
image inspiration to editing support and voice playback, while keeping the actual writing, judgement
and creative direction firmly in human hands where they belong.
</p>
</header>
<div class="row g-4">
<div class="col-lg-8">
<section id="what-ai-is-good-for" class="mb-5">
<h2 class="h3 fw-bold mb-3">What AI is actually good for</h2>
<p>
AI is often most useful when it is treated as an assistant rather than an author. It can help
generate options, organise thoughts, test ideas, summarise information, and speed up certain
repetitive tasks.
</p>
<p>
Used sensibly, it can be helpful for:
</p>
<ul>
<li>brainstorming ideas</li>
<li>testing scene descriptions</li>
<li>creating image inspiration</li>
<li>reading text aloud with synthetic voices</li>
<li>spotting awkward phrasing or repetition</li>
<li>helping structure notes and workflows</li>
</ul>
<p class="mb-0">
It is strongest when used to support your process, not replace your creative brain.
</p>
</section>
<section id="what-ai-is-bad-for" class="mb-5">
<h2 class="h3 fw-bold mb-3">What AI is bad for</h2>
<p>
AI can sound convincing while being wrong, bland, generic, or weirdly overconfident. It also has
a strong tendency to produce text that looks smooth on the surface while lacking depth, originality
and genuine emotional intelligence.
</p>
<p>
That makes it a poor replacement for:
</p>
<ul>
<li>the core writing of a novel</li>
<li>authentic voice and character work</li>
<li>subtle emotional judgement</li>
<li>deep originality</li>
<li>final creative decision-making</li>
</ul>
<p class="mb-0">
In other words, it can assist with craft and process, but it is not your imagination and should not be mistaken for it.
</p>
</section>
<section id="brainstorming-and-idea-testing" class="mb-5">
<h2 class="h3 fw-bold mb-3">Brainstorming and testing ideas</h2>
<p>
One of the best uses of AI for authors is as a thinking partner when you are trying to clarify an idea.
That might mean exploring plot possibilities, testing alternate scene directions, or asking for suggestions
when something in the story feels thin or unclear.
</p>
<p>
The value here is not that the AI magically produces the perfect answer. It is that it can throw possibilities
back at you quickly, helping you react, reject, refine and sharpen your own thoughts.
</p>
<p class="mb-0">
Sometimes the most useful AI answer is the one that makes you realise what you do not want.
</p>
</section>
<section id="ai-for-editing-support" class="mb-5">
<h2 class="h3 fw-bold mb-3">Using AI to support editing</h2>
<p>
AI can be useful during editing, especially when you want another angle on the text. It can help identify
repetition, flag clunky phrasing, suggest places that may need tightening, or help summarise structural issues
you are already circling around.
</p>
<p>
That said, its suggestions still need judgement. AI is perfectly capable of recommending changes that flatten
style, simplify voice, or remove exactly the thing that made the writing interesting in the first place.
</p>
<p class="mb-0">
The trick is to use it as a filter for possibilities, not as a final editor issuing commandments from a silicon mountaintop.
</p>
</section>
<section id="ai-voice-playback" class="mb-5">
<h2 class="h3 fw-bold mb-3">AI voice playback and hearing the text</h2>
<p>
One of the most practical uses of AI for authors is synthetic voice playback. Hearing your words read aloud
can reveal problems you simply do not notice while reading silently.
</p>
<p>
It can help expose:
</p>
<ul>
<li>repetition</li>
<li>missing words</li>
<li>awkward rhythm</li>
<li>unnatural dialogue</li>
<li>sentences that look fine but sound dreadful</li>
</ul>
<p>
This can be especially useful because your own eyes are too forgiving. They know what you meant to write and
often glide right over the bits that need fixing.
</p>
<p class="mb-0">
The synthetic voice creates just enough distance to let you hear the work more honestly.
</p>
</section>
<section id="ai-images-for-inspiration" class="mb-5">
<h2 class="h3 fw-bold mb-3">Using AI images for inspiration</h2>
<p>
AI image tools can be genuinely helpful when you are trying to visualise a character, a location, a mood,
or the atmosphere of a scene. Sometimes putting a description into an image model helps clarify what you have
been trying to picture all along.
</p>
<p>
This can be useful for:
</p>
<ul>
<li>character inspiration</li>
<li>setting exploration</li>
<li>cover concept mood boards</li>
<li>marketing ideas</li>
<li>checking whether a description produces the feel you intended</li>
</ul>
<p class="mb-0">
Used this way, AI imagery is less about replacing art and more about helping ideas become visible.
</p>
</section>
<section id="ai-for-cover-concepts" class="mb-5">
<h2 class="h3 fw-bold mb-3">AI in the cover design process</h2>
<p>
AI can also play a role in cover development, not necessarily as the final cover, but as a way to test mood,
composition, symbolism and visual direction before doing the serious design work.
</p>
<p>
It is useful for rough concepts because it is fast. You can try multiple directions without spending hours
building each one from scratch. That speed makes it good for exploration.
</p>
<p class="mb-0">
The danger is confusing a striking AI image with a functional book cover. A cover still needs typography,
hierarchy, genre signalling and all the boring practical bits that actually help sell books.
</p>
</section>
<section id="ai-for-marketing-and-admin" class="mb-5">
<h2 class="h3 fw-bold mb-3">AI for marketing and admin support</h2>
<p>
Beyond writing and visuals, AI can be handy for the more practical side of author life. It can help draft
blog outlines, summarise ideas for newsletters, suggest website structures, help organise launch plans,
or turn messy thoughts into something more structured.
</p>
<p>
This is often where AI shines because these tasks benefit from speed and structure without demanding that
the tool be the true creative source.
</p>
<p class="mb-0">
It is much easier to let AI help with the admin jungle than to trust it with the heart of your novel.
</p>
</section>
<section id="ethical-and-creative-concerns" class="mb-5">
<h2 class="h3 fw-bold mb-3">Ethical and creative concerns</h2>
<p>
AI does raise real questions, and authors are right to think about them seriously. Concerns often include
originality, training data, authorship, transparency, and the risk of flooding the market with low-effort work.
</p>
<p>
There is a meaningful difference between using AI as a practical support tool and using it to mass-produce
hollow content while pretending it is the same as real creative labour.
</p>
<p class="mb-0">
Authors should think carefully about where they draw their own line and be honest about how they use these tools.
</p>
</section>
<section id="common-mistakes-with-ai" class="mb-5">
<h2 class="h3 fw-bold mb-3">Common mistakes authors make with AI</h2>
<div class="card border-0 bg-light rounded-4">
<div class="card-body p-4">
<ul class="mb-0">
<li>treating AI output as final rather than provisional</li>
<li>allowing it to flatten personal voice</li>
<li>using it to generate generic prose and calling it creativity</li>
<li>trusting facts or claims without checking them</li>
<li>mistaking speed for quality</li>
<li>letting the tool dictate the project instead of serving it</li>
</ul>
</div>
</div>
</section>
<section id="a-sensible-approach" class="mb-5">
<h2 class="h3 fw-bold mb-3">A sensible way to approach AI</h2>
<p>
The most useful mindset is probably this: let AI help with the bits that benefit from speed, iteration,
and alternate perspectives, but keep the core creative work under human control.
</p>
<p>
That means using it to support:
</p>
<ul>
<li>idea exploration</li>
<li>editing passes</li>
<li>voice playback</li>
<li>visual inspiration</li>
<li>workflow organisation</li>
</ul>
<p class="mb-0">
It does not mean handing over authorship and hoping the machine accidentally produces a soul.
</p>
</section>
<section id="my-view-on-ai" class="mb-5">
<h2 class="h3 fw-bold mb-3">My view on AI for authors</h2>
<p>
I think AI is a useful tool when kept in its proper place. It can support creativity, sharpen process,
speed up exploration, and help authors catch things they might otherwise miss. That is valuable.
</p>
<p>
But I do not think it replaces actual writing, actual judgement, or actual artistic intent. The danger is
not that AI exists. The danger is that people start mistaking convenience for craft.
</p>
<p class="mb-0">
Used properly, AI can make an author more effective. Used lazily, it just makes the work emptier faster.
</p>
</section>
</div>
<div class="col-lg-4">
<div class="sticky-lg-top" style="top: 2rem;">
<div class="card border-0 shadow-sm rounded-4 mb-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">On this page</h2>
<ul class="list-unstyled small mb-0">
<li class="mb-2"><a href="#what-ai-is-good-for" class="text-decoration-none">What AI is good for</a></li>
<li class="mb-2"><a href="#what-ai-is-bad-for" class="text-decoration-none">What AI is bad for</a></li>
<li class="mb-2"><a href="#brainstorming-and-idea-testing" class="text-decoration-none">Brainstorming and ideas</a></li>
<li class="mb-2"><a href="#ai-for-editing-support" class="text-decoration-none">Editing support</a></li>
<li class="mb-2"><a href="#ai-voice-playback" class="text-decoration-none">AI voice playback</a></li>
<li class="mb-2"><a href="#ai-images-for-inspiration" class="text-decoration-none">AI images for inspiration</a></li>
<li class="mb-2"><a href="#ai-for-cover-concepts" class="text-decoration-none">AI for cover concepts</a></li>
<li class="mb-2"><a href="#ai-for-marketing-and-admin" class="text-decoration-none">Marketing and admin</a></li>
<li class="mb-2"><a href="#ethical-and-creative-concerns" class="text-decoration-none">Ethical concerns</a></li>
<li class="mb-2"><a href="#common-mistakes-with-ai" class="text-decoration-none">Common mistakes</a></li>
<li class="mb-2"><a href="#a-sensible-approach" class="text-decoration-none">A sensible approach</a></li>
<li><a href="#my-view-on-ai" class="text-decoration-none">My view</a></li>
</ul>
</div>
</div>
<div class="card border-0 bg-light rounded-4 mb-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Quick takeaway</h2>
<p class="small mb-0">
AI is useful as a support tool for ideas, editing, images and workflow, but it is not a substitute
for voice, judgement or genuine creative intent.
</p>
</div>
</div>
<div class="card border-0 bg-white shadow-sm rounded-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Related guides</h2>
<ul class="list-unstyled small mb-0">
<li class="mb-2">
<a asp-controller="IndieAuthor" asp-action="EditingAndProofreading" class="text-decoration-none">
Editing and Proofreading
</a>
</li>
<li class="mb-2">
<a asp-controller="IndieAuthor" asp-action="CoverDesign" class="text-decoration-none">
Cover Design
</a>
</li>
<li>
<a asp-controller="IndieAuthor" asp-action="TraditionalVsSelfPublishing" class="text-decoration-none">
Traditional vs Self Publishing
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="border-top pt-4 mt-5">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
<a asp-controller="IndieAuthor" asp-action="AuthorFinances" class="btn btn-dark">
Previous: Author Finances
</a>
<a asp-controller="IndieAuthor" asp-action="Index" class="btn btn-dark">
Back to guide index
</a>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,360 @@
@{
ViewData["Title"] = "Amazon Advertising for Indie Authors";
}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-12 col-xl-10">
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb small">
<li class="breadcrumb-item">
<a asp-controller="IndieAuthor" asp-action="Index">Indie Author Guide</a>
</li>
<li class="breadcrumb-item active" aria-current="page">Amazon Advertising</li>
</ol>
</nav>
<header class="mb-5">
<span class="badge text-bg-dark mb-3">Indie Author Guide</span>
<h1 class="display-5 fw-bold mb-3">Amazon Advertising for indie authors</h1>
<p class="lead mb-3">
Amazon Ads can be one of the most obvious ways to get your book in front of readers, and one of
the easiest ways to burn through money while feeling oddly productive. The platform makes it look
wonderfully simple... set a bid, choose some targets, launch a campaign, and wait for readers to
appear. Real life is a bit less romantic.
</p>
<p class="mb-0">
This page looks at how Amazon advertising works for indie authors, the different campaign types,
pay-per-click costs, why a single book often struggles to make advertising profitable, and why
the long-term value usually comes from building a catalogue rather than expecting one title to
carry the entire business on its back.
</p>
</header>
<div class="row g-4">
<div class="col-lg-8">
<section id="what-amazon-ads-are" class="mb-5">
<h2 class="h3 fw-bold mb-3">What Amazon Ads actually are</h2>
<p>
Amazon advertising allows you to pay for placement inside Amazons own shopping ecosystem.
Your book can appear in search results, on product pages, and in other positions where
potential readers might see it while browsing.
</p>
<p>
The core idea is simple. You bid for visibility. If somebody clicks your ad, you pay.
Whether that click turns into a sale is another matter entirely.
</p>
<p class="mb-0">
That means advertising is not really about buying sales directly. It is about paying for
the chance to be seen.
</p>
</section>
<section id="pay-per-click" class="mb-5">
<h2 class="h3 fw-bold mb-3">Pay per click, not pay per sale</h2>
<p>
This is the first thing authors need to understand properly. Amazon Ads are generally
<strong>pay per click</strong>. You pay when somebody clicks the ad, not when they buy
the book.
</p>
<p>
So if your ad gets plenty of clicks but your cover, blurb, reviews, or sample do not
convince people to buy, you still pay for the traffic.
</p>
<p class="mb-0">
In other words, ads do not rescue a weak product page. They simply send more people to it.
</p>
</section>
<section id="campaign-types" class="mb-5">
<h2 class="h3 fw-bold mb-3">The main campaign types</h2>
<p>
Amazon offers different types of ad targeting, and each behaves a little differently.
The most common options for authors are:
</p>
<ul>
<li><strong>Automatic targeting</strong>, where Amazon decides where to place the ad based on your book</li>
<li><strong>Keyword targeting</strong>, where you target search terms readers may use</li>
<li><strong>Product targeting</strong>, where you target specific books or similar product pages</li>
</ul>
<p>
Automatic campaigns can be useful for testing and discovery. Manual campaigns give you more
control and often better insight into what is working.
</p>
<p class="mb-0">
Most authors eventually end up using a mixture rather than relying on one type alone.
</p>
</section>
<section id="keywords-and-targeting" class="mb-5">
<h2 class="h3 fw-bold mb-3">Keywords, product targeting and relevance</h2>
<p>
Advertising works best when the ad is shown to people who are already likely to be interested
in your sort of book. That sounds obvious, yet plenty of campaigns get built on vague hopeful
thinking rather than actual relevance.
</p>
<p>
Good targeting often means:
</p>
<ul>
<li>relevant search keywords</li>
<li>comparable books in similar genres or niches</li>
<li>authors with overlapping readerships</li>
<li>tightly focused themes rather than broad wishful nonsense</li>
</ul>
<p class="mb-0">
A thriller should not be wandering drunkenly into places where cosy romance readers are minding
their own business, unless you enjoy paying for useless clicks.
</p>
</section>
<section id="bids-and-budgets" class="mb-5">
<h2 class="h3 fw-bold mb-3">Bids and daily budgets</h2>
<p>
Two of the main controls in Amazon Ads are your bid and your daily budget.
</p>
<p>
Your <strong>bid</strong> is roughly what you are willing to pay for a click. Your
<strong>daily budget</strong> is how much you are prepared to spend in a day before the
campaign pauses.
</p>
<p>
These settings sound simple, but they interact with competition, ad placement and conversion
rate. Bid too low and the ad barely appears. Bid too high and you can spend money at a truly
irritating speed.
</p>
<p class="mb-0">
Advertising is basically a controlled experiment in buying attention, so small adjustments are
usually wiser than dramatic flailing.
</p>
</section>
<section id="why-one-book-struggles" class="mb-5">
<h2 class="h3 fw-bold mb-3">Why one book often does not make ads profitable</h2>
<p>
This is the bit a lot of people learn the hard way. If you only have one book, the economics
of advertising can be rough.
</p>
<p>
Your ad spend is competing against the royalty from a single sale. Depending on your format,
price point, print cost, and click costs, it can be very difficult to make the numbers work.
</p>
<p>
That is why many indie authors say the real value of ads becomes clearer once you have a
<strong>catalogue</strong>. If a reader discovers one book and then goes on to buy others,
the advertising spend starts to support a bigger ecosystem rather than one lonely product.
</p>
<p class="mb-0">
A portfolio changes the maths. One book usually takes the hit by itself.
</p>
</section>
<section id="the-value-of-a-catalogue" class="mb-5">
<h2 class="h3 fw-bold mb-3">Why a catalogue matters</h2>
<p>
Advertising becomes more interesting when readers have somewhere else to go after the first book.
That could mean:
</p>
<ul>
<li>other books in a series</li>
<li>more books by the same author</li>
<li>linked standalone titles</li>
<li>different formats such as ebook, print and audio</li>
</ul>
<p>
In that situation, an ad is not just trying to recover the profit from one sale. It may be
helping to acquire a reader who returns more than once.
</p>
<p class="mb-0">
That is a much stronger business model than endlessly trying to bully one book into paying for everything.
</p>
</section>
<section id="what-makes-an-ad-convert" class="mb-5">
<h2 class="h3 fw-bold mb-3">What actually makes the ad convert</h2>
<p>
The ad itself is only the front door. Once a reader clicks, the product page does the real work.
</p>
<p>
Things that influence conversion include:
</p>
<ul>
<li>cover design</li>
<li>book title and subtitle</li>
<li>blurb quality</li>
<li>reviews and ratings</li>
<li>sample readability</li>
<li>price</li>
</ul>
<p class="mb-0">
If those things are weak, advertising simply delivers more people to a page that fails to persuade them.
</p>
</section>
<section id="measuring-results" class="mb-5">
<h2 class="h3 fw-bold mb-3">Measuring results properly</h2>
<p>
One of the traps with advertising is obsessing over impressions or clicks without paying enough
attention to what matters further down the chain.
</p>
<p>
Things worth watching include:
</p>
<ul>
<li>click-through rate</li>
<li>cost per click</li>
<li>spend over time</li>
<li>sales attributed to the campaign</li>
<li>overall profitability, not just activity</li>
</ul>
<p class="mb-0">
Busy dashboards can look exciting while quietly draining money. Data needs interpretation, not admiration.
</p>
</section>
<section id="common-mistakes" class="mb-5">
<h2 class="h3 fw-bold mb-3">Common mistakes authors make</h2>
<div class="card border-0 bg-light rounded-4">
<div class="card-body p-4">
<ul class="mb-0">
<li>running ads before the book page is strong enough</li>
<li>bidding too aggressively too early</li>
<li>targeting too broadly</li>
<li>expecting immediate profit from one book</li>
<li>focusing on clicks instead of sales and read-through</li>
<li>changing too many variables at once</li>
<li>assuming more spend automatically means more success</li>
</ul>
</div>
</div>
</section>
<section id="what-amazon-ads-get-right" class="mb-5">
<h2 class="h3 fw-bold mb-3">What Amazon Ads get right</h2>
<div class="card border-0 bg-light rounded-4">
<div class="card-body p-4">
<ul class="mb-0">
<li>they place books in front of readers already browsing Amazon</li>
<li>they are accessible to indie authors without huge budgets</li>
<li>they offer multiple targeting methods</li>
<li>they provide measurable campaign data</li>
<li>they can support long-term visibility if used sensibly</li>
</ul>
</div>
</div>
</section>
<section id="what-amazon-ads-get-wrong" class="mb-5">
<h2 class="h3 fw-bold mb-3">What Amazon Ads get wrong</h2>
<div class="card border-0 bg-light rounded-4">
<div class="card-body p-4">
<ul class="mb-0">
<li>they make advertising look easier than it is</li>
<li>they encourage spending before authors understand the economics</li>
<li>reporting can look clearer than the true business picture</li>
<li>they do not solve weak conversion problems</li>
<li>they can punish optimism with brutal efficiency</li>
</ul>
</div>
</div>
</section>
<section id="my-view-on-amazon-ads" class="mb-5">
<h2 class="h3 fw-bold mb-3">My view on Amazon advertising</h2>
<p>
I think Amazon Ads can be useful, but they need to be approached with realistic expectations.
They are not a magic machine for turning books into profit. They are one tool among many for
increasing visibility.
</p>
<p>
The biggest mindset shift is understanding that advertising often makes more sense as part of a
longer-term author strategy rather than as a quick win on one title. If readers enjoy one book
and go on to buy others, the spend starts to make more sense. Without that wider ecosystem, it
can be a much tougher slog.
</p>
<p class="mb-0">
So yes, use ads if they suit your strategy... but keep one eye on the numbers and the other on
your remaining pound coins.
</p>
</section>
</div>
<div class="col-lg-4">
<div class="sticky-lg-top" style="top: 2rem;">
<div class="card border-0 shadow-sm rounded-4 mb-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">On this page</h2>
<ul class="list-unstyled small mb-0">
<li class="mb-2"><a href="#what-amazon-ads-are" class="text-decoration-none">What Amazon Ads are</a></li>
<li class="mb-2"><a href="#pay-per-click" class="text-decoration-none">Pay per click</a></li>
<li class="mb-2"><a href="#campaign-types" class="text-decoration-none">Campaign types</a></li>
<li class="mb-2"><a href="#keywords-and-targeting" class="text-decoration-none">Keywords and targeting</a></li>
<li class="mb-2"><a href="#bids-and-budgets" class="text-decoration-none">Bids and budgets</a></li>
<li class="mb-2"><a href="#why-one-book-struggles" class="text-decoration-none">Why one book struggles</a></li>
<li class="mb-2"><a href="#the-value-of-a-catalogue" class="text-decoration-none">Why a catalogue matters</a></li>
<li class="mb-2"><a href="#what-makes-an-ad-convert" class="text-decoration-none">What makes an ad convert</a></li>
<li class="mb-2"><a href="#measuring-results" class="text-decoration-none">Measuring results</a></li>
<li class="mb-2"><a href="#common-mistakes" class="text-decoration-none">Common mistakes</a></li>
<li class="mb-2"><a href="#what-amazon-ads-get-right" class="text-decoration-none">What Amazon Ads get right</a></li>
<li class="mb-2"><a href="#what-amazon-ads-get-wrong" class="text-decoration-none">What Amazon Ads get wrong</a></li>
<li><a href="#my-view-on-amazon-ads" class="text-decoration-none">My view</a></li>
</ul>
</div>
</div>
<div class="card border-0 bg-light rounded-4 mb-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Quick takeaway</h2>
<p class="small mb-0">
Amazon Ads can help with visibility, but one book alone often struggles to make the
economics work. A catalogue gives advertising far more room to breathe.
</p>
</div>
</div>
<div class="card border-0 bg-white shadow-sm rounded-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Related guides</h2>
<ul class="list-unstyled small mb-0">
<li class="mb-2">
<a asp-controller="IndieAuthor" asp-action="ReviewsAndArcReaders" class="text-decoration-none">
Reviews and ARC Readers
</a>
</li>
<li class="mb-2">
<a asp-controller="IndieAuthor" asp-action="AuthorFinances" class="text-decoration-none">
Author Finances
</a>
</li>
<li>
<a asp-controller="IndieAuthor" asp-action="Kdp" class="text-decoration-none">
Publishing on KDP
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="border-top pt-4 mt-5">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
<a asp-controller="IndieAuthor" asp-action="ReviewsAndArcReaders" class="btn btn-dark">
Previous: Reviews and ARC Readers
</a>
<a asp-controller="IndieAuthor" asp-action="AuthorFinances" class="btn btn-dark">
Next: Author Finances
</a>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,375 @@
@{
ViewData["Title"] = "Audiobooks and Amazon ACX";
}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-12 col-xl-10">
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb small">
<li class="breadcrumb-item">
<a asp-controller="IndieAuthor" asp-action="Index">Indie Author Guide</a>
</li>
<li class="breadcrumb-item active" aria-current="page">Audiobooks and ACX</li>
</ol>
</nav>
<header class="mb-5">
<span class="badge text-bg-dark mb-3">Indie Author Guide</span>
<h1 class="display-5 fw-bold mb-3">Audiobooks, narration and Amazon ACX</h1>
<p class="lead mb-3">
Audiobooks can make a book feel gloriously alive. A good narrator adds warmth, tone, character and rhythm
in a way the printed page simply cannot. They also introduce a whole new production process, with fresh
costs, technical requirements, and plenty of opportunities for things to go slightly sideways.
</p>
<p class="mb-0">
This page covers how indie authors approach audiobook creation, how ACX fits into the picture, how to
find a narrator, what editing involves, and why the technical side, chapter files, sound levels, opening
credits and all, matters more than many people realise at the start.
</p>
</header>
<div class="row g-4">
<div class="col-lg-8">
<section id="why-audiobooks-matter" class="mb-5">
<h2 class="h3 fw-bold mb-3">Why audiobooks matter</h2>
<p>
Audiobooks are not just an optional extra any more. For many readers, or rather listeners, they
are a major part of how books are consumed. Some people barely sit down with a paperback at all.
They listen while driving, walking, working, cooking, cleaning, or trying to avoid hearing their
own thoughts for five consecutive minutes.
</p>
<p>
For an indie author, an audiobook can expand the reach of a title and give readers another way
into the story. It can also make the work feel more premium and more complete, especially if you
are building a proper catalogue rather than just tossing one lonely book into the void and hoping
for the best.
</p>
<p class="mb-0">
The catch is that audio is not just a file conversion job. It is a performance, a production and
a technical delivery process all rolled into one.
</p>
</section>
<section id="what-is-acx" class="mb-5">
<h2 class="h3 fw-bold mb-3">What is ACX?</h2>
<p>
<strong>ACX</strong> is Amazons audiobook production and distribution platform. It connects rights
holders, such as authors and publishers, with narrators and producers, and provides a route into
the Audible and Amazon ecosystem.
</p>
<p>
For many indie authors, ACX is the most obvious place to start because it offers both the production
marketplace and the distribution link in one place. You can use it to find a narrator, agree terms,
manage the production process, review the files, and publish the finished audiobook.
</p>
<p class="mb-0">
In practical terms, it is often the audiobook equivalent of KDP, though with more moving parts and
far more dependence on the human performance side of the equation.
</p>
</section>
<section id="how-the-process-works" class="mb-5">
<h2 class="h3 fw-bold mb-3">How the audiobook process works</h2>
<p>
From the outside, people often imagine audiobooks are made by handing a manuscript to a narrator
and waiting for magic to occur. In reality, there is usually a proper workflow.
</p>
<p>
A typical audiobook process includes:
</p>
<ul>
<li>preparing the final manuscript</li>
<li>choosing whether to narrate it yourself or hire a narrator</li>
<li>auditioning and selecting a voice artist</li>
<li>agreeing the payment model or royalty arrangement</li>
<li>recording the chapters</li>
<li>editing and cleaning the audio</li>
<li>checking sound levels and technical compliance</li>
<li>reviewing proof files</li>
<li>approving the final audiobook for distribution</li>
</ul>
<p class="mb-0">
None of that is impossible, but it is more involved than uploading a Kindle file and calling it a day.
</p>
</section>
<section id="finding-a-narrator" class="mb-5">
<h2 class="h3 fw-bold mb-3">Finding the right narrator</h2>
<p>
This is one of the biggest creative decisions in the whole process. A narrator can elevate a book
beautifully, or drain the life out of it with all the emotional charm of a satnav reading a tax return.
</p>
<p>
The right narrator is not just somebody with a pleasant voice. They need the right tone for the book,
the right pacing, a good ear for character, and the ability to deliver material naturally over many
hours without sounding forced or theatrical in the wrong way.
</p>
<p>
Things worth listening for in auditions include:
</p>
<ul>
<li>clarity and consistency</li>
<li>emotional fit for the story</li>
<li>accent suitability where relevant</li>
<li>handling of dialogue and character voices</li>
<li>whether the reading sounds alive rather than merely correct</li>
</ul>
<p class="mb-0">
If the narrator does not feel right, the audiobook does not feel right. Simple as that.
</p>
</section>
<section id="paying-for-production" class="mb-5">
<h2 class="h3 fw-bold mb-3">Paying for production, royalty share or upfront cost</h2>
<p>
One of the first financial choices in audiobook production is how the narrator or producer will be
paid. Broadly speaking, you are usually looking at either an upfront payment model, a royalty share
model, or in some cases a mixture of the two.
</p>
<p>
With an upfront model, you pay for the finished audio production directly. That gives you more clarity
over cost and generally more control, but it obviously means paying real money before the audiobook has
earned a penny.
</p>
<p>
With a royalty share arrangement, the narrator shares in the audiobook income instead. That can reduce
upfront cost, which is appealing, but it also means you are sharing revenue later and may have fewer
suitable narrators willing to work on that basis.
</p>
<div class="card border-0 bg-light rounded-4 mt-4">
<div class="card-body p-4">
<h3 class="h5 fw-bold mb-3">Things to weigh up</h3>
<ul class="mb-0">
<li>your available budget</li>
<li>the expected market for the audiobook</li>
<li>whether you want long-term control over income</li>
<li>how attractive your project is to narrators on a royalty basis</li>
<li>how quickly you want to move</li>
</ul>
</div>
</div>
</section>
<section id="editing-the-audio" class="mb-5">
<h2 class="h3 fw-bold mb-3">Editing the audio</h2>
<p>
Audio editing is where a lot of the invisible work lives. Even if the narration performance is strong,
the files may still need tidying, noise reduction, spacing adjustments, pickup edits, level balancing,
and general polishing so the end result sounds smooth and professional.
</p>
<p>
This part tends to be wildly underestimated by people who have never done it before. It is not just
about trimming a few pauses. It is about making the listening experience feel seamless.
</p>
<p class="mb-0">
If you are handling any of this yourself, decent tools and careful listening matter. Audio is ruthless.
Once you notice a click, volume jump or weird mouth noise, you cannot unhear the damned thing.
</p>
</section>
<section id="sound-levels-and-technical-requirements" class="mb-5">
<h2 class="h3 fw-bold mb-3">Sound levels and technical requirements</h2>
<p>
Audiobook platforms do not just want a nice reading. They also want files that meet technical standards.
That means paying attention to things like loudness, peak levels, noise floor, file format and consistent
chapter delivery.
</p>
<p>
This is the bit that often catches people out. You can have a great narration and still fail technical
checks if the files are not prepared properly.
</p>
<p>
Typical areas that matter include:
</p>
<ul>
<li>overall loudness level</li>
<li>peak level limits</li>
<li>background noise or hiss</li>
<li>consistent sound from chapter to chapter</li>
<li>clean opening and closing space</li>
</ul>
<p class="mb-0">
Glamorous? No. Important? Absolutely.
</p>
</section>
<section id="chapter-files-and-credits" class="mb-5">
<h2 class="h3 fw-bold mb-3">Chapter files, opening credits and closing credits</h2>
<p>
Audiobooks are normally delivered as separate audio files for each chapter or section, along with opening
and closing credits. The structure matters because it affects both the listener experience and platform
acceptance.
</p>
<p>
You usually need to think about:
</p>
<ul>
<li>intro and title announcements</li>
<li>chapter naming and ordering</li>
<li>front matter and back matter</li>
<li>end credits wording</li>
<li>clean file naming and organisation</li>
</ul>
<p class="mb-0">
A tidy structure makes everything easier, from proofing to upload to later corrections.
</p>
</section>
<section id="proofing-and-approvals" class="mb-5">
<h2 class="h3 fw-bold mb-3">Proofing and approvals</h2>
<p>
Before the audiobook goes live, somebody needs to listen through and catch mistakes. That can include
misreads, repeated lines, missing words, awkward pronunciations, pacing problems, technical faults, or
anything else that jars.
</p>
<p>
Proofing audio is time-consuming because there is no shortcut around actually listening. If the audiobook
is ten hours long, there are not many magical ways around spending a large amount of time with it.
</p>
<p class="mb-0">
This is one of those stages where patience pays off. Rushing here is how errors make it into the final release.
</p>
</section>
<section id="cover-and-metadata" class="mb-5">
<h2 class="h3 fw-bold mb-3">Audiobook cover and metadata</h2>
<p>
The audiobook also needs proper presentation. That includes suitable cover artwork in the right format,
clean metadata, author and narrator details, title information, and a professional product description.
</p>
<p>
Even though the listener experiences the book through sound, the cover still matters. People are still
browsing store pages, still making snap judgments, and still deciding whether the book looks worth their time.
</p>
<p class="mb-0">
Audio may be heard with the ears, but it is still sold with the packaging.
</p>
</section>
<section id="what-acx-gets-right" class="mb-5">
<h2 class="h3 fw-bold mb-3">What ACX gets right</h2>
<div class="card border-0 bg-light rounded-4">
<div class="card-body p-4">
<ul class="mb-0">
<li>It connects authors with narrators and producers in one place</li>
<li>It gives indie authors a realistic route into the Audible ecosystem</li>
<li>It provides a defined production workflow</li>
<li>It helps structure the approval and delivery process</li>
<li>It lowers the barrier to entering audiobook publishing</li>
</ul>
</div>
</div>
</section>
<section id="what-acx-gets-wrong" class="mb-5">
<h2 class="h3 fw-bold mb-3">What ACX gets wrong</h2>
<div class="card border-0 bg-light rounded-4">
<div class="card-body p-4">
<ul class="mb-0">
<li>It can make audiobook production look simpler than it really is</li>
<li>The technical side can feel intimidating for new authors</li>
<li>Distribution and exclusivity choices can be restrictive</li>
<li>It still depends heavily on finding the right narrator, which is never a guaranteed quick win</li>
<li>Audio production is slow compared with ebook publishing</li>
</ul>
</div>
</div>
</section>
<section id="my-view-on-audiobooks" class="mb-5">
<h2 class="h3 fw-bold mb-3">My view on audiobooks as an indie author</h2>
<p>
Audiobooks are brilliant, but they are not the easy add-on some people imagine. They take time, attention,
money, technical care and a lot of listening. When done well, though, they can add enormous value to a book
and create a completely different experience for readers.
</p>
<p>
I think the key is to treat the audiobook as its own production, not as an afterthought. It deserves the same
care as the print and ebook versions, because once listeners press play, they are trusting you with hours of
their time and a voice will now carry your story instead of the readers own imagination.
</p>
<p class="mb-0">
That is a powerful thing when it works. And painfully obvious when it does not.
</p>
</section>
</div>
<div class="col-lg-4">
<div class="sticky-lg-top" style="top: 2rem;">
<div class="card border-0 shadow-sm rounded-4 mb-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">On this page</h2>
<ul class="list-unstyled small mb-0">
<li class="mb-2"><a href="#why-audiobooks-matter" class="text-decoration-none">Why audiobooks matter</a></li>
<li class="mb-2"><a href="#what-is-acx" class="text-decoration-none">What is ACX?</a></li>
<li class="mb-2"><a href="#how-the-process-works" class="text-decoration-none">How the process works</a></li>
<li class="mb-2"><a href="#finding-a-narrator" class="text-decoration-none">Finding a narrator</a></li>
<li class="mb-2"><a href="#paying-for-production" class="text-decoration-none">Paying for production</a></li>
<li class="mb-2"><a href="#editing-the-audio" class="text-decoration-none">Editing the audio</a></li>
<li class="mb-2"><a href="#sound-levels-and-technical-requirements" class="text-decoration-none">Sound levels and requirements</a></li>
<li class="mb-2"><a href="#chapter-files-and-credits" class="text-decoration-none">Chapter files and credits</a></li>
<li class="mb-2"><a href="#proofing-and-approvals" class="text-decoration-none">Proofing and approvals</a></li>
<li class="mb-2"><a href="#cover-and-metadata" class="text-decoration-none">Cover and metadata</a></li>
<li class="mb-2"><a href="#what-acx-gets-right" class="text-decoration-none">What ACX gets right</a></li>
<li class="mb-2"><a href="#what-acx-gets-wrong" class="text-decoration-none">What ACX gets wrong</a></li>
<li><a href="#my-view-on-audiobooks" class="text-decoration-none">My view</a></li>
</ul>
</div>
</div>
<div class="card border-0 bg-light rounded-4 mb-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Quick takeaway</h2>
<p class="small mb-0">
Audiobooks can hugely expand a books reach, but they are a full production job, not a quick export button.
</p>
</div>
</div>
<div class="card border-0 bg-white shadow-sm rounded-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Related guides</h2>
<ul class="list-unstyled small mb-0">
<li class="mb-2">
<a asp-controller="IndieAuthor" asp-action="Kdp" class="text-decoration-none">
Publishing on KDP
</a>
</li>
<li class="mb-2">
<a asp-controller="IndieAuthor" asp-action="IngramSpark" class="text-decoration-none">
Publishing on IngramSpark
</a>
</li>
<li>
<a asp-controller="IndieAuthor" asp-action="AuthorFinances" class="text-decoration-none">
Author Finances
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="border-top pt-4 mt-5">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
<a asp-controller="IndieAuthor" asp-action="ingramSpark" class="btn btn-dark">
Previous: Publishing on IngramSpark
</a>
<a asp-controller="IndieAuthor" asp-action="ReviewsAndArcReaders" class="btn btn-dark">
Next: Reviews and ARC Readers
</a>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,394 @@
@{
ViewData["Title"] = "Author Finances";
}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-12 col-xl-10">
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb small">
<li class="breadcrumb-item">
<a asp-controller="IndieAuthor" asp-action="Index">Indie Author Guide</a>
</li>
<li class="breadcrumb-item active" aria-current="page">Author Finances</li>
</ol>
</nav>
<header class="mb-5">
<span class="badge text-bg-dark mb-3">Indie Author Guide</span>
<h1 class="display-5 fw-bold mb-3">Author finances and the real cost of self-publishing</h1>
<p class="lead mb-3">
One of the biggest myths around self-publishing is that once your book is live, the money simply
starts rolling in. It does not. Or at least, not for most people. The financial side of indie
publishing is a mixture of costs, royalties, advertising spend, platform deductions, print
margins, and the occasional unpleasant surprise.
</p>
<p class="mb-0">
This page looks at the financial reality behind independent publishing, including setup costs,
royalties, print economics, advertising, and why building a catalogue usually matters far more
than expecting a single book to carry the whole operation.
</p>
</header>
<div class="row g-4">
<div class="col-lg-8">
<section id="the-basic-financial-picture" class="mb-5">
<h2 class="h3 fw-bold mb-3">The basic financial picture</h2>
<p>
Self-publishing income is rarely a simple case of retail price minus profit. Every format
comes with its own pricing model, platform deductions, and practical limitations.
</p>
<p>
Depending on how you publish, the money coming in may be reduced by:
</p>
<ul>
<li>printing costs</li>
<li>platform commission</li>
<li>wholesale discounts</li>
<li>advertising spend</li>
<li>production costs such as editing, cover design or audio</li>
</ul>
<p class="mb-0">
It is perfectly possible to sell books and still make very little actual profit, especially early on.
</p>
</section>
<section id="upfront-costs" class="mb-5">
<h2 class="h3 fw-bold mb-3">Upfront costs</h2>
<p>
Before a book earns anything, it usually costs something. The exact amount varies wildly depending
on how much you do yourself and how much you outsource.
</p>
<div class="table-responsive">
<table class="table table-bordered align-middle">
<thead class="table-light">
<tr>
<th>Typical cost area</th>
<th>Examples</th>
</tr>
</thead>
<tbody>
<tr>
<td>Editing</td>
<td>Professional editing, proofreading, beta reading support</td>
</tr>
<tr>
<td>Cover design</td>
<td>Designer fees, stock assets, software, image generation tools</td>
</tr>
<tr>
<td>Formatting</td>
<td>Interior layout tools, formatting services, proof copies</td>
</tr>
<tr>
<td>ISBNs</td>
<td>Purchasing and registering your own ISBNs</td>
</tr>
<tr>
<td>Audio production</td>
<td>Narration, editing, mastering, platform preparation</td>
</tr>
<tr>
<td>Marketing</td>
<td>Amazon Ads, promotional graphics, giveaways, website costs</td>
</tr>
</tbody>
</table>
</div>
<p class="mb-0">
Some authors keep these costs low by doing a lot themselves. Others invest heavily upfront.
Neither approach guarantees success. It simply changes the risk profile.
</p>
</section>
<section id="royalties-and-income-streams" class="mb-5">
<h2 class="h3 fw-bold mb-3">Royalties and income streams</h2>
<p>
Self-published authors often earn income from several different sources rather than one neat pot.
</p>
<p>
Common income streams include:
</p>
<ul>
<li>Kindle ebook sales</li>
<li>Kindle Unlimited page reads</li>
<li>paperback sales</li>
<li>hardback sales</li>
<li>audiobook sales</li>
<li>direct sales, if you offer them</li>
</ul>
<p class="mb-0">
Each of these may have very different margins, reporting delays, and patterns of performance.
</p>
</section>
<section id="ebook-economics" class="mb-5">
<h2 class="h3 fw-bold mb-3">Ebook economics</h2>
<p>
Ebooks are often the cleanest financial model in self-publishing because there are no print costs.
That does not mean every sale is highly profitable, but it does mean the margin is usually easier
to understand than with print.
</p>
<p>
Pricing still matters, though. Ebook royalties are shaped by the platform rules and the price band
you choose. Price too low and you may struggle to earn enough. Price too high and readers may simply
walk away.
</p>
<p class="mb-0">
Ebook pricing is not just about what feels fair to the author. It is about what the market will accept.
</p>
</section>
<section id="print-economics" class="mb-5">
<h2 class="h3 fw-bold mb-3">Print economics</h2>
<p>
Print books are where many authors meet the cold wind of reality. Long books cost more to print,
hardbacks cost more than paperbacks, and distribution discounts can eat into margins very quickly.
</p>
<p>
For print books, the basic financial picture usually involves:
</p>
<ul>
<li>retail price</li>
<li>printing cost</li>
<li>platform share or wholesale discount</li>
<li>author margin</li>
</ul>
<p class="mb-0">
That is why a beautifully chunky hardback can feel emotionally satisfying while being financially
far less exciting than outsiders imagine.
</p>
</section>
<section id="ingram-and-wholesale" class="mb-5">
<h2 class="h3 fw-bold mb-3">IngramSpark, wholesale discounts and margins</h2>
<p>
IngramSpark introduces a different kind of financial thinking because it is built around access
to the wider book trade. That means wholesale discount becomes a key part of the model.
</p>
<p>
Retailers need room to make their own margin, which means the publisher, in this case you,
gives up part of the cover price before printing costs are even taken into account.
</p>
<p class="mb-0">
The result is that a book can look expensive to readers while still generating only a modest return
for the author.
</p>
</section>
<section id="returns-risk" class="mb-5">
<h2 class="h3 fw-bold mb-3">Returns risk</h2>
<p>
If you enable returns through IngramSpark, you are stepping into one of the weirder traditions of
the book trade. Retailers can return unsold books, and the financial consequences come back to you.
</p>
<p>
That means the wrong settings can create situations where distribution looks promising on paper but
carries genuine financial exposure in practice.
</p>
<p class="mb-0">
Returns are one of those things every indie author should understand before casually clicking yes.
</p>
</section>
<section id="advertising-costs" class="mb-5">
<h2 class="h3 fw-bold mb-3">Advertising costs</h2>
<p>
Advertising is usually one of the fastest-moving expenses in self-publishing. Unlike cover design
or ISBNs, which are relatively fixed, ads can keep draining money every day if you let them.
</p>
<p>
Common realities include:
</p>
<ul>
<li>clicks cost money whether or not readers buy</li>
<li>one book often struggles to make PPC profitable</li>
<li>catalogue depth can make ad spend more worthwhile</li>
<li>poor conversion can quietly waste budget</li>
</ul>
<p class="mb-0">
Advertising can help visibility, but it is not free momentum. It is paid exposure.
</p>
</section>
<section id="the-portfolio-effect" class="mb-5">
<h2 class="h3 fw-bold mb-3">Why a portfolio of books changes the maths</h2>
<p>
A single book has to recover its own costs by itself. That can be a hard road. Once you have multiple
books, the picture changes.
</p>
<p>
A reader acquired through one title might go on to:
</p>
<ul>
<li>buy other books in the series</li>
<li>try your backlist</li>
<li>pick up other formats such as audio or hardback</li>
<li>become a repeat reader over time</li>
</ul>
<p class="mb-0">
That is why many indie authors treat the first book less as a profit engine and more as the beginning
of a catalogue.
</p>
</section>
<section id="cash-flow-vs-profit" class="mb-5">
<h2 class="h3 fw-bold mb-3">Cash flow vs actual profit</h2>
<p>
Seeing royalty income arrive can feel encouraging, and rightly so, but income is not the same thing
as profit. Properly understanding the finances means comparing money coming in with money already spent.
</p>
<p>
That includes things like:
</p>
<ul>
<li>software subscriptions</li>
<li>proof copies</li>
<li>cover and editing costs</li>
<li>advertising spend</li>
<li>website and hosting costs</li>
<li>promotional materials</li>
</ul>
<p class="mb-0">
A book can have sales momentum and still be recovering its investment rather than genuinely “making money” yet.
</p>
</section>
<section id="why-most-first-books-dont-profit" class="mb-5">
<h2 class="h3 fw-bold mb-3">Why most first books do not make much money</h2>
<p>
This is not pessimism. It is just the boring old truth. Most first books do not generate huge profits.
Some barely recover their direct costs at all.
</p>
<p>
That usually comes down to a few simple reasons:
</p>
<ul>
<li>the author has not yet built an audience</li>
<li>there is no backlist to support read-through</li>
<li>marketing is still being learned</li>
<li>production costs land before meaningful sales do</li>
</ul>
<p class="mb-0">
This is why treating Book One as the foundation of something bigger often makes more sense than expecting
immediate riches.
</p>
</section>
<section id="showing-real-numbers" class="mb-5">
<h2 class="h3 fw-bold mb-3">Showing real numbers</h2>
<p>
One of the most useful things an author website can do is show approximate real figures rather than vague
motivational waffle. That could include examples such as:
</p>
<ul>
<li>print cost versus retail price</li>
<li>royalty per paperback or hardback sale</li>
<li>ebook royalty examples at different price points</li>
<li>advertising spend versus attributed sales</li>
<li>monthly or lifetime totals by format</li>
</ul>
<p class="mb-0">
This page could eventually become a live balance-sheet style section connected to backend data, which
would make it far more useful than the average “you can do it” author advice page.
</p>
</section>
<section id="my-view-on-author-finances" class="mb-5">
<h2 class="h3 fw-bold mb-3">My view on author finances</h2>
<p>
I think the healthiest way to look at self-publishing finances is with honesty and patience. The first book
may not make much money. It may not even break even for a while. That does not automatically mean it has failed.
</p>
<p>
Sometimes the real value of a book is that it establishes your name, builds experience, attracts readers,
and becomes part of a growing body of work. Financially, the long game usually matters more than the first
little spike of sales.
</p>
<p class="mb-0">
The dream is not just to publish a book. It is to build something that can keep earning over time.
</p>
</section>
</div>
<div class="col-lg-4">
<div class="sticky-lg-top" style="top: 2rem;">
<div class="card border-0 shadow-sm rounded-4 mb-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">On this page</h2>
<ul class="list-unstyled small mb-0">
<li class="mb-2"><a href="#the-basic-financial-picture" class="text-decoration-none">The basic financial picture</a></li>
<li class="mb-2"><a href="#upfront-costs" class="text-decoration-none">Upfront costs</a></li>
<li class="mb-2"><a href="#royalties-and-income-streams" class="text-decoration-none">Royalties and income streams</a></li>
<li class="mb-2"><a href="#ebook-economics" class="text-decoration-none">Ebook economics</a></li>
<li class="mb-2"><a href="#print-economics" class="text-decoration-none">Print economics</a></li>
<li class="mb-2"><a href="#ingram-and-wholesale" class="text-decoration-none">Ingram and wholesale</a></li>
<li class="mb-2"><a href="#returns-risk" class="text-decoration-none">Returns risk</a></li>
<li class="mb-2"><a href="#advertising-costs" class="text-decoration-none">Advertising costs</a></li>
<li class="mb-2"><a href="#the-portfolio-effect" class="text-decoration-none">Portfolio effect</a></li>
<li class="mb-2"><a href="#cash-flow-vs-profit" class="text-decoration-none">Cash flow vs profit</a></li>
<li class="mb-2"><a href="#why-most-first-books-dont-profit" class="text-decoration-none">Why first books struggle</a></li>
<li class="mb-2"><a href="#showing-real-numbers" class="text-decoration-none">Showing real numbers</a></li>
<li><a href="#my-view-on-author-finances" class="text-decoration-none">My view</a></li>
</ul>
</div>
</div>
<div class="card border-0 bg-light rounded-4 mb-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Quick takeaway</h2>
<p class="small mb-0">
Self-publishing income is real, but so are the costs. The financial picture usually makes far
more sense once you have multiple books rather than one title doing all the heavy lifting.
</p>
</div>
</div>
<div class="card border-0 bg-white shadow-sm rounded-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Related guides</h2>
<ul class="list-unstyled small mb-0">
<li class="mb-2">
<a asp-controller="IndieAuthor" asp-action="AmazonAdvertising" class="text-decoration-none">
Amazon Advertising
</a>
</li>
<li class="mb-2">
<a asp-controller="IndieAuthor" asp-action="IngramSpark" class="text-decoration-none">
Publishing on IngramSpark
</a>
</li>
<li>
<a asp-controller="IndieAuthor" asp-action="KdpVsIngramSpark" class="text-decoration-none">
KDP vs IngramSpark
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="border-top pt-4 mt-5">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
<a asp-controller="IndieAuthor" asp-action="AmazonAdvertising" class="btn btn-dark">
Previous: Amazon Advertising
</a>
<a asp-controller="IndieAuthor" asp-action="AiForAuthors" class="btn btn-dark">
Next: AI for Authors
</a>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,356 @@
@{
ViewData["Title"] = "Cover Design";
}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-12 col-xl-10">
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb small">
<li class="breadcrumb-item">
<a asp-controller="IndieAuthor" asp-action="Index">Indie Author Guide</a>
</li>
<li class="breadcrumb-item active" aria-current="page">Cover Design</li>
</ol>
</nav>
<header class="mb-5">
<span class="badge text-bg-dark mb-3">Indie Author Guide</span>
<h1 class="display-5 fw-bold mb-3">Cover design for indie authors</h1>
<p class="lead mb-3">
However much writers may wish otherwise, readers absolutely judge a book by its cover.
The cover is not just decoration. It is packaging, branding, positioning and salesmanship
all rolled into one. Before anyone reads the blurb, downloads a sample or listens to a chapter,
the cover is already doing its work, or failing to.
</p>
<p class="mb-0">
This page looks at what makes a strong cover, the realities of designing one as an indie author,
how AI can be useful as part of the creative process, and why getting the cover good enough to
compete matters far more than indulging every artistic whim you happen to have that week.
</p>
</header>
<div class="row g-4">
<div class="col-lg-8">
<section id="why-covers-matter" class="mb-5">
<h2 class="h3 fw-bold mb-3">Why covers matter so much</h2>
<p>
A cover has one brutal job, to make the right reader stop scrolling.
</p>
<p>
It needs to signal genre, tone and quality within seconds. A good cover helps a reader
feel that the book belongs in the same world as other books they already enjoy. A weak
cover creates hesitation, and hesitation kills clicks.
</p>
<p class="mb-0">
The painful truth is that even a brilliant story can be quietly sabotaged by a poor cover.
</p>
</section>
<section id="what-a-cover-needs-to-do" class="mb-5">
<h2 class="h3 fw-bold mb-3">What a cover actually needs to do</h2>
<p>
A strong cover is not just about looking pretty. It needs to work as a piece of visual communication.
</p>
<p>
At minimum, a cover should:
</p>
<ul>
<li>look professional</li>
<li>match the genre or market expectation</li>
<li>remain clear at thumbnail size</li>
<li>present the title and author name clearly</li>
<li>create curiosity or emotional pull</li>
</ul>
<p class="mb-0">
If it fails at those things, it does not matter how meaningful the symbolism is to you personally.
Readers are not standing in a gallery decoding your artistic soul. They are shopping.
</p>
</section>
<section id="genre-signals" class="mb-5">
<h2 class="h3 fw-bold mb-3">Genre signals and reader expectations</h2>
<p>
Covers work partly because readers learn visual shorthand. Certain fonts, colour palettes,
layouts and image styles immediately suggest thriller, romance, fantasy, historical fiction,
literary fiction, and so on.
</p>
<p>
That does not mean every cover must be identical, but it does mean a cover should not confuse
the target audience. If your thriller looks like light womens fiction, or your dark family
drama looks like a cosy village mystery, you are making life harder for yourself.
</p>
<p class="mb-0">
A cover should promise the right sort of reading experience before the reader even clicks.
</p>
</section>
<section id="thumbnail-test" class="mb-5">
<h2 class="h3 fw-bold mb-3">The thumbnail test</h2>
<p>
One of the most important tests for any modern cover is how it looks at a very small size.
Most readers first encounter books as tiny rectangles on a screen, not as glorious full-size
jackets under warm bookshop lighting. In many cases these thumbnails are also in black and white.
Only recently have Amazon started to produce Colour Kindle. My very first cover looked great,
right up to the point where you viewed it as black and white. At that point it merged into a
mess of grey and greyer. Not pretty, and no use on a Kindle search.
</p>
<p>
At thumbnail size, the cover should still:
</p>
<ul>
<li>be readable enough to identify</li>
<li>have a strong overall silhouette</li>
<li>avoid looking muddy or cluttered</li>
<li>hold together visually</li>
</ul>
<p class="mb-0">
If the design only works when viewed full screen at 200 percent, it is not really working where it matters.
</p>
</section>
<section id="title-and-typography" class="mb-5">
<h2 class="h3 fw-bold mb-3">Title treatment and typography</h2>
<p>
Typography does a huge amount of heavy lifting on a cover. The font choice, size, spacing
and position of the title can change the whole feel of the design.
</p>
<p>
Common mistakes include:
</p>
<ul>
<li>fonts that do not suit the genre</li>
<li>text that is too small</li>
<li>poor contrast against the background</li>
<li>too many different font styles fighting each other</li>
<li>trying to be clever when simple would be stronger</li>
</ul>
<p class="mb-0">
If readers cannot quickly read the title, that is not mysterious. It is just annoying.
</p>
</section>
<section id="doing-it-yourself" class="mb-5">
<h2 class="h3 fw-bold mb-3">Doing it yourself as an indie author</h2>
<p>
Many indie authors design their own covers, at least initially, either to save money or because
they already have some design skills. That can work very well, but it comes with a trap.
</p>
<p>
The trap is that authors are often too emotionally attached to the story to see the cover as a sales tool.
They start designing for themselves rather than for the reader.
</p>
<p class="mb-0">
If you design your own cover, you need to think like a marketer as well as a creator.
</p>
</section>
<section id="working-with-a-designer" class="mb-5">
<h2 class="h3 fw-bold mb-3">Working with a designer</h2>
<p>
Hiring a professional cover designer can be a very smart investment, especially if design is not
your strength or you want a result that competes immediately at a high level.
</p>
<p>
A good designer can help with:
</p>
<ul>
<li>genre positioning</li>
<li>clean typography</li>
<li>layout and hierarchy</li>
<li>print-ready files</li>
<li>consistency across a series or author brand</li>
</ul>
<p class="mb-0">
The trick is finding someone who understands the market for your type of book, not just someone
who can make attractive pictures.
</p>
</section>
<section id="using-ai-for-inspiration" class="mb-5">
<h2 class="h3 fw-bold mb-3">Using AI for inspiration</h2>
<p>
AI can be genuinely useful in the cover process, not necessarily as the final cover itself,
but as a way of exploring mood, composition, character looks, setting ideas and visual possibilities.
</p>
<p>
It can help authors:
</p>
<ul>
<li>test visual ideas quickly</li>
<li>clarify what they are trying to describe</li>
<li>explore colour and atmosphere</li>
<li>create reference material for later design work</li>
</ul>
<p class="mb-0">
Used sensibly, AI is not replacing the cover design process. It is helping spark and refine ideas.
</p>
</section>
<section id="ai-strengths-and-limits" class="mb-5">
<h2 class="h3 fw-bold mb-3">The strengths and limits of AI-generated imagery</h2>
<p>
AI image tools can be brilliant for concept exploration, but they are not magical.
They can produce striking visuals quickly, but they can also generate bizarre details,
inconsistent anatomy, muddled typography, and compositions that collapse the moment
you ask for something specific and technically precise.
</p>
<p>
This makes them useful for inspiration, mockups and experimentation, but not automatically
a replacement for proper design judgement.
</p>
<p class="mb-0">
A cover still needs human taste, selection and refinement, otherwise you are just letting the machine
throw visual spaghetti at the wall and hoping some of it looks expensive.
</p>
</section>
<section id="print-vs-ebook-cover" class="mb-5">
<h2 class="h3 fw-bold mb-3">Ebook covers vs full print covers</h2>
<p>
It is worth remembering that an ebook cover and a full print cover are not the same thing.
The front cover is only part of the package for print editions.
</p>
<p>
Print preparation may also involve:
</p>
<ul>
<li>spine width calculation</li>
<li>back cover layout</li>
<li>barcode placement</li>
<li>bleed and trim setup</li>
<li>colour profile considerations</li>
</ul>
<p class="mb-0">
A design that looks fine as a flat front cover still needs to survive the practical realities of print production.
</p>
</section>
<section id="common-cover-mistakes" class="mb-5">
<h2 class="h3 fw-bold mb-3">Common cover mistakes</h2>
<div class="card border-0 bg-light rounded-4">
<div class="card-body p-4">
<ul class="mb-0">
<li>trying to cram too much onto the cover</li>
<li>poor text contrast</li>
<li>weak hierarchy between title and author name</li>
<li>using imagery that does not match the books tone</li>
<li>designing for personal symbolism instead of market impact</li>
<li>forgetting how the cover looks at thumbnail size</li>
<li>letting AI output dictate the design without proper editing</li>
</ul>
</div>
</div>
</section>
<section id="series-branding" class="mb-5">
<h2 class="h3 fw-bold mb-3">Series branding and consistency</h2>
<p>
If you are writing a series, cover consistency becomes even more important. Readers should be able
to recognise related books at a glance.
</p>
<p>
That does not mean every cover must be identical, but it should feel as though the books belong together.
Consistency in fonts, layout, colour treatment or visual style helps create a recognisable brand.
</p>
<p class="mb-0">
That is especially valuable online, where your books may appear next to one another on a retailer page.
</p>
</section>
<section id="my-view-on-cover-design" class="mb-5">
<h2 class="h3 fw-bold mb-3">My view on cover design</h2>
<p>
I think cover design is one of the areas where indie authors have to be especially honest with themselves.
Writing a book does not automatically make someone a strong cover designer, in the same way owning a frying pan
does not automatically make someone a chef.
</p>
<p>
That does not mean authors should not be involved. Quite the opposite. But the goal should be to create a cover
that works in the market, not simply one that feels emotionally satisfying in isolation.
</p>
<p class="mb-0">
The best cover is not the one that explains everything. It is the one that makes the right reader want to know more.
</p>
</section>
</div>
<div class="col-lg-4">
<div class="sticky-lg-top" style="top: 2rem;">
<div class="card border-0 shadow-sm rounded-4 mb-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">On this page</h2>
<ul class="list-unstyled small mb-0">
<li class="mb-2"><a href="#why-covers-matter" class="text-decoration-none">Why covers matter</a></li>
<li class="mb-2"><a href="#what-a-cover-needs-to-do" class="text-decoration-none">What a cover needs to do</a></li>
<li class="mb-2"><a href="#genre-signals" class="text-decoration-none">Genre signals</a></li>
<li class="mb-2"><a href="#thumbnail-test" class="text-decoration-none">The thumbnail test</a></li>
<li class="mb-2"><a href="#title-and-typography" class="text-decoration-none">Title and typography</a></li>
<li class="mb-2"><a href="#doing-it-yourself" class="text-decoration-none">Doing it yourself</a></li>
<li class="mb-2"><a href="#working-with-a-designer" class="text-decoration-none">Working with a designer</a></li>
<li class="mb-2"><a href="#using-ai-for-inspiration" class="text-decoration-none">Using AI for inspiration</a></li>
<li class="mb-2"><a href="#ai-strengths-and-limits" class="text-decoration-none">AI strengths and limits</a></li>
<li class="mb-2"><a href="#print-vs-ebook-cover" class="text-decoration-none">Ebook vs print cover</a></li>
<li class="mb-2"><a href="#common-cover-mistakes" class="text-decoration-none">Common mistakes</a></li>
<li class="mb-2"><a href="#series-branding" class="text-decoration-none">Series branding</a></li>
<li><a href="#my-view-on-cover-design" class="text-decoration-none">My view</a></li>
</ul>
</div>
</div>
<div class="card border-0 bg-light rounded-4 mb-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Quick takeaway</h2>
<p class="small mb-0">
A cover is not just art. It is a sales tool. It needs to signal genre, look professional,
and work at thumbnail size if it is going to do its job properly.
</p>
</div>
</div>
<div class="card border-0 bg-white shadow-sm rounded-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Related guides</h2>
<ul class="list-unstyled small mb-0">
<li class="mb-2">
<a asp-controller="IndieAuthor" asp-action="AiForAuthors" class="text-decoration-none">
AI for Authors
</a>
</li>
<li class="mb-2">
<a asp-controller="IndieAuthor" asp-action="AmazonAdvertising" class="text-decoration-none">
Amazon Advertising
</a>
</li>
<li>
<a asp-controller="IndieAuthor" asp-action="TraditionalVsSelfPublishing" class="text-decoration-none">
Traditional vs Self Publishing
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="border-top pt-4 mt-5">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
<a asp-controller="IndieAuthor" asp-action="EditingAndProofReading" class="btn btn-dark">
Previous: Editing and Proofreading
</a>
<a asp-controller="IndieAuthor" asp-action="KdpVsIngramSpark" class="btn btn-dark">
Next: KDP vs IngramSpark
</a>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,398 @@
@{
ViewData["Title"] = "Editing and Proofreading";
}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-12 col-xl-10">
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb small">
<li class="breadcrumb-item">
<a asp-controller="IndieAuthor" asp-action="Index">Indie Author Guide</a>
</li>
<li class="breadcrumb-item active" aria-current="page">Editing and Proofreading</li>
</ol>
</nav>
<header class="mb-5">
<span class="badge text-bg-dark mb-3">Indie Author Guide</span>
<h1 class="display-5 fw-bold mb-3">Editing and proofreading as an indie author</h1>
<p class="lead mb-3">
Finishing a manuscript is not the same thing as finishing a book. In fact, for most authors,
the first full draft is where the real work begins. Editing is where the story gets sharper,
cleaner, more emotionally effective, and less likely to contain the same phrase three times
in one paragraph like it has developed a nervous twitch.
</p>
<p class="mb-0">
This page looks at the difference between editing and proofreading, the stages involved,
the value of reading work aloud, and how tools such as AI voice playback can help expose
awkward phrasing, repetition, typos, pacing problems, and the sort of errors your eyes
glide straight past because your brain thinks it already knows what the sentence says.
</p>
</header>
<div class="row g-4">
<div class="col-lg-8">
<section id="why-editing-matters" class="mb-5">
<h2 class="h3 fw-bold mb-3">Why editing matters</h2>
<p>
A strong idea can be weakened by clumsy delivery. A powerful scene can lose impact
if it drags, repeats itself, or says too much. A good book is not usually written
in one clean burst of genius. It is shaped.
</p>
<p>
Editing is the stage where you begin turning a raw manuscript into something that
feels intentional. It is where you spot structural problems, smooth out the prose,
tighten the dialogue, strengthen weak transitions, and remove the bits that only made
sense because you, the author, already knew what you meant.
</p>
<p class="mb-0">
In other words, editing is not a chore stapled awkwardly onto writing. It is a huge
part of writing.
</p>
</section>
<section id="editing-vs-proofreading" class="mb-5">
<h2 class="h3 fw-bold mb-3">Editing vs proofreading</h2>
<p>
These terms often get lumped together, but they are not the same thing.
</p>
<div class="table-responsive">
<table class="table table-bordered align-middle">
<thead class="table-light">
<tr>
<th>Stage</th>
<th>Main focus</th>
<th>Typical issues</th>
</tr>
</thead>
<tbody>
<tr>
<td>Editing</td>
<td>Improving the writing itself</td>
<td>Structure, pacing, repetition, clarity, tone, dialogue, scene order</td>
</tr>
<tr>
<td>Proofreading</td>
<td>Correcting final surface errors</td>
<td>Typos, spelling, punctuation, formatting slips, missing words</td>
</tr>
</tbody>
</table>
</div>
<p class="mb-0">
Put simply, editing changes the book for the better. Proofreading catches the little
gremlins still running around after the heavy lifting is done.
</p>
</section>
<section id="the-main-stages" class="mb-5">
<h2 class="h3 fw-bold mb-3">The main stages of editing</h2>
<p>
Different authors work differently, but most manuscripts benefit from being tackled
in layers rather than trying to fix absolutely everything at once.
</p>
<p>
Typical stages include:
</p>
<ul>
<li><strong>Structural editing</strong>, looking at plot, chapter order, pacing, character arcs and overall flow</li>
<li><strong>Line editing</strong>, improving the wording, tone, rhythm and readability sentence by sentence</li>
<li><strong>Copy editing</strong>, checking grammar, consistency, punctuation and technical accuracy</li>
<li><strong>Proofreading</strong>, catching final typos and formatting errors before publication</li>
</ul>
<p class="mb-0">
Trying to do all of that simultaneously is a lovely way to go half mad and still miss things.
</p>
</section>
<section id="structural-editing" class="mb-5">
<h2 class="h3 fw-bold mb-3">Structural editing</h2>
<p>
Structural editing is the big-picture stage. This is where you look at whether the
story itself works.
</p>
<p>
Questions at this stage might include:
</p>
<ul>
<li>Does the chapter order make sense?</li>
<li>Does the middle sag?</li>
<li>Are emotional beats landing properly?</li>
<li>Is there repeated information the reader only needs once?</li>
<li>Do character choices feel believable?</li>
<li>Is anything missing that the story actually needs?</li>
</ul>
<p class="mb-0">
This is not the moment to fuss over commas while an entire subplot is limping along with one leg missing.
</p>
</section>
<section id="line-editing" class="mb-5">
<h2 class="h3 fw-bold mb-3">Line editing and tightening the prose</h2>
<p>
Once the structure is sound, the line-by-line work becomes much more worthwhile.
This is where you improve the actual reading experience.
</p>
<p>
Line editing often involves:
</p>
<ul>
<li>cutting repetition</li>
<li>improving rhythm and flow</li>
<li>removing clunky phrasing</li>
<li>tightening dialogue</li>
<li>making description more precise</li>
<li>reducing over-explanation</li>
</ul>
<p class="mb-0">
This stage is where prose starts sounding less like a draft and more like a finished book.
</p>
</section>
<section id="proofreading-stage" class="mb-5">
<h2 class="h3 fw-bold mb-3">Proofreading the final version</h2>
<p>
Proofreading comes late, once the major editing decisions are done. There is no point
polishing sentences you are about to delete, nor lovingly correcting punctuation in a
chapter that might get split in two next week.
</p>
<p>
This final pass is about catching the stubborn leftovers:
</p>
<ul>
<li>spelling mistakes</li>
<li>missing or repeated words</li>
<li>punctuation slips</li>
<li>inconsistent capitalisation</li>
<li>small formatting errors</li>
</ul>
<p class="mb-0">
Proofreading is less glamorous than structural editing, but it is the stage that stops readers
tripping over obvious errors and muttering darkly about standards.
</p>
</section>
<section id="reading-aloud" class="mb-5">
<h2 class="h3 fw-bold mb-3">Why reading aloud works so well</h2>
<p>
Reading text aloud, or hearing it read aloud, is one of the most effective editing tools
there is. The ear catches things the eye happily skips.
</p>
<p>
A sentence can look fine on the page and still sound awkward once spoken. Repetition becomes
more obvious. Dialogue reveals whether it sounds natural or stilted. Overlong sentences stop
pretending to be elegant and instead collapse wheezing into the furniture.
</p>
<p class="mb-0">
If something sounds wrong, it often is wrong, even if you cannot immediately explain why.
</p>
</section>
<section id="using-ai-voices" class="mb-5">
<h2 class="h3 fw-bold mb-3">Using AI voices to catch mistakes</h2>
<p>
One of the most useful modern editing methods is to use AI-generated voice playback
to listen back to your chapters. It creates enough distance from the text that you begin
hearing the words more like a reader would experience them.
</p>
<p>
This can be brilliant for spotting:
</p>
<ul>
<li>repeated phrases</li>
<li>typos that the eye keeps missing</li>
<li>unnatural dialogue</li>
<li>rhythm problems</li>
<li>accidental word duplication</li>
<li>sentences that are technically correct but still sound wrong</li>
</ul>
<p>
It is especially useful because your brain is far too willing to autocorrect your own work
as you read it silently. Hearing the text externally cuts through that nonsense.
</p>
<p>
I use a service called ElevenReader. It's by those clever ElevenLabs people. Now everyone has differing
opinions about using AI, but hear me out... literally.
</p>
<audio controls class="w-100">
<source src="~/audio/editing-and-proof-reading.mp3" type="audio/mpeg" />
Your browser does not support the audio element.
</audio>
<p class="small">
Read on or click the play button.
</p>
<p>
When I write a chapter, and especially if it's a longer one I load it up into the ElevenReader app
and listen to it back many times. It's like having a friend or family member read your work back to
you, only they don't get fed up the 10th time you ask them.
</p>
<p>
What I tend to do is listen to it with the manuscript in front of me. You'd be surprised at how many
typos and misused words you include and would not see if you were purely editing the document alone.
One of the mistakes I made in my first book was typing the word "form" instead of "from". I must have
read that passage twenty times and never saw it, but as soon as I listened to it back, it stood out like
a sore thumb.
</p>
<p>
Repetition is another gotcha. I found I would use words like "amazing" far too often. I'd write them in
dialogue and again, on the page it looked fine. But, oh my word, when you hear it back, you realise how
awful it sounds. Seriously... try it. You'll thank me.
</p>
<p class="mb-0">
Used properly, AI voice playback is not replacing editing. It is giving you another angle of attack.
</p>
</section>
<section id="common-things-to-watch-for" class="mb-5">
<h2 class="h3 fw-bold mb-3">Common things to watch for</h2>
<div class="card border-0 bg-light rounded-4">
<div class="card-body p-4">
<ul class="mb-0">
<li>repetition of words, ideas or emotional beats</li>
<li>characters saying the same thing in slightly different ways</li>
<li>scenes that start too early or end too late</li>
<li>awkward transitions between chapters</li>
<li>overwritten description</li>
<li>dialogue that sounds written rather than spoken</li>
<li>small continuity slips</li>
<li>paragraphs that simply go on too long</li>
</ul>
</div>
</div>
</section>
<section id="beta-readers-and-feedback" class="mb-5">
<h2 class="h3 fw-bold mb-3">Beta readers and outside feedback</h2>
<p>
However carefully you edit, there comes a point where you are too close to the work.
Fresh eyes matter.
</p>
<p>
Beta readers can help identify:
</p>
<ul>
<li>confusing sections</li>
<li>slow chapters</li>
<li>unconvincing character decisions</li>
<li>plot points that need clearer setup</li>
<li>moments that are emotionally stronger or weaker than you realised</li>
</ul>
<p class="mb-0">
Not every piece of feedback should be obeyed like holy scripture, but patterns matter. If several people
stumble over the same section, the section is probably the problem.
</p>
</section>
<section id="editing-over-time" class="mb-5">
<h2 class="h3 fw-bold mb-3">Why time helps</h2>
<p>
Distance improves editing. Leaving a manuscript alone for a while, even briefly, can make a huge difference.
When you come back, you are more likely to notice what is actually on the page rather than what you remember writing.
</p>
<p>
This is one reason long projects often improve through multiple passes spread over time. The more distance you gain,
the less your brain protects the text from criticism.
</p>
<p class="mb-0">
Annoying, really, because it means patience helps and everyone wants the book finished yesterday.
</p>
</section>
<section id="my-view-on-editing" class="mb-5">
<h2 class="h3 fw-bold mb-3">My view on editing as an indie author</h2>
<p>
Editing is where a book earns its right to exist in public. Drafting creates the raw material,
but editing is where that material becomes readable, controlled and deliberate.
</p>
<p>
I think one of the most useful things an indie author can do is build a repeatable editing process.
That might include silent reading, printed markup, AI voice playback, beta readers, and multiple passes
focused on different problems. The exact method can vary, but the principle is the same... do not rely on one pass.
</p>
<p class="mb-0">
Your first draft is you telling yourself the story. Editing is you making it work for everyone else.
</p>
</section>
</div>
<div class="col-lg-4">
<div class="sticky-lg-top" style="top: 2rem;">
<div class="card border-0 shadow-sm rounded-4 mb-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">On this page</h2>
<ul class="list-unstyled small mb-0">
<li class="mb-2"><a href="#why-editing-matters" class="text-decoration-none">Why editing matters</a></li>
<li class="mb-2"><a href="#editing-vs-proofreading" class="text-decoration-none">Editing vs proofreading</a></li>
<li class="mb-2"><a href="#the-main-stages" class="text-decoration-none">The main stages</a></li>
<li class="mb-2"><a href="#structural-editing" class="text-decoration-none">Structural editing</a></li>
<li class="mb-2"><a href="#line-editing" class="text-decoration-none">Line editing</a></li>
<li class="mb-2"><a href="#proofreading-stage" class="text-decoration-none">Proofreading</a></li>
<li class="mb-2"><a href="#reading-aloud" class="text-decoration-none">Reading aloud</a></li>
<li class="mb-2"><a href="#using-ai-voices" class="text-decoration-none">Using AI voices</a></li>
<li class="mb-2"><a href="#common-things-to-watch-for" class="text-decoration-none">Things to watch for</a></li>
<li class="mb-2"><a href="#beta-readers-and-feedback" class="text-decoration-none">Beta readers</a></li>
<li class="mb-2"><a href="#editing-over-time" class="text-decoration-none">Why time helps</a></li>
<li><a href="#my-view-on-editing" class="text-decoration-none">My view</a></li>
</ul>
</div>
</div>
<div class="card border-0 bg-light rounded-4 mb-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Quick takeaway</h2>
<p class="small mb-0">
Editing improves the book. Proofreading cleans up what remains. Hearing the text aloud
is one of the best ways to catch what your eyes miss.
</p>
</div>
</div>
<div class="card border-0 bg-white shadow-sm rounded-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Related guides</h2>
<ul class="list-unstyled small mb-0">
<li class="mb-2">
<a asp-controller="IndieAuthor" asp-action="ReviewsAndArcReaders" class="text-decoration-none">
Reviews and ARC Readers
</a>
</li>
<li class="mb-2">
<a asp-controller="IndieAuthor" asp-action="AiForAuthors" class="text-decoration-none">
AI for Authors
</a>
</li>
<li>
<a asp-controller="IndieAuthor" asp-action="CoverDesign" class="text-decoration-none">
Cover Design
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="border-top pt-4 mt-5">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
<a asp-controller="IndieAuthor" asp-action="ISBNs" class="btn btn-dark">
Previous: ISBNs
</a>
<a asp-controller="IndieAuthor" asp-action="CoverDesign" class="btn btn-dark">
Next: Cover Design
</a>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,229 @@
@{
ViewData["Title"] = "Indie Author and Self-Publishing Guide";
}
<div class="container py-5">
<div class="row justify-content-center mb-5">
<div class="col-12 col-xl-10">
<div class="bg-dark rounded-4 p-4 p-md-5 shadow-sm border">
<span class="badge text-bg-dark mb-3">Indie Author Guide</span>
<h1 class="display-5 fw-bold mb-3">Indie Author and Self-Publishing</h1>
<p class="lead mb-3">
Self-publishing gives writers more control than ever before, but it also means learning how
the whole machine works... ISBNs, ebooks, print, audiobooks, pricing, advertising, reviews,
cover design, metadata, and all the fiddly little details nobody warns you about.
</p>
<p class="mb-0">
This section of the site is a practical guide based on real experience. Not theory, not recycled
fluff, and definitely not the fantasy that publishing one book will instantly turn life into a
champagne-fuelled author empire. It is intended as a growing resource for writers who want to
understand what self-publishing actually involves.
</p>
</div>
</div>
</div>
<div class="row justify-content-center mb-4">
<div class="col-12 col-xl-10">
<div class="row g-4">
<div class="col-md-6 col-lg-4">
<div class="card h-100 border-0 shadow-sm rounded-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Traditional vs Self-Publishing</h2>
<p class="text-muted mb-4">
A frank comparison of creative freedom, word count expectations, genre boundaries,
timescales, control, and the trade-offs involved.
</p>
<a asp-controller="IndieAuthor" asp-action="TraditionalVsSelfPublishing" class="btn btn-outline-dark">
Read more
</a>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="card h-100 border-0 shadow-sm rounded-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">ISBNs</h2>
<p class="text-muted mb-4">
What ISBNs are, when you need them, how many to buy, what they cost, and how to
complete the registrations properly.
</p>
<a asp-controller="IndieAuthor" asp-action="Isbns" class="btn btn-outline-dark">
Read more
</a>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="card h-100 border-0 shadow-sm rounded-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Editing and Proofreading</h2>
<p class="text-muted mb-4">
My editing process, including AI voices, listening back to chapters, catching
repetition, awkward phrasing, typos, and structural problems.
</p>
<a asp-controller="IndieAuthor" asp-action="EditingAndProofreading" class="btn btn-outline-dark">
Read more
</a>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="card h-100 border-0 shadow-sm rounded-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Cover Design</h2>
<p class="text-muted mb-4">
Cover ideas, Photoshop, using AI for inspiration, what makes a strong cover, and why
many authors end up doing jobs they never wanted.
</p>
<a asp-controller="IndieAuthor" asp-action="CoverDesign" class="btn btn-outline-dark">
Read more
</a>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="card h-100 border-0 shadow-sm rounded-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">KDP vs IngramSpark</h2>
<p class="text-muted mb-4">
The real-world differences between the two platforms, when to use one, when to use
both, and how distribution actually works.
</p>
<a asp-controller="IndieAuthor" asp-action="KdpVsIngramSpark" class="btn btn-outline-dark">
Read more
</a>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="card h-100 border-0 shadow-sm rounded-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Publishing on KDP</h2>
<p class="text-muted mb-4">
Kindle publishing, KDP Select, Kindle Unlimited, royalties, setup screens, and what
Amazon gets right... and wrong.
</p>
<a asp-controller="IndieAuthor" asp-action="Kdp" class="btn btn-outline-dark">
Read more
</a>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="card h-100 border-0 shadow-sm rounded-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Publishing on IngramSpark</h2>
<p class="text-muted mb-4">
Hardbacks, paperbacks, file preparation, proof approvals, pricing, wholesale discount,
ecommerce links, and the ugly truth about returns.
</p>
<a asp-controller="IndieAuthor" asp-action="IngramSpark" class="btn btn-outline-dark">
Read more
</a>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="card h-100 border-0 shadow-sm rounded-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Audiobooks and ACX</h2>
<p class="text-muted mb-4">
Narrators, ACX, editing audio, chapter files, sound levels, proofing, and why audio
production is its own beast.
</p>
<a asp-controller="IndieAuthor" asp-action="Audiobooks" class="btn btn-outline-dark">
Read more
</a>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="card h-100 border-0 shadow-sm rounded-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Reviews and ARC Readers</h2>
<p class="text-muted mb-4">
Why reviews matter, how difficult they can be to get, the idea behind ARC readers,
and what actually happens in practice.
</p>
<a asp-controller="IndieAuthor" asp-action="ReviewsAndArcReaders" class="btn btn-outline-dark">
Read more
</a>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="card h-100 border-0 shadow-sm rounded-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Amazon Advertising</h2>
<p class="text-muted mb-4">
Pay per click, campaign types, wasted spend, realistic expectations, and why one book
alone usually does not make advertising profitable.
</p>
<a asp-controller="IndieAuthor" asp-action="AmazonAdvertising" class="btn btn-outline-dark">
Read more
</a>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="card h-100 border-0 shadow-sm rounded-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Author Finances</h2>
<p class="text-muted mb-4">
Approximate costs, royalties, ad spend, profit margins, and the numbers behind indie
publishing, with the option to expand this into live figures later.
</p>
<a asp-controller="IndieAuthor" asp-action="AuthorFinances" class="btn btn-outline-dark">
Read more
</a>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="card h-100 border-0 shadow-sm rounded-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">AI for Authors</h2>
<p class="text-muted mb-4">
Using AI as a practical creative tool, from image inspiration to voice playback and
idea testing, without handing over the actual writing.
</p>
<a asp-controller="IndieAuthor" asp-action="AiForAuthors" class="btn btn-outline-dark">
Read more
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row justify-content-center mt-5">
<div class="col-12 col-xl-10">
<div class="rounded-4 border p-4 p-md-5 bg-white text-black shadow-sm">
<h2 class="h3 fw-bold mb-3">A realistic note before you begin</h2>
<p class="mb-3">
Self-publishing can be brilliant. It gives writers control, flexibility, speed, and the ability
to build something entirely their own. But it also means wearing far too many hats, usually at
the same time.
</p>
<p class="mb-0">
Writing the book is only one part of the job. You may also end up acting as editor, formatter,
publisher, marketer, project manager, accountant, and reluctant amateur designer. That sounds a
bit grim, but the upside is that you get to make the decisions... and keep learning as you go.
</p>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,296 @@
@{
ViewData["Title"] = "Publishing on IngramSpark";
}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-12 col-xl-10">
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb small">
<li class="breadcrumb-item">
<a asp-controller="IndieAuthor" asp-action="Index">Indie Author Guide</a>
</li>
<li class="breadcrumb-item active" aria-current="page">Publishing on IngramSpark</li>
</ol>
</nav>
<header class="mb-5">
<span class="badge text-bg-dark mb-3">Indie Author Guide</span>
<h1 class="display-5 fw-bold mb-3">Publishing print books with IngramSpark</h1>
<p class="lead mb-3">
While Amazon KDP is often the easiest way to get a book onto Amazon, IngramSpark exists for a
slightly different reason. It is designed to distribute books into the wider book trade,
including bookshops, libraries and online retailers around the world.
</p>
<p class="mb-0">
In theory, it is the bridge between independent authors and the traditional retail ecosystem.
In practice, it introduces a few extra layers of complexity... pricing models, wholesale
discounts, returns policies, and a print setup that needs a little more care than simply
clicking “publish”.
</p>
</header>
<div class="row g-4">
<div class="col-lg-8">
<section id="what-is-ingramspark" class="mb-5">
<h2 class="h3 fw-bold mb-3">What is IngramSpark?</h2>
<p>
IngramSpark is a publishing platform operated by <strong>Ingram</strong>, one of the
largest book distributors in the world. It allows publishers and independent authors
to upload print book files which can then be printed on demand and distributed through
the global Ingram distribution network.
</p>
<p>
That network includes:
</p>
<ul>
<li>bookshops</li>
<li>libraries</li>
<li>online retailers</li>
<li>wholesale distributors</li>
</ul>
<p class="mb-0">
In other words, it is designed to make your book visible beyond Amazons own ecosystem.
</p>
</section>
<section id="why-authors-use-ingram" class="mb-5">
<h2 class="h3 fw-bold mb-3">Why indie authors use IngramSpark</h2>
<p>
Many indie authors use IngramSpark alongside KDP rather than instead of it. Each
platform does different things well.
</p>
<p>
The main reasons authors choose IngramSpark include:
</p>
<ul>
<li>wider print distribution</li>
<li>access to bookshops and libraries</li>
<li>hardback publishing options</li>
<li>global print-on-demand network</li>
<li>professional publishing metadata systems</li>
</ul>
<p class="mb-0">
For authors building a long-term catalogue, it can be an important piece of the
distribution puzzle.
</p>
</section>
<section id="file-preparation" class="mb-5">
<h2 class="h3 fw-bold mb-3">Preparing your files</h2>
<p>
IngramSpark expects properly prepared print files. That usually means a
professionally formatted interior PDF and a correctly sized cover PDF including
spine width and bleed areas.
</p>
<p>
Typical requirements include:
</p>
<ul>
<li>correct trim size</li>
<li>proper margins and gutter</li>
<li>embedded fonts</li>
<li>high resolution images</li>
<li>cover sized precisely for page count</li>
</ul>
<p class="mb-0">
It is not complicated once you understand it, but it does reward careful preparation.
</p>
</section>
<section id="proof-copies" class="mb-5">
<h2 class="h3 fw-bold mb-3">Proof copies and approvals</h2>
<p>
Before the book goes live, you normally approve a proof copy. This allows you to
check the printed version for layout errors, cover alignment, colour problems,
and other surprises that only appear once ink hits paper.
</p>
<p>
Things worth checking carefully include:
</p>
<ul>
<li>spine alignment</li>
<li>cover colour balance</li>
<li>page margins</li>
<li>image quality</li>
<li>typos that somehow survived editing</li>
</ul>
<p class="mb-0">
Proofing properly can save a lot of embarrassment later.
</p>
</section>
<section id="pricing-and-wholesale" class="mb-5">
<h2 class="h3 fw-bold mb-3">Pricing and wholesale discounts</h2>
<p>
This is where many new authors receive their first gentle introduction to the economics
of book publishing.
</p>
<p>
When publishing through IngramSpark you set:
</p>
<ul>
<li>the retail price</li>
<li>the wholesale discount offered to retailers</li>
</ul>
<p>
Bookshops expect a discount so they can make their margin. That discount typically
sits somewhere around the 4055% range.
</p>
<p class="mb-0">
Once printing cost and discount are accounted for, the authors share can be
surprisingly modest.
</p>
</section>
<section id="returns" class="mb-5">
<h2 class="h3 fw-bold mb-3">The reality of book returns</h2>
<p>
Traditional book distribution includes the concept of <strong>returns</strong>.
Retailers can send unsold books back to the distributor and receive a refund.
</p>
<p>
With IngramSpark you can choose whether to allow returns, but enabling them carries
financial risk. Returned books can be destroyed or shipped back to you, and the
cost ultimately falls on the publisher.
</p>
<p class="mb-0">
It is one of those slightly uncomfortable truths of the publishing industry that
many cheerful “how to publish your book” guides prefer not to dwell on.
</p>
</section>
<section id="distribution-network" class="mb-5">
<h2 class="h3 fw-bold mb-3">Distribution through the Ingram network</h2>
<p>
Once your book is active in the Ingram system, it becomes visible to thousands
of retailers through the distribution catalogue used by the book trade.
</p>
<p>
That does not mean bookshops will automatically stock it. It simply means they
can order it through the same supply chain they already use.
</p>
<p class="mb-0">
Discoverability still depends heavily on marketing, reputation and demand.
</p>
</section>
<section id="what-ingram-gets-right" class="mb-5">
<h2 class="h3 fw-bold mb-3">What IngramSpark gets right</h2>
<div class="card border-0 bg-light rounded-4">
<div class="card-body p-4">
<ul class="mb-0">
<li>global distribution network</li>
<li>professional publishing infrastructure</li>
<li>strong print quality</li>
<li>hardback publishing options</li>
<li>integration with the wider book trade</li>
</ul>
</div>
</div>
</section>
<section id="what-ingram-gets-wrong" class="mb-5">
<h2 class="h3 fw-bold mb-3">What IngramSpark gets wrong</h2>
<div class="card border-0 bg-light rounded-4">
<div class="card-body p-4">
<ul class="mb-0">
<li>setup is more complex than KDP</li>
<li>pricing maths can be confusing for new authors</li>
<li>returns policies carry risk</li>
<li>support can sometimes feel distant</li>
<li>distribution does not guarantee bookshop placement</li>
</ul>
</div>
</div>
</section>
<section id="my-view-on-ingram" class="mb-5">
<h2 class="h3 fw-bold mb-3">My view on IngramSpark</h2>
<p>
IngramSpark is not the easiest publishing platform to learn, but it plays an
important role if you want your book available through the wider retail
ecosystem.
</p>
<p>
For many indie authors the most practical approach is to use KDP for Amazon
visibility while using IngramSpark for wider distribution and hardback
editions.
</p>
<p class="mb-0">
Like most parts of self-publishing, it rewards patience and careful setup
rather than rushing through the forms and hoping the universe sorts it out.
</p>
</section>
</div>
<div class="col-lg-4">
<div class="sticky-lg-top" style="top:2rem">
<div class="card border-0 shadow-sm rounded-4 mb-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">On this page</h2>
<ul class="list-unstyled small mb-0">
<li class="mb-2"><a href="#what-is-ingramspark">What is IngramSpark?</a></li>
<li class="mb-2"><a href="#why-authors-use-ingram">Why authors use it</a></li>
<li class="mb-2"><a href="#file-preparation">Preparing files</a></li>
<li class="mb-2"><a href="#proof-copies">Proof copies</a></li>
<li class="mb-2"><a href="#pricing-and-wholesale">Pricing and wholesale</a></li>
<li class="mb-2"><a href="#returns">Returns</a></li>
<li class="mb-2"><a href="#distribution-network">Distribution</a></li>
<li class="mb-2"><a href="#what-ingram-gets-right">What it gets right</a></li>
<li class="mb-2"><a href="#what-ingram-gets-wrong">What it gets wrong</a></li>
<li><a href="#my-view-on-ingram">My view</a></li>
</ul>
</div>
</div>
<div class="card border-0 bg-light rounded-4 mb-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Quick takeaway</h2>
<p class="small mb-0">
IngramSpark is about distribution beyond Amazon, but it introduces
wholesale pricing, returns, and a few economic realities.
</p>
</div>
</div>
<div class="card border-0 shadow-sm rounded-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Related guides</h2>
<ul class="list-unstyled small mb-0">
<li class="mb-2">
<a asp-controller="IndieAuthor" asp-action="Kdp">Publishing on KDP</a>
</li>
<li class="mb-2">
<a asp-controller="IndieAuthor" asp-action="KdpVsIngramSpark">KDP vs IngramSpark</a>
</li>
<li>
<a asp-controller="IndieAuthor" asp-action="AuthorFinances">Author finances</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="border-top pt-4 mt-5">
<div class="d-flex justify-content-between">
<a asp-controller="IndieAuthor" asp-action="Kdp" class="btn btn-dark">
Previous: Publishing on KDP
</a>
<a asp-controller="IndieAuthor" asp-action="Audiobooks" class="btn btn-dark">
Next: Audiobooks and ACX
</a>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,350 @@
@{
ViewData["Title"] = "ISBNs for Indie Authors";
}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-12 col-xl-10">
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb small">
<li class="breadcrumb-item">
<a asp-controller="IndieAuthor" asp-action="Index">Indie Author Guide</a>
</li>
<li class="breadcrumb-item active" aria-current="page">ISBNs</li>
</ol>
</nav>
<header class="mb-5">
<span class="badge text-bg-dark mb-3">Indie Author Guide</span>
<h1 class="display-5 fw-bold mb-3">ISBNs, what they are and how indie authors use them</h1>
<p class="lead mb-3">
If you are publishing your own book, sooner or later you will come across ISBNs. They sound
mysterious and important, which is because they are... but they are not nearly as complicated as
some publishing guides make out.
</p>
<p class="mb-0">
This page explains what an ISBN actually is, when you need one, how many you may need for a single
title, how registration works, and some of the practical realities indie authors run into when
dealing with print, ebook and audiobook formats.
</p>
</header>
<div class="row g-4">
<div class="col-lg-8">
<section id="what-is-an-isbn" class="mb-5">
<h2 class="h3 fw-bold mb-3">What is an ISBN?</h2>
<p>
ISBN stands for <strong>International Standard Book Number</strong>. In simple terms, it is a
unique number used to identify a specific edition and format of a book.
</p>
<p>
That last part matters. An ISBN is not just attached to the story itself. It is attached to
a particular <strong>version</strong> of that book. So if you publish the same title in
paperback, hardback and ebook, those are generally treated as separate products.
</p>
<p class="mb-0">
Think of it less like the identity of the novel and more like the identity of a specific
published format of that novel.
</p>
</section>
<section id="when-do-you-need-an-isbn" class="mb-5">
<h2 class="h3 fw-bold mb-3">When do you need an ISBN?</h2>
<p>
In practice, you usually need an ISBN for printed editions such as paperbacks and hardbacks.
Whether you need one for an ebook depends on the platform you are using and how much control
you want over your publishing setup.
</p>
<div class="table-responsive">
<table class="table table-bordered align-middle">
<thead class="table-light">
<tr>
<th>Format</th>
<th>Usually needs its own ISBN?</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td>Paperback</td>
<td>Yes</td>
<td>Normally requires its own ISBN.</td>
</tr>
<tr>
<td>Hardback</td>
<td>Yes</td>
<td>Separate from the paperback edition.</td>
</tr>
<tr>
<td>Kindle ebook</td>
<td>Not always</td>
<td>Amazon can list Kindle books without you supplying your own ISBN.</td>
</tr>
<tr>
<td>EPUB on other platforms</td>
<td>Often yes</td>
<td>Depends on distributor and how widely you are publishing.</td>
</tr>
<tr>
<td>Audiobook</td>
<td>Sometimes</td>
<td>Depends on the platform and distribution route.</td>
</tr>
</tbody>
</table>
</div>
<p class="mb-0">
The key thing is this: if you are releasing your book in more than one format, do not assume
one ISBN covers the lot. It usually does not.
</p>
</section>
<section id="how-many-isbns" class="mb-5">
<h2 class="h3 fw-bold mb-3">How many ISBNs might one book need?</h2>
<p>
This catches a lot of new authors out. One title can easily use multiple ISBNs.
</p>
<p>
For example, a single novel might need:
</p>
<ul>
<li>one ISBN for the paperback</li>
<li>one ISBN for the hardback</li>
<li>one ISBN for a wide-distribution ebook edition</li>
</ul>
<p class="mb-0">
So even if you are only publishing one book, you may want more than one ISBN available from
the start. Buy too few and you can end up going back for more sooner than expected, which is
mildly annoying at best and a complete faff at worst.
</p>
</section>
<section id="buying-isbns" class="mb-5">
<h2 class="h3 fw-bold mb-3">Buying ISBNs</h2>
<p>
In the UK, authors normally buy ISBNs from the official national ISBN agency. Other
countries have their own agencies and systems.
</p>
<p>
What matters most is buying them from the correct official source for your country, rather
than from some random middleman trying to look useful while charging extra for the privilege.
</p>
<p>
If you are planning more than one format, or more than one book, it often makes sense to buy
a block rather than a single ISBN. That gives you flexibility later and avoids having to redo
the whole mental calculation every time you add a new edition.
</p>
<div class="table-responsive">
<table class="table table-bordered align-middle">
<thead class="table-light">
<tr>
<th>1 Single ISBN</th>
<th>Prefix for 10 ISBNs</th>
<th>Prefix for 100 ISBNs</th>
</tr>
</thead>
<tbody>
<tr>
<td>£93 per ISBN</td>
<td>£174 per block of 10</td>
<td>£387 per block of 100</td>
</tr>
</tbody>
</table>
</div>
<p class="mb-0">
The prices above were correct at the time of publication, however you
can find current pricing and order your ISBNs on the <a href="https://www.nielsenisbnstore.com/Home/Isbn" target="_blank">NielsenIQ BookData website</a>.
</p>
</section>
<section id="registering-isbns" class="mb-5">
<h2 class="h3 fw-bold mb-3">Registering each ISBN properly</h2>
<p>
Buying an ISBN is only part of the job. You also need to register it correctly against the
right book format and metadata.
</p>
<p>
Although you can buy the ISBNs within seconds, it takes quite a lot longer to be able to
actually register them. To do this you have to wait for an account to be set up on the
<a href="https://www.nielsentitleeditor.com/titleeditor/" target="_blank">NelisenIQ Title Editor</a>.
Now it appears that this account creation is performed manually, and takes approximately
two weeks to come through, so you will need to keep this in mind. That said you do not need to do this
part until you have your book files ready for submission to your chosen platforms.
</p>
<p>
That normally includes details such as:
</p>
<ul>
<li>title and subtitle</li>
<li>author or pen name</li>
<li>format and binding type</li>
<li>publication date</li>
<li>price</li>
<li>publisher name or imprint</li>
<li>the page count for the book</li>
<li>the number of images, if any, and what type</li>
<li>the physical dimensions of the book</li>
</ul>
<p>
This is worth doing carefully. Metadata errors can create confusion later, especially once
books begin appearing across retailers, databases and library systems.
</p>
<p class="mb-0">
In other words, this is not the glamorous part of publishing, but it is absolutely one of
the bits that benefits from getting it right first time. Don't worry if you do make a mistake though,
you can edit this data once it's live on the system. obviously it's better to get it right first time.
</p>
</section>
<section id="amazon-kdp-isbns" class="mb-5">
<h2 class="h3 fw-bold mb-3">Do Amazon and KDP give you an ISBN?</h2>
<p>
For some formats, Amazon can provide an identifier or allow publication without you using
your own purchased ISBN. That can be convenient, especially when starting out as it's a free service.
</p>
<p>
The trade-off is control. Using platform-provided identifiers can tie that edition more
closely to the platform that issued it. If you want a cleaner, more independent publishing
setup, owning your own ISBNs is often the better route.
</p>
<p class="mb-0">
Convenience is nice. Control is nicer. It just depends how seriously you are treating the
book and how far you plan to take it.
</p>
</section>
<section id="common-mistakes" class="mb-5">
<h2 class="h3 fw-bold mb-3">Common mistakes to avoid</h2>
<div class="card border-0 bg-light rounded-4">
<div class="card-body p-4">
<ul class="mb-0">
<li>Assuming one ISBN covers every format</li>
<li>Buying too few and then needing more almost immediately</li>
<li>Registering the wrong format against the wrong number</li>
<li>Forgetting to keep a clear record of which ISBN belongs to which edition</li>
<li>Treating metadata as an afterthought</li>
</ul>
</div>
</div>
</section>
<section class="mb-5">
<h2 class="h3 fw-bold mb-3">My view as an indie author</h2>
<p>
ISBNs are one of those things that seem intimidating before you start, then turn out to be
mostly admin once you understand the rules. They are not exciting, but they do matter.
</p>
<p>
I chose to purchase a block of 10 ISBNs. You're almost certainly going to need more than one,
however in heindsight I wish I'd bought a block of 100. If you're going to write more than 2 or 3
books then it's the cheapest option over time.
</p>
<p>
For my first book I used up 4 ISBNs:
</p>
<ul>
<li>
1 for the eBook. Initially I thought I would use multiple distribution platforms, so it
made sense. However after a few months I settled on KDP and Kindle Select which gives you
access to Kindle Unlimited
</li>
<li>
1 for the Amazon KDP paperback. For my second book I'm not doing this due to the way Amazon
groups editions of my book
</li>
<li>
1 for my IngramSpark paperback, or what I call Bookshop Edition
</li>
<li>
1 for my IngramSpark hardback, or what I call my Collector's Edition
</li>
<li>
I didn't use one for the audiobook. I found out i didn't need to so I didn't
</li>
</ul>
<p>
For indie authors, the real question is less “what is an ISBN?” and more “how many formats
am I realistically going to publish, and how much control do I want over them?”
</p>
<p class="mb-0">
Answer that properly and the ISBN decision becomes much simpler.
</p>
</section>
</div>
<div class="col-lg-4">
<div class="sticky-lg-top" style="top: 2rem;">
<div class="card border-0 shadow-sm rounded-4 mb-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">On this page</h2>
<ul class="list-unstyled small mb-0">
<li class="mb-2"><a href="#what-is-an-isbn" class="text-decoration-none">What is an ISBN?</a></li>
<li class="mb-2"><a href="#when-do-you-need-an-isbn" class="text-decoration-none">When do you need one?</a></li>
<li class="mb-2"><a href="#how-many-isbns" class="text-decoration-none">How many might you need?</a></li>
<li class="mb-2"><a href="#buying-isbns" class="text-decoration-none">Buying ISBNs</a></li>
<li class="mb-2"><a href="#registering-isbns" class="text-decoration-none">Registering each one</a></li>
<li class="mb-2"><a href="#amazon-kdp-isbns" class="text-decoration-none">Amazon and KDP</a></li>
<li><a href="#common-mistakes" class="text-decoration-none">Common mistakes</a></li>
</ul>
</div>
</div>
<div class="card border-0 bg-light rounded-4 mb-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Quick takeaway</h2>
<p class="small mb-0">
One format usually means one ISBN. Paperback, hardback and ebook editions often
need to be treated separately.
</p>
</div>
</div>
<div class="card border-0 bg-white shadow-sm rounded-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Related guides</h2>
<ul class="list-unstyled small mb-0">
<li class="mb-2">
<a asp-controller="IndieAuthor" asp-action="Kdp" class="text-decoration-none">
Publishing on KDP
</a>
</li>
<li class="mb-2">
<a asp-controller="IndieAuthor" asp-action="IngramSpark" class="text-decoration-none">
Publishing on IngramSpark
</a>
</li>
<li>
<a asp-controller="IndieAuthor" asp-action="KdpVsIngramSpark" class="text-decoration-none">
KDP vs IngramSpark
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="border-top pt-4 mt-5">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
<a asp-controller="IndieAuthor" asp-action="TraditionalVsSelfPublishing" class="btn btn-dark">
Previous: Traditional vs Self-Publishing
</a>
<a asp-controller="IndieAuthor" asp-action="EditingAndProofReading" class="btn btn-dark">
Next: Editing and Proofreading
</a>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,383 @@
@{
ViewData["Title"] = "Publishing on Amazon KDP";
}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-12 col-xl-10">
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb small">
<li class="breadcrumb-item">
<a asp-controller="IndieAuthor" asp-action="Index">Indie Author Guide</a>
</li>
<li class="breadcrumb-item active" aria-current="page">Publishing on KDP</li>
</ol>
</nav>
<header class="mb-5">
<span class="badge text-bg-dark mb-3">Indie Author Guide</span>
<h1 class="display-5 fw-bold mb-3">Publishing on Amazon KDP</h1>
<p class="lead mb-3">
For many indie authors, Amazon KDP is the first real step into publishing. It is accessible,
relatively quick to use, and gives authors a direct route to publishing ebooks and printed books
without begging for permission from gatekeepers who think every novel on earth ought to be 80,000
words and neatly shoved into one marketable box.
</p>
<p class="mb-0">
This page looks at what KDP is, how it works, what you can publish through it, the pros and cons
of Kindle Unlimited, and some of the practical realities of dealing with setup screens, royalties,
categories, keywords, and all the little details that make a difference.
</p>
</header>
<div class="row g-4">
<div class="col-lg-8">
<section id="what-is-kdp" class="mb-5">
<h2 class="h3 fw-bold mb-3">What is KDP?</h2>
<p>
<strong>KDP</strong>, or <strong>Kindle Direct Publishing</strong>, is Amazons self-publishing
platform. It allows authors and publishers to upload books directly to Amazon and sell them
without going through a traditional publishing house.
</p>
<p>
For indie authors, it is one of the easiest ways to get a book in front of readers. You can
publish ebooks for Kindle, and you can also publish print editions such as paperbacks and,
in some cases, hardbacks.
</p>
<p class="mb-0">
The big attraction is speed and control. You make the decisions, you upload the files, you
set the pricing, and you retain far more ownership over the process than you ever would in
traditional publishing.
</p>
</section>
<section id="why-authors-use-kdp" class="mb-5">
<h2 class="h3 fw-bold mb-3">Why authors use KDP</h2>
<p>
KDP is popular because it removes a lot of barriers. You do not need an agent, you do not
need a publishing deal, and you do not need somebody in London to decide whether your book
is currently fashionable enough for them to care.
</p>
<p>
Some of the main reasons indie authors use KDP include:
</p>
<ul>
<li>easy access to the Amazon marketplace</li>
<li>fast setup compared with traditional publishing</li>
<li>control over pricing and metadata</li>
<li>direct publishing of Kindle ebooks</li>
<li>the option to create paperback editions</li>
<li>built-in connection to Amazon sales pages and reviews</li>
</ul>
<p class="mb-0">
It is not perfect, but it is often the most obvious place to begin, especially if your goal
is simply to get the book live and available to buy.
</p>
</section>
<section id="what-you-can-publish" class="mb-5">
<h2 class="h3 fw-bold mb-3">What you can publish through KDP</h2>
<p>
KDP is most strongly associated with Kindle ebooks, but it can also handle certain print
formats. That makes it useful for authors who want to keep ebook and print publishing under
one roof, at least to begin with.
</p>
<div class="table-responsive">
<table class="table table-bordered align-middle">
<thead class="table-light">
<tr>
<th>Format</th>
<th>Supported by KDP?</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td>Kindle ebook</td>
<td>Yes</td>
<td>The core KDP format and usually the easiest to publish.</td>
</tr>
<tr>
<td>Paperback</td>
<td>Yes</td>
<td>Print-on-demand through Amazon.</td>
</tr>
<tr>
<td>Hardback</td>
<td>Sometimes</td>
<td>Available in some setups, but many indie authors still prefer IngramSpark for hardbacks.</td>
</tr>
<tr>
<td>Audiobook</td>
<td>No</td>
<td>Audiobooks are handled separately, often through ACX rather than KDP itself.</td>
</tr>
</tbody>
</table>
</div>
<p class="mb-0">
In practice, many indie authors use KDP for ebooks and Amazon print visibility, then use
other services for wider print distribution or audio.
</p>
</section>
<section id="setting-up-a-book" class="mb-5">
<h2 class="h3 fw-bold mb-3">Setting up a book on KDP</h2>
<p>
KDP walks you through the setup in stages. On the surface, it all looks fairly simple:
title, description, keywords, categories, manuscript upload, cover upload, price, done.
In reality, each of those boxes has the power to help or quietly sabotage your book.
</p>
<p>
A typical KDP setup includes:
</p>
<ul>
<li>book title and subtitle</li>
<li>series information, if relevant</li>
<li>author name or pen name</li>
<li>book description</li>
<li>publishing rights confirmation</li>
<li>keywords and categories</li>
<li>age or audience information where needed</li>
<li>manuscript upload</li>
<li>cover upload or use of Amazons cover tools</li>
<li>pricing and territory settings</li>
</ul>
<p class="mb-0">
None of this is especially difficult, but it does reward care. Sloppy metadata and rushed
presentation make a book look amateur before anyone has even read page one.
</p>
</section>
<section id="kindle-ebooks" class="mb-5">
<h2 class="h3 fw-bold mb-3">Kindle ebooks</h2>
<p>
For many authors, the ebook edition is the easiest place to start. It does not involve print
costs, there is no physical proof copy to approve, and updating the file is generally less
painful than with print.
</p>
<p>
Kindle also gives you access to Amazons enormous customer base, which is obviously the bit
everyone likes. The less glamorous bit is that it also means competing with approximately a
million other books and a small mountain of algorithm-driven nonsense.
</p>
<p class="mb-0">
Still, ebooks are often the lowest-friction way to get a book selling online, and for many
indie authors they remain the main entry point into the market.
</p>
</section>
<section id="kindle-unlimited" class="mb-5">
<h2 class="h3 fw-bold mb-3">Kindle Unlimited and KDP Select</h2>
<p>
KDP Select is Amazons optional exclusivity programme for ebooks. If you enrol your Kindle
book in it, that ebook generally has to remain exclusive to Amazon for the enrolment period.
In return, the book can be included in <strong>Kindle Unlimited</strong>.
</p>
<p>
Kindle Unlimited allows subscribers to read books from the programme as part of their
membership. Authors are then paid based on pages read rather than only on direct purchases.
</p>
<p>
That sounds lovely in theory. In practice, it depends heavily on the type of book, your
audience, your pricing strategy, and whether being exclusive to Amazon suits your wider
publishing plans.
</p>
<div class="card border-0 bg-light rounded-4 mt-4">
<div class="card-body p-4">
<h3 class="h5 fw-bold mb-3">Things to think about with Kindle Unlimited</h3>
<ul class="mb-0">
<li>You gain access to Kindle Unlimited readers</li>
<li>You are usually giving Amazon ebook exclusivity for that period</li>
<li>You may earn from page reads rather than only book sales</li>
<li>It can help visibility for some genres more than others</li>
<li>It may not suit authors who want their ebooks wide on multiple stores</li>
</ul>
</div>
</div>
</section>
<section id="paperbacks-and-print" class="mb-5">
<h2 class="h3 fw-bold mb-3">Paperbacks and print-on-demand</h2>
<p>
KDP can also handle paperback publishing through print-on-demand. That means copies are
printed when ordered, rather than you having to buy and store a garage full of books like a
literary greengrocer.
</p>
<p>
This is incredibly useful for indie authors because it lowers the financial risk. You do not
need a giant print run upfront. You simply upload the interior and cover files, choose the
relevant trim and print settings, then approve the proofing stage.
</p>
<p class="mb-0">
Print-on-demand is one of the reasons self-publishing is even viable for so many authors now.
The downside is that print cost and retail margin can squeeze profit pretty hard, especially
on long books.
</p>
</section>
<section id="keywords-categories-and-metadata" class="mb-5">
<h2 class="h3 fw-bold mb-3">Keywords, categories and metadata</h2>
<p>
This is one of the least exciting but most important parts of KDP. Your book description,
categories, keywords, subtitle, and general metadata all affect how discoverable the book is
and how accurately Amazon understands what it is.
</p>
<p>
If you pick lazy categories or vague keywords, you make life harder for yourself. The same
goes for weak descriptions. You can write a brilliant novel and still trip over your own
shoelaces at the metadata stage.
</p>
<p class="mb-0">
Good metadata will not magically sell a bad book, but bad metadata can absolutely bury a
good one.
</p>
</section>
<section id="royalties-and-pricing" class="mb-5">
<h2 class="h3 fw-bold mb-3">Royalties and pricing</h2>
<p>
One of the first things new authors notice on KDP is that pricing is tied closely to royalty
structures. In ebooks especially, price can affect which royalty option applies. Print books
then add printing cost on top, which can make the maths much less glamorous than people hope.
</p>
<p>
The important thing is not to price purely based on ego. Your book may well deserve to be
treated like a masterpiece, but readers still compare it with everything else on the store.
</p>
<p class="mb-0">
Pricing is part positioning, part experiment, and part cold acceptance that profit per book
is often thinner than outsiders imagine.
</p>
</section>
<section id="what-kdp-gets-right" class="mb-5">
<h2 class="h3 fw-bold mb-3">What KDP gets right</h2>
<div class="card border-0 bg-light rounded-4">
<div class="card-body p-4">
<ul class="mb-0">
<li>It is easy to access and reasonably straightforward to use</li>
<li>It gets books onto Amazon quickly</li>
<li>It supports both ebook and print workflows</li>
<li>It gives indie authors direct control over publishing decisions</li>
<li>It lowers the barrier to entry dramatically</li>
</ul>
</div>
</div>
</section>
<section id="what-kdp-gets-wrong" class="mb-5">
<h2 class="h3 fw-bold mb-3">What KDP gets wrong</h2>
<div class="card border-0 bg-light rounded-4">
<div class="card-body p-4">
<ul class="mb-0">
<li>It encourages dependence on a single giant retailer</li>
<li>Its systems can feel rigid or opaque in places</li>
<li>Metadata and category control is not always as flexible as authors would like</li>
<li>Kindle Unlimited exclusivity can be limiting</li>
<li>It can make publishing look easier than marketing actually is</li>
</ul>
</div>
</div>
</section>
<section id="my-view-on-kdp" class="mb-5">
<h2 class="h3 fw-bold mb-3">My view on KDP as an indie author</h2>
<p>
KDP is a very useful tool. It is not a magic wand, not a publishing strategy by itself, and
definitely not a guarantee of sales. But it is one of the most practical ways for an indie
author to get a book into the world.
</p>
<p>
The real danger is assuming that uploading the book is the finish line. It is not. It is the
start of the next phase, which involves presentation, discoverability, reviews, pricing,
advertising, and slowly building a catalogue that gives readers somewhere else to go if they
like your work.
</p>
<p class="mb-0">
In other words, KDP can publish your book. It cannot build your career for you. Bit rude of
it really.
</p>
</section>
</div>
<div class="col-lg-4">
<div class="sticky-lg-top" style="top: 2rem;">
<div class="card border-0 shadow-sm rounded-4 mb-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">On this page</h2>
<ul class="list-unstyled small mb-0">
<li class="mb-2"><a href="#what-is-kdp" class="text-decoration-none">What is KDP?</a></li>
<li class="mb-2"><a href="#why-authors-use-kdp" class="text-decoration-none">Why authors use it</a></li>
<li class="mb-2"><a href="#what-you-can-publish" class="text-decoration-none">What you can publish</a></li>
<li class="mb-2"><a href="#setting-up-a-book" class="text-decoration-none">Setting up a book</a></li>
<li class="mb-2"><a href="#kindle-ebooks" class="text-decoration-none">Kindle ebooks</a></li>
<li class="mb-2"><a href="#kindle-unlimited" class="text-decoration-none">Kindle Unlimited</a></li>
<li class="mb-2"><a href="#paperbacks-and-print" class="text-decoration-none">Paperbacks and print</a></li>
<li class="mb-2"><a href="#keywords-categories-and-metadata" class="text-decoration-none">Metadata</a></li>
<li class="mb-2"><a href="#royalties-and-pricing" class="text-decoration-none">Royalties and pricing</a></li>
<li class="mb-2"><a href="#what-kdp-gets-right" class="text-decoration-none">What KDP gets right</a></li>
<li class="mb-2"><a href="#what-kdp-gets-wrong" class="text-decoration-none">What KDP gets wrong</a></li>
<li><a href="#my-view-on-kdp" class="text-decoration-none">My view</a></li>
</ul>
</div>
</div>
<div class="card border-0 bg-light rounded-4 mb-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Quick takeaway</h2>
<p class="small mb-0">
KDP is one of the easiest ways for indie authors to publish ebooks and print books,
but getting listed is only the beginning. Visibility is the real battle.
</p>
</div>
</div>
<div class="card border-0 bg-white shadow-sm rounded-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Related guides</h2>
<ul class="list-unstyled small mb-0">
<li class="mb-2">
<a asp-controller="IndieAuthor" asp-action="Isbns" class="text-decoration-none">
ISBNs
</a>
</li>
<li class="mb-2">
<a asp-controller="IndieAuthor" asp-action="IngramSpark" class="text-decoration-none">
Publishing on IngramSpark
</a>
</li>
<li>
<a asp-controller="IndieAuthor" asp-action="KdpVsIngramSpark" class="text-decoration-none">
KDP vs IngramSpark
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="border-top pt-4 mt-5">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
<a asp-controller="IndieAuthor" asp-action="KdpVsIngramSpark" class="btn btn-dark">
Previous: KDP vs IngramSpark
</a>
<a asp-controller="IndieAuthor" asp-action="IngramSpark" class="btn btn-dark">
Next: Publishing on IngramSpark
</a>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,377 @@
@{
ViewData["Title"] = "KDP vs IngramSpark";
}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-12 col-xl-10">
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb small">
<li class="breadcrumb-item">
<a asp-controller="IndieAuthor" asp-action="Index">Indie Author Guide</a>
</li>
<li class="breadcrumb-item active">KDP vs IngramSpark</li>
</ol>
</nav>
<header class="mb-5">
<span class="badge text-bg-dark mb-3">Indie Author Guide</span>
<h1 class="display-5 fw-bold mb-3">KDP vs IngramSpark</h1>
<p class="lead mb-3">
One of the most common questions new indie authors ask is whether they should publish their print
books through Amazon KDP or through IngramSpark.
</p>
<p class="mb-0">
The honest answer is that they serve slightly different purposes. Many independent authors
eventually end up using both platforms together, each handling the parts of publishing they
do best.
</p>
</header>
<div class="row g-4">
<div class="col-lg-8">
<section id="the-short-answer" class="mb-5">
<h2 class="h3 fw-bold mb-3">The short answer</h2>
<p>
If your primary goal is selling books on Amazon, KDP is usually the easiest and most
straightforward route.
</p>
<p>
If you want your book available through bookshops, libraries and the wider book trade,
IngramSpark provides access to the global distribution network used by retailers.
</p>
<p class="mb-0">
For many authors the practical solution is to combine the two.
</p>
</section>
<section id="side-by-side-comparison" class="mb-5">
<h2 class="h3 fw-bold mb-3">Side-by-side comparison</h2>
<div class="table-responsive">
<table class="table table-bordered align-middle">
<thead class="table-light">
<tr>
<th>Feature</th>
<th>KDP</th>
<th>IngramSpark</th>
</tr>
</thead>
<tbody>
<tr>
<td>Primary purpose</td>
<td>Amazon publishing platform</td>
<td>Global book distribution network</td>
</tr>
<tr>
<td>Ebooks</td>
<td>Excellent Kindle support</td>
<td>Not its main focus</td>
</tr>
<tr>
<td>Paperbacks</td>
<td>Simple print-on-demand</td>
<td>Professional trade distribution</td>
</tr>
<tr>
<td>Hardbacks</td>
<td>Available in some cases</td>
<td>Strong support for hardback formats</td>
</tr>
<tr>
<td>Distribution</td>
<td>Primarily Amazon</td>
<td>Bookshops, libraries, retailers worldwide</td>
</tr>
<tr>
<td>Setup complexity</td>
<td>Very straightforward</td>
<td>More detailed setup</td>
</tr>
<tr>
<td>Wholesale discount</td>
<td>Not required</td>
<td>Required for trade distribution</td>
</tr>
<tr>
<td>Returns system</td>
<td>No traditional return system</td>
<td>Retail returns possible</td>
</tr>
<tr>
<td>Best for</td>
<td>Amazon-focused publishing</td>
<td>Trade distribution and hardbacks</td>
</tr>
</tbody>
</table>
</div>
</section>
<section id="when-kdp-makes-sense" class="mb-5">
<h2 class="h3 fw-bold mb-3">When KDP makes the most sense</h2>
<p>
For many indie authors, KDP is the simplest way to get started.
</p>
<ul>
<li>you want fast publishing</li>
<li>Amazon is your main sales channel</li>
<li>you want simple setup</li>
<li>you want Kindle ebook support</li>
<li>you prefer minimal distribution complexity</li>
</ul>
<p class="mb-0">
It removes a lot of friction from the publishing process and allows authors to focus on writing,
presentation and marketing.
</p>
</section>
<section id="when-ingram-makes-sense" class="mb-5">
<h2 class="h3 fw-bold mb-3">When IngramSpark makes sense</h2>
<p>
IngramSpark becomes valuable when authors want access to the wider book trade.
</p>
<ul>
<li>bookshop availability</li>
<li>library distribution</li>
<li>global retail catalogues</li>
<li>hardback editions</li>
<li>wider publishing infrastructure</li>
</ul>
<p class="mb-0">
It is particularly useful for authors building a catalogue or publishing imprint rather than
simply uploading a single book.
</p>
</section>
<section id="using-both-platforms" class="mb-5">
<h2 class="h3 fw-bold mb-3">Using both platforms together</h2>
<p>
A common strategy among indie authors is to combine both services.
</p>
<p>
Typical setups look something like this:
</p>
<ul>
<li>KDP for Kindle ebooks</li>
<li>KDP for Amazon print visibility</li>
<li>IngramSpark for wider distribution</li>
<li>IngramSpark for hardback editions</li>
</ul>
<p class="mb-0">
Each platform then handles the parts it is best at rather than forcing one system to do everything.
</p>
</section>
<section id="the-bookshop-myth" class="mb-5">
<h2 class="h3 fw-bold mb-3">The bookshop myth</h2>
<p>
Many authors assume that once a book is listed in the Ingram distribution network, bookshops
will automatically stock it.
</p>
<p>
Unfortunately that is not how it works.
</p>
<p>
Bookshops stock books they believe will sell. Being available through Ingram simply means the
book can be ordered through their normal supply chain. It does not guarantee shelf space.
</p>
<p class="mb-0">
Distribution creates opportunity. It does not create demand.
</p>
</section>
<section id="the-financial-reality" class="mb-5">
<h2 class="h3 fw-bold mb-3">The financial reality</h2>
<p>
Both platforms involve economic trade-offs.
</p>
<ul>
<li>KDP print margins are often modest</li>
<li>IngramSpark requires wholesale discounts</li>
<li>print cost increases with book length</li>
<li>returns policies can introduce risk</li>
</ul>
<p class="mb-0">
Understanding the numbers is an important part of publishing decisions.
</p>
</section>
<section id="my-view" class="mb-5">
<h2 class="h3 fw-bold mb-3">My view as an indie author</h2>
<p>
KDP and IngramSpark are not really competing services. They are different tools
within the same publishing toolbox.
</p>
<p>
KDP is excellent for fast publishing and Amazon visibility.
</p>
<p>
IngramSpark is valuable for broader distribution and professional publishing
infrastructure.
</p>
<p class="mb-0">
Using both together often provides the most flexibility for indie authors building
a long-term catalogue.
</p>
</section>
</div>
<div class="col-lg-4">
<div class="sticky-lg-top" style="top:2rem">
<div class="card border-0 shadow-sm rounded-4 mb-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">On this page</h2>
<ul class="list-unstyled small mb-0">
<li class="mb-2"><a href="#the-short-answer">The short answer</a></li>
<li class="mb-2"><a href="#side-by-side-comparison">Comparison table</a></li>
<li class="mb-2"><a href="#when-kdp-makes-sense">When KDP makes sense</a></li>
<li class="mb-2"><a href="#when-ingram-makes-sense">When IngramSpark makes sense</a></li>
<li class="mb-2"><a href="#using-both-platforms">Using both platforms</a></li>
<li class="mb-2"><a href="#the-bookshop-myth">The bookshop myth</a></li>
<li class="mb-2"><a href="#the-financial-reality">Financial reality</a></li>
<li><a href="#my-view">My view</a></li>
</ul>
</div>
</div>
<div class="card border-0 bg-light rounded-4 mb-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Quick takeaway</h2>
<p class="small mb-0">
KDP is ideal for Amazon publishing. IngramSpark extends your reach into
the wider book trade. Many authors use both.
</p>
</div>
</div>
<div class="card border-0 shadow-sm rounded-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Related guides</h2>
<ul class="list-unstyled small mb-0">
<li class="mb-2">
<a asp-controller="IndieAuthor" asp-action="Kdp">
Publishing on KDP
</a>
</li>
<li class="mb-2">
<a asp-controller="IndieAuthor" asp-action="IngramSpark">
Publishing on IngramSpark
</a>
</li>
<li>
<a asp-controller="IndieAuthor" asp-action="AuthorFinances">
Author finances
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="border-top pt-4 mt-5">
<div class="d-flex justify-content-between">
<a asp-controller="IndieAuthor" asp-action="CoverDesign" class="btn btn-dark">
Previous: Cover Design
</a>
<a asp-controller="IndieAuthor" asp-action="Kdp" class="btn btn-dark">
Next: Publishing on KDP
</a>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,275 @@
@{
ViewData["Title"] = "Reviews and ARC Readers";
}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-12 col-xl-10">
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb small">
<li class="breadcrumb-item">
<a asp-controller="IndieAuthor" asp-action="Index">Indie Author Guide</a>
</li>
<li class="breadcrumb-item active" aria-current="page">Reviews and ARC Readers</li>
</ol>
</nav>
<header class="mb-5">
<span class="badge text-bg-dark mb-3">Indie Author Guide</span>
<h1 class="display-5 fw-bold mb-3">Reviews and ARC readers</h1>
<p class="lead mb-3">
Reviews are one of the most important pieces of social proof on Amazon and other book retailers.
When a reader lands on your book page, they often glance at the rating and number of reviews
before they even read the blurb.
</p>
<p class="mb-0">
Unfortunately, getting reviews is not easy. Even readers who genuinely enjoyed your book often
forget to leave one. This page looks at why reviews matter, what ARC readers are, and some of
the realities indie authors face when trying to build early feedback for their work.
</p>
</header>
<div class="row g-4">
<div class="col-lg-8">
<section id="why-reviews-matter" class="mb-5">
<h2 class="h3 fw-bold mb-3">Why reviews matter</h2>
<p>
Reviews serve two main purposes. First, they help potential readers decide whether the
book is worth their time and money. Second, they feed into the algorithms that determine
how visible a book becomes on retail platforms like Amazon.
</p>
<p>
A book with no reviews often looks risky to a new reader. A book with a handful of
thoughtful reviews feels safer. Once a book gathers more reviews, it begins to look
established.
</p>
<p class="mb-0">
That perception matters a lot more than authors sometimes realise.
</p>
</section>
<section id="how-readers-use-reviews" class="mb-5">
<h2 class="h3 fw-bold mb-3">How readers actually use reviews</h2>
<p>
Most readers dont analyse reviews in great detail. Instead they tend to scan quickly.
</p>
<ul>
<li>They glance at the star rating</li>
<li>They check roughly how many reviews there are</li>
<li>They skim one or two short comments</li>
<li>They look for signs the book fits their taste</li>
</ul>
<p>
Reviews therefore work as a form of reassurance. Even a short comment like
“I really enjoyed this story” can help signal that the book has already been read
and appreciated by someone else.
</p>
<p class="mb-0">
A completely empty review section, on the other hand, can make a book feel invisible.
</p>
</section>
<section id="what-arc-readers-are" class="mb-5">
<h2 class="h3 fw-bold mb-3">What ARC readers are</h2>
<p>
ARC stands for <strong>Advance Review Copy</strong>. These are early copies of a book
given to readers before publication in the hope they will leave honest reviews once
the book is released.
</p>
<p>
ARC readers might receive:
</p>
<ul>
<li>a digital manuscript</li>
<li>a pre-release ebook</li>
<li>a printed proof copy</li>
</ul>
<p class="mb-0">
The idea is simple. Early readers help seed the review section so the book does not
launch into the world looking completely empty.
</p>
</section>
<section id="finding-arc-readers" class="mb-5">
<h2 class="h3 fw-bold mb-3">Finding ARC readers</h2>
<p>
In theory this sounds straightforward. In practice it can be surprisingly difficult.
</p>
<p>
Many authors try to recruit ARC readers from:
</p>
<ul>
<li>mailing lists</li>
<li>social media followers</li>
<li>reader communities</li>
<li>genre-specific groups</li>
<li>friends and early supporters</li>
</ul>
<p class="mb-0">
Even then, not everyone who accepts an ARC will actually leave a review. Some readers
simply forget, get busy, or never quite finish the book.
</p>
</section>
<section id="managing-expectations" class="mb-5">
<h2 class="h3 fw-bold mb-3">Managing expectations</h2>
<p>
One of the realities indie authors eventually learn is that only a small percentage
of readers leave reviews.
</p>
<p>
Even enthusiastic readers often finish a book, close the app, and move on with their
day. Writing a review takes time and effort, and many readers simply dont think to do it.
</p>
<p class="mb-0">
This means that a modest number of reviews does not necessarily reflect a lack of
readership. It often just reflects normal human behaviour.
</p>
</section>
<section id="honest-reviews" class="mb-5">
<h2 class="h3 fw-bold mb-3">Why honest reviews matter</h2>
<p>
ARC readers should always be encouraged to leave honest reviews rather than positive
ones. The purpose is not to manufacture praise but to give readers genuine feedback
from people who have read the book.
</p>
<p>
Honest reviews build credibility. If every review sounds suspiciously identical or
overly enthusiastic, readers may become sceptical.
</p>
<p class="mb-0">
A mix of reactions is normal and healthy.
</p>
</section>
<section id="amazon-rules" class="mb-5">
<h2 class="h3 fw-bold mb-3">Amazon review rules</h2>
<p>
Amazon has strict policies around reviews. Authors should avoid anything that could
be interpreted as manipulating the review system.
</p>
<p>
Examples of things to avoid include:
</p>
<ul>
<li>paying for positive reviews</li>
<li>review swaps between authors</li>
<li>asking friends or family to leave reviews without disclosure</li>
<li>offering rewards for favourable reviews</li>
</ul>
<p class="mb-0">
The safest approach is simply to ask readers to leave an honest review if they enjoyed
the book.
</p>
</section>
<section id="how-to-encourage-reviews" class="mb-5">
<h2 class="h3 fw-bold mb-3">Encouraging readers to leave reviews</h2>
<p>
While you cannot force readers to leave reviews, you can make it easier for them.
</p>
<ul>
<li>Include a short request at the end of the book</li>
<li>Thank readers for their support</li>
<li>Provide a simple reminder that reviews help authors</li>
<li>Keep the request brief and polite</li>
</ul>
<p class="mb-0">
A gentle reminder is often enough to nudge a few readers into leaving feedback.
</p>
</section>
<section id="my-experience-with-reviews" class="mb-5">
<h2 class="h3 fw-bold mb-3">My experience with reviews</h2>
<p>
In my own experience, gathering reviews can be slower than you expect. Even when readers
say they enjoyed the book, that doesnt always translate into a written review on the
retailer page.
</p>
<p>
That can feel frustrating, but its part of the normal rhythm of publishing. Reviews
tend to accumulate gradually over time rather than appearing all at once.
</p>
<p class="mb-0">
The best approach is to focus on writing the next book while the reviews slowly build
in the background.
</p>
</section>
</div>
<div class="col-lg-4">
<div class="sticky-lg-top" style="top: 2rem;">
<div class="card border-0 shadow-sm rounded-4 mb-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">On this page</h2>
<ul class="list-unstyled small mb-0">
<li class="mb-2"><a href="#why-reviews-matter" class="text-decoration-none">Why reviews matter</a></li>
<li class="mb-2"><a href="#how-readers-use-reviews" class="text-decoration-none">How readers use reviews</a></li>
<li class="mb-2"><a href="#what-arc-readers-are" class="text-decoration-none">What ARC readers are</a></li>
<li class="mb-2"><a href="#finding-arc-readers" class="text-decoration-none">Finding ARC readers</a></li>
<li class="mb-2"><a href="#managing-expectations" class="text-decoration-none">Managing expectations</a></li>
<li class="mb-2"><a href="#honest-reviews" class="text-decoration-none">Honest reviews</a></li>
<li class="mb-2"><a href="#amazon-rules" class="text-decoration-none">Amazon rules</a></li>
<li class="mb-2"><a href="#how-to-encourage-reviews" class="text-decoration-none">Encouraging reviews</a></li>
<li><a href="#my-experience-with-reviews" class="text-decoration-none">My experience</a></li>
</ul>
</div>
</div>
<div class="card border-0 bg-light rounded-4 mb-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Quick takeaway</h2>
<p class="small mb-0">
Reviews build trust with readers and improve visibility, but they usually
accumulate slowly. Even enthusiastic readers often forget to leave them.
</p>
</div>
</div>
<div class="card border-0 bg-white shadow-sm rounded-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Related guides</h2>
<ul class="list-unstyled small mb-0">
<li class="mb-2">
<a asp-controller="IndieAuthor" asp-action="AmazonAdvertising" class="text-decoration-none">
Amazon Advertising
</a>
</li>
<li class="mb-2">
<a asp-controller="IndieAuthor" asp-action="EditingAndProofreading" class="text-decoration-none">
Editing and Proofreading
</a>
</li>
<li>
<a asp-controller="IndieAuthor" asp-action="Index" class="text-decoration-none">
Guide Index
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="border-top pt-4 mt-5">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
<a asp-controller="IndieAuthor" asp-action="Audiobooks" class="btn btn-dark">
Previous: Audiobooks and ACX
</a>
<a asp-controller="IndieAuthor" asp-action="AmazonAdvertising" class="btn btn-dark">
Next: Amazon Advertising
</a>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,310 @@
@{
ViewData["Title"] = "Traditional Publishing vs Self Publishing";
}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-12 col-xl-10">
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb small">
<li class="breadcrumb-item">
<a asp-controller="IndieAuthor" asp-action="Index">Indie Author Guide</a>
</li>
<li class="breadcrumb-item active" aria-current="page">Traditional vs Self Publishing</li>
</ol>
</nav>
<header class="mb-5">
<span class="badge text-bg-dark mb-3">Indie Author Guide</span>
<h1 class="display-5 fw-bold mb-3">Traditional publishing vs self publishing</h1>
<p class="lead mb-3">
For decades traditional publishing was the only realistic route into the book industry.
Authors would submit manuscripts to literary agents, agents would approach publishers,
and if everything aligned the book might eventually appear in print.
</p>
<p class="mb-0">
Today things are very different. Platforms like Amazon KDP and IngramSpark allow authors
to publish their work directly. That doesnt make one path automatically better than the
other. Each route has advantages, disadvantages, and trade-offs that authors need to
understand before deciding which direction suits them.
</p>
</header>
<div class="row g-4">
<div class="col-lg-8">
<section id="traditional-publishing-overview" class="mb-5">
<h2 class="h3 fw-bold mb-3">How traditional publishing works</h2>
<p>
Traditional publishing usually involves several stages before a book ever reaches
readers. The author submits their manuscript to literary agents. If an agent
believes the work has commercial potential they may agree to represent the author.
</p>
<p>
The agent then approaches publishing houses in the hope that one of them will
acquire the book. If accepted, the publisher typically handles editing, cover
design, printing, and distribution.
</p>
<p class="mb-0">
In return, the author receives a royalty on sales and often gives the publisher
certain rights to the work for a period of time.
</p>
</section>
<section id="traditional-pros" class="mb-5">
<h2 class="h3 fw-bold mb-3">Advantages of traditional publishing</h2>
<div class="card border-0 bg-light rounded-4">
<div class="card-body p-4">
<ul class="mb-0">
<li>Professional editorial support</li>
<li>Established distribution networks</li>
<li>Access to physical bookshops</li>
<li>Marketing support from the publisher</li>
<li>Industry recognition and credibility</li>
</ul>
</div>
</div>
<p class="mt-3 mb-0">
For some authors, particularly those seeking large commercial exposure or literary
recognition, these advantages can be significant.
</p>
</section>
<section id="traditional-cons" class="mb-5">
<h2 class="h3 fw-bold mb-3">Challenges with traditional publishing</h2>
<div class="card border-0 bg-light rounded-4">
<div class="card-body p-4">
<ul class="mb-0">
<li>Very competitive submission process</li>
<li>Long timelines before publication</li>
<li>Less control over the final product</li>
<li>Lower royalty percentages</li>
<li>Pressure to fit established genre expectations</li>
</ul>
</div>
</div>
<p class="mt-3 mb-0">
The submission process alone can take months or even years, and many manuscripts
never reach publication through this route. It's not unheard of for it to take two
years or more for your work to get published.
</p>
<p class="mt-3 mb-0">
Additionally don't think that if you do manage to get traditionally published you'll
then get to sit back and enjoy the ride. You'll be expected to do very similar promotional
work that you would do if you were self published.
</p>
</section>
<section id="self-publishing-overview" class="mb-5">
<h2 class="h3 fw-bold mb-3">How self publishing works</h2>
<p>
Self publishing removes the agent and publisher from the process. Instead,
the author takes responsibility for preparing the book and distributing it
through platforms such as Amazon KDP, IngramSpark, or audiobook services
like ACX.
</p>
<p>
The author controls the process from start to finish. That includes editing,
cover design, formatting, pricing, and marketing.
</p>
<p class="mb-0">
While this offers far greater control, it also means the author must learn
skills traditionally handled by publishing professionals.
</p>
</section>
<section id="self-publishing-pros" class="mb-5">
<h2 class="h3 fw-bold mb-3">Advantages of self publishing</h2>
<div class="card border-0 bg-light rounded-4">
<div class="card-body p-4">
<ul class="mb-0">
<li>Full creative control</li>
<li>Faster publishing timelines</li>
<li>Higher royalty percentages</li>
<li>Ability to publish longer or unconventional works</li>
<li>Direct relationship with readers</li>
</ul>
</div>
</div>
<p class="mt-3 mb-0">
For authors who enjoy the creative and technical side of publishing,
self publishing can be extremely empowering.
</p>
</section>
<section id="self-publishing-cons" class="mb-5">
<h2 class="h3 fw-bold mb-3">Challenges of self publishing</h2>
<div class="card border-0 bg-light rounded-4">
<div class="card-body p-4">
<ul class="mb-0">
<li>The author must manage the entire publishing process</li>
<li>Up-front costs such as covers, editing, or ISBNs</li>
<li>Marketing responsibility falls largely on the author</li>
<li>Discoverability can be difficult for new writers</li>
<li>Bookshops may be less likely to stock independently published titles</li>
</ul>
</div>
</div>
<p class="mt-3 mb-0">
Self publishing offers freedom, but it also requires a willingness to learn
and manage many different aspects of the publishing process.
</p>
<p class="mt-3 mb-0">
Everything involved in self publishing is paid for by the author. Nothing is
for free.
</p>
</section>
<section id="genre-and-length-expectations" class="mb-5">
<h2 class="h3 fw-bold mb-3">Genre expectations and word counts</h2>
<p>
Traditional publishing often expects manuscripts to fit within specific
genre categories and typical word-count ranges. For example, some genres
have unofficial expectations around the length of a debut novel.
</p>
<p>
Self publishing removes most of these constraints. Authors can experiment
more freely with structure, length, and storytelling style.
</p>
<p class="mb-0">
That freedom can be creatively liberating, but it also means the author must
decide what works best for the story rather than relying on industry norms.
</p>
</section>
<section id="control-vs-support" class="mb-5">
<h2 class="h3 fw-bold mb-3">Control vs support</h2>
<p>
One of the clearest differences between the two approaches is the balance
between control and support.
</p>
<p>
Traditional publishing provides professional teams who help shape the book
and bring it to market. Self publishing places that responsibility directly
in the hands of the author.
</p>
<p class="mb-0">
Some writers prefer the guidance and structure of traditional publishing,
while others value the independence and flexibility of doing it themselves.
</p>
</section>
<section id="my-view" class="mb-5">
<h2 class="h3 fw-bold mb-3">My personal view</h2>
<p>
Both routes have strengths. Traditional publishing offers professional
support and established distribution channels. Self publishing offers
independence and creative freedom.
</p>
<p>
For many authors the decision comes down to what they want from the experience.
Some writers prefer to focus entirely on writing and leave the rest to a
publishing team. Others enjoy being involved in every stage of bringing
a book to life.
</p>
<p>
Neither path is guaranteed to lead to success. Both require persistence,
patience, and a willingness to keep improving with every book.
</p>
<p>
For me, the decision was an easy one. My writing doesn't fall into a neat genre.
Its gritty and raw and there simply isn't one that fits. In addition I like to write
character rich stories where the characters develop and grow with the reader.
</p>
<p class="mb-0">
I feel
that if I were to try and go down the traditional publishing route editors would hate
my work. They'd want to reduce it considerably. They love limiting an author's work
to 80,000 words whcih I could never allow. I also feel they would want to rip the
heart out of my work, reducing the characterisation to just a few lines, making
descriptive passages one paragraph maximum. Sorry, but that's not for me. I reckognise
that emersive novels aren't for everyone, and that's fine. But they are what I like to
write.
</p>
</section>
</div>
<div class="col-lg-4">
<div class="sticky-lg-top" style="top: 2rem;">
<div class="card border-0 shadow-sm rounded-4 mb-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">On this page</h2>
<ul class="list-unstyled small mb-0">
<li class="mb-2"><a href="#traditional-publishing-overview" class="text-decoration-none">Traditional publishing</a></li>
<li class="mb-2"><a href="#traditional-pros" class="text-decoration-none">Advantages</a></li>
<li class="mb-2"><a href="#traditional-cons" class="text-decoration-none">Challenges</a></li>
<li class="mb-2"><a href="#self-publishing-overview" class="text-decoration-none">Self publishing</a></li>
<li class="mb-2"><a href="#self-publishing-pros" class="text-decoration-none">Advantages</a></li>
<li class="mb-2"><a href="#self-publishing-cons" class="text-decoration-none">Challenges</a></li>
<li class="mb-2"><a href="#genre-and-length-expectations" class="text-decoration-none">Genre expectations</a></li>
<li class="mb-2"><a href="#control-vs-support" class="text-decoration-none">Control vs support</a></li>
<li><a href="#my-view" class="text-decoration-none">My view</a></li>
</ul>
</div>
</div>
<div class="card border-0 bg-light rounded-4 mb-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Quick takeaway</h2>
<p class="small mb-0">
Traditional publishing offers support and distribution. Self publishing
offers control and flexibility. The right choice depends on the authors
goals and working style.
</p>
</div>
</div>
<div class="card border-0 bg-white shadow-sm rounded-4">
<div class="card-body p-4">
<h2 class="h5 fw-bold mb-3">Related guides</h2>
<ul class="list-unstyled small mb-0">
<li class="mb-2">
<a asp-controller="IndieAuthor" asp-action="ReviewsAndArcReaders" class="text-decoration-none">
Reviews and ARC Readers
</a>
</li>
<li class="mb-2">
<a asp-controller="IndieAuthor" asp-action="KdpVsIngramSpark" class="text-decoration-none">
KDP vs IngramSpark
</a>
</li>
<li>
<a asp-controller="IndieAuthor" asp-action="Index" class="text-decoration-none">
Guide Index
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="border-top pt-4 mt-5">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
<a asp-controller="IndieAuthor" asp-action="Index" class="btn btn-dark">
Back to guide index
</a>
<a asp-controller="IndieAuthor" asp-action="Isbns" class="btn btn-dark">
Next: ISBNs
</a>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,157 @@
@{
ViewData["Title"] = "The Alpha Flame: Discovery Chapter 1";
}
<div class="container">
<div class="row">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
<li class="breadcrumb-item active" aria-current="page">Chapter 1 - Drowning in Silence</li>
</ol>
</nav>
</div>
</div>
<!-- Header -->
<div class="text-center mb-5">
<h1 class="fw-bold">Chapter 1 - Drowning in Silence - Beth</h1>
<p>An exclusive glimpse into Beth's story</p>
</div>
<!-- Excerpt Content -->
<div class="row gx-5">
<!-- Scene Image -->
<div class="col-lg-5 mb-4 mb-lg-0">
<responsive-image src="beth-stood-in-bathroom.png" alt="Scene from Beth's story" class="img-fluid rounded-5 border border-3 border-dark shadow-lg" display-width-percentage="50"></responsive-image>
</div>
<!-- Audio and Text -->
<div class="col-lg-7">
<div class="bg-white rounded-5 border border-3 border-dark shadow-lg p-3">
<div class="ratio ratio-16x9">
<video controls="controls" poster="/images/Chapter-1.png" class="rounded-5">
<source src="~/videos/Chapter-1-preview.mp4" type="video/mp4" />
</video>
</div>
<p class="text-center text-muted small pt-2">
Watch Beth narrating part of Chapter 1 - Drowning in Silence.
</p>
<!-- Audio Player -->
<div class="audio-player text-center">
<audio controls>
<source src="/audio/the-alpha-flame-discovery-chapter-1.mp3" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
<p class="text-center text-muted small">
Listen to Beth narrating the complete Chapter 1 - Drowning in Silence.
</p>
</div>
<!-- Text Content -->
<div class="chapter-text">
<p class="chapter-title">Drowning in Silence - Beth</p>
<p>
<em>Id never known silence like that before. The kind that creeps under your skin and settles in your bones, sinking in so deep it feels like it might smother you. When I opened the door, that silence wrapped itself around me, choking me, filling me up until there was nothing else. I didnt even know what I was seeing at first. I think maybe my mind tried to protect me, tried to shield me from what was right in front of me, even though I knew, deep down, that everything was about to change.</em>
</p>
<p>
<em>She was slumped there in the bath, water cold and still around her, her face as blank as a wax dolls, skin washed out, lifeless. The first thought I had, the thing Ill never forgive myself for, was how wrong it looked. It felt surreal, like a trick. This wasnt her. It couldnt be. My mum wasnt a drinker, not like this, not ever, but there was an empty bottle lying on its side beside the bath, rolling slightly as I opened the door wider. It felt like it was mocking me, daring me to believe what I was seeing.</em>
</p>
<p>
<em>I felt sick, my throat clenching, my stomach twisting, and for a moment, I hated her, or whoever had done this to her. Hated the absurdity, the impossibility of it. Shed never have chosen that bottle over me, over herself. And yet there it was, an empty accusation, staring at me from the floor, her face pale and her lips blue. I couldnt make sense of it. I just stood there, a dead thing staring back at her, just as lifeless as she was.</em>
</p>
<p>
<em>They say your life flashes before your eyes when you die, but I think theyre wrong. I think its the people left behind, the ones who have to see it, who have to stand there, watching their entire world collapse around them. I saw everything; all the tiny pieces of a life shed held together for me, every smile, every reassuring word, every single thing that had kept me safe. And I realised, right then, that I was all alone. Utterly and completely alone.</em>
</p>
<p>
<em>Theres something that breaks in you when you lose everything in one heartbeat. Its like the walls inside you just give way, crumbling into nothing, until all thats left is this empty shell. I felt it, that shattering, like glass splintering into a million pieces inside my chest. I remember gripping the doorframe so hard my knuckles turned white, the pain grounding me, keeping me from slipping into whatever dark pit was opening up beneath my feet. I couldnt look away from her. I couldnt move, couldnt breathe. I was frozen, trapped in this nightmare that wouldnt end, a part of me hoping that if I stared long enough, Id wake up. That this would all just go away.</em>
</p>
<p>
<em>But it didnt. And I knew it wouldnt. Because that was the moment my life ended too. She may have been the one in the water, but I was drowning right along with her...</em>
</p>
</div>
</div>
</div>
</div>
</div>
@section Scripts{
<script>
const player = new Plyr('audio');
</script>
}
@section Meta {
<MetaTag meta-title="Chapter 1: Beth - The Alpha Flame by Catherine Lynwood"
meta-description="Explore Chapter 1 of 'The Alpha Flame' by Catherine Lynwood. Discover Maggie's captivating story, full of determination and secrets, set in the vivid 1980s."
meta-keywords="The Alpha Flame, Chapter 1, Maggie, Catherine Lynwood, 1980s fiction, family secrets, strong female characters, captivating novels, fiction by Catherine Lynwood"
meta-author="Catherine Lynwood"
meta-url="https://www.catherinelynwood.com/the-alpha-flame/discovery/chapters/chapter-1-beth"
meta-image="https://www.catherinelynwood.com/images/webp/beth-stood-in-bathroom-600.webp"
meta-image-alt="Beth from 'The Alpha Flame' by Catherine Lynwood"
og-site-name="Catherine Lynwood - The Alpha Flame"
article-published-time="@new DateTime(2024,11,20)"
article-modified-time="@new DateTime(2024,11,20)"
twitter-card-type="player"
twitter-site-handle="@@CathLynwood"
twitter-creator-handle="@@CathLynwood"
twitter-player-width="480"
twitter-player-height="80" />
<script type="application/ld+json">
{
"@@context": "https://schema.org",
"@@type": "Chapter",
"name": "Chapter 1: Drowning in Silence Beth",
"url": "https://www.catherinelynwood.com/the-alpha-flame/discovery/chapters/chapter-1-beth",
"description": "Beth returns home to a haunting silence, discovering her mother lifeless in the bath. This moment shatters her world, leaving her feeling utterly alone.",
"position": 1,
"inLanguage": "en-GB",
"isPartOf": {
"@@type": "Book",
"name": "The Alpha Flame: Discovery",
"author": {
"@@type": "Person",
"name": "Catherine Lynwood",
"url": "https://www.catherinelynwood.com"
},
"publisher": {
"@@type": "Organization",
"name": "Catherine Lynwood"
},
"inLanguage": "en-GB",
"workExample": [
{
"@@type": "Book",
"bookFormat": "https://schema.org/Hardcover",
"isbn": "978-1-0682258-0-2",
"name": "The Alpha Flame: Discovery Hardback"
},
{
"@@type": "Book",
"bookFormat": "https://schema.org/Paperback",
"isbn": "978-1-0682258-1-9",
"name": "The Alpha Flame: Discovery Softback"
},
{
"@@type": "Book",
"bookFormat": "https://schema.org/Paperback",
"isbn": "978-1-0682258-2-6",
"name": "The Alpha Flame: Discovery Amazon Edition"
},
{
"@@type": "Book",
"bookFormat": "https://schema.org/EBook",
"isbn": "978-1-0682258-3-3",
"name": "The Alpha Flame: Discovery eBook"
}
]
}
}
</script>
}

View File

@ -0,0 +1,173 @@
@{
ViewData["Title"] = "The Alpha Flame: Discovery Chapter 13";
}
<div class="container">
<div class="row">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
<li class="breadcrumb-item active" aria-current="page">Chapter 13 - A Name She Never Owned</li>
</ol>
</nav>
</div>
</div>
<!-- Header -->
<div class="text-center mb-5">
<h1 class="fw-bold">Chapter 13 - A Name She Never Owned - Susie</h1>
<p>An exclusive glimpse into Susie's story</p>
</div>
<!-- Excerpt Content -->
<div class="row gx-5">
<!-- Scene Image -->
<div class="col-lg-5 mb-4 mb-lg-0">
<responsive-image src="pub-from-chapter-13.png" alt="The Pub from Chapter 13" class="img-fluid rounded-5 border border-3 border-dark shadow-lg" display-width-percentage="50"></responsive-image>
</div>
<!-- Audio and Text -->
<div class="col-lg-7">
<div class="bg-white rounded-5 border border-3 border-dark shadow-lg p-3">
<!-- Audio Player -->
<div class="audio-player text-center">
<audio controls>
<source src="/audio/the-alpha-flame-discovery-chapter-13.mp3" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
<p class="text-center text-muted small">
Listen to Susie narrating a large excerpt from Chapter 13 - A Name She Never Owned
</p>
</div>
<!-- Text Content -->
<div class="chapter-text">
<p class="chapter-title">A Name She Never Owned - Susie</p>
<p><em>I wasnt sure this was a good idea. Not the date itself, hell, I deserved a decent night out for once, but who I was going with. I mean hes a punter for Gods sake. That said, guys arent exactly queueing up to take me out, and he certainly seemed quite harmless, although perhaps a little needy. I didnt feel threatened by him at all, and Id definitely learned how to defend myself over the past three years, so I had no worries there.</em></p>
<p><em>My biggest problem was my wardrobe, if you can call it that. It wasnt exactly huge, so I didnt have many clothes to choose from. Having spent half my time in short skirts and stockings in the past three years, I decided to go for jeans and a pretty top. I had the nice one that Maggie gave me back at the beginning of the year that I hadnt worn that much, and over the top I decided to wear my leather jacket. Yeah, that would do just perfect.</em></p>
<p><em>I looked at my watch. Nearly eight. I grabbed my handbag and pulled the door shut behind me, heading downstairs and out of the block. Opening the door to the street, the cold hit me like a smack in the face. Good job I wasnt working tonight; Id have frozen my tits off, I thought.</em></p>
<p><em>As I crossed the road, I saw Bens car waiting in the layby. He waved as I approached. I opened the door and got in.</em></p>
<p>“Hi,” he said, smiling nervously. “I wondered if youd remember.”</p>
<p>“Of course,” I replied, returning his smile.</p>
<p><em>He leant over, I thought he was going to try and kiss me, and for a split second I panicked, reaching for the door handle, but instead he reached behind the seat, picking something up. He brought his hand around in front of me, presenting the most amazing bunch of red roses I had ever seen.</em></p>
<p>“These are for you.”</p>
<p>“Wow! Thank you,” I said, genuinely blown away. “Thats so nice of you. No ones ever bought me flowers before.”</p>
<p><em>Bens smile deepened. “Are you ready?”</em></p>
<p>“Sure.”</p>
<p><em>He started the car and pulled away, driving slowly, a little nervously.</em></p>
<p>“Have you been driving long?” I asked after a few moments.</p>
<p><em>Ben glanced across. “About a year. Am I that bad?”</em></p>
<p><em>I laughed softly. “No, not at all. Its just that most guys seem to drive a lot faster.”</em></p>
<p>“I dont really like driving that much. It makes me a little nervous, but I have to do it… otherwise I would never get out of the house.”</p>
<p>“Do you live on your own?”</p>
<p>“Yes.” His expression darkened slightly. “My parents died last year… in a car crash. Thats why Im nervous. I only drive because I have to.”</p>
<p><em>I reached over and rested my hand on his knee for a second, trying to offer some comfort. Ben gave my hand a gentle squeeze in acknowledgment.</em></p>
<p>“Listen,” he said, glancing at me again. “I dont want this to be weird. So as far as Im concerned, yesterday didnt happen. Were just on a date because… well, I like you. I think youre gorgeous.”</p>
<p>“Thank you,” I said, feeling a blush creep up my cheeks.</p>
<p>“And Im not expecting anything either,” he added quickly, looking flustered. “Sex wise, I mean. This is just two people going out for a drink. What either of us does for a living doesnt matter. Okay?”</p>
<p><em>I smiled, warmed by his awkward honesty. “Suits me.”</em></p>
<p><em>We drove for about fifteen minutes, down a few country lanes and up a steep hill, until we came to a lovely country pub nestled into the hillside. Ben pulled into the car park and switched off the engine.</em></p>
<p>“This looks nice. Have you been here before?” I asked.</p>
<p>“No, never. A friend told me about it when I asked him where we could go.”</p>
<p><em>Shocked, I stared at him. “You told your mate youre going on a date with me?”</em></p>
<p>“Yeah. Whats wrong with that?”</p>
<p>“Did you tell him Im a prostitute?”</p>
<p><em>Ben gave a half-smile. “I thought we agreed what each of us does doesnt matter?”</em></p>
<p>“It doesnt.”</p>
<p>“Exactly. But if you must know, I just told him I had a date with a beautiful girl. Thats all.”</p>
<p>“So, youre embarrassed to be seen with me?”</p>
<p><em>Ben looked flustered. “No, not at all. If I was, I wouldnt have asked you out. Ill shout it from the top of that hill over there if you like, but it wont change the way I feel.”</em></p>
<p><em>I was quite touched.</em></p>
<p><em>We got out of the car and Ben locked the doors. The car park was surrounded by a low stone wall. Ben led the way over to some steps that wound up to the pub entrance above. When we reached the door, he held it open for me.</em></p>
<p><em>The pub was lovely. Very old-fashioned, with a big solid oak bar, a few assorted tables, and a mixture of traditional chairs and cosy lounge sofas scattered around. The scent of woodsmoke filled the air.</em></p>
<p>“Do you want to go and sit down, and Ill get the drinks?” Ben asked, glancing at the bar.</p>
<p>“Yeah, okay. Can I have a vodka and Coke, please?”</p>
<p><em>I headed towards a sumptuous-looking sofa in the corner by the open fire and slumped down. It was beautifully soft, and for the first time in ages I started to relax. A thought flashed through my mind… maybe I should have dressed up more. Ben seemed nice. I felt a little underdressed now. Maybe next time, I thought.</em></p>
<p><em>It wasnt long before Ben returned with the drinks. He placed mine carefully in front of me.</em></p>
</div>
</div>
</div>
</div>
</div>
@section Scripts{
<script>
const player = new Plyr('audio');
</script>
}
@section Meta {
<MetaTag meta-title="Chapter 2: Maggie - The Alpha Flame by Catherine Lynwood"
meta-description="Explore Chapter 13 of 'The Alpha Flame' by Catherine Lynwood. Discover Susie's captivating story, full of determination and secrets, set in the vivid 1980s."
meta-keywords="The Alpha Flame, Chapter 13, Susie, Catherine Lynwood, 1980s fiction, family secrets, strong female characters, captivating novels, fiction by Catherine Lynwood"
meta-author="Catherine Lynwood"
meta-url="https://www.catherinelynwood.com/the-alpha-flame/discovery/chapters/chapter-13-susie"
meta-image="https://www.catherinelynwood.com/images/webp/pub-from-chapter-13-600.webp"
meta-image-alt="Maggie from 'The Alpha Flame' by Catherine Lynwood"
og-site-name="Catherine Lynwood - The Alpha Flame"
article-published-time="@new DateTime(2024, 11, 20)"
article-modified-time="@new DateTime(2024, 11, 20)"
twitter-card-type="player"
twitter-site-handle="@@CathLynwood"
twitter-creator-handle="@@CathLynwood"
twitter-player-width="480"
twitter-player-height="80" />
<script type="application/ld+json">
{
"@@context": "https://schema.org",
"@@type": "Chapter",
"name": "Chapter 13: A Name She Never Owned - Susie",
"url": "https://www.catherinelynwood.com/the-alpha-flame/discovery/chapters/chapter-13-susie",
"description": "Maggie Grant bursts onto the page with wit, confidence, and a fiery spirit. As she faces challenges at college and flirts with independence, her strength and secrets begin to unfold.",
"position": 13,
"inLanguage": "en-GB",
"isPartOf": {
"@@type": "Book",
"name": "The Alpha Flame: Discovery",
"author": {
"@@type": "Person",
"name": "Catherine Lynwood",
"url": "https://www.catherinelynwood.com"
},
"publisher": {
"@@type": "Organization",
"name": "Catherine Lynwood"
},
"inLanguage": "en-GB",
"workExample": [
{
"@@type": "Book",
"bookFormat": "https://schema.org/Hardcover",
"isbn": "978-1-0682258-0-2",
"name": "The Alpha Flame: Discovery Hardback"
},
{
"@@type": "Book",
"bookFormat": "https://schema.org/Paperback",
"isbn": "978-1-0682258-1-9",
"name": "The Alpha Flame: Discovery Softback"
},
{
"@@type": "Book",
"bookFormat": "https://schema.org/Paperback",
"isbn": "978-1-0682258-2-6",
"name": "The Alpha Flame: Discovery Amazon Edition"
},
{
"@@type": "Book",
"bookFormat": "https://schema.org/EBook",
"isbn": "978-1-0682258-3-3",
"name": "The Alpha Flame: Discovery eBook"
}
]
}
}
</script>
}

View File

@ -0,0 +1,156 @@
@{
ViewData["Title"] = "The Alpha Flame: Discovery Chapter 2";
}
<div class="container">
<div class="row">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
<li class="breadcrumb-item active" aria-current="page">Chapter 2 - The Last Lesson</li>
</ol>
</nav>
</div>
</div>
<!-- Header -->
<div class="text-center mb-5">
<h1 class="fw-bold">Chapter 2- The Last Lesson - Maggie</h1>
<p>An exclusive glimpse into Maggie's story</p>
</div>
<!-- Excerpt Content -->
<div class="row gx-5">
<!-- Scene Image -->
<div class="col-lg-5 mb-4 mb-lg-0">
<responsive-image src="maggie-with-her-tr6-2.png" alt="Maggie With Her TR6" class="img-fluid rounded-5 border border-3 border-dark shadow-lg" display-width-percentage="50"></responsive-image>
</div>
<!-- Audio and Text -->
<div class="col-lg-7">
<div class="bg-white rounded-5 border border-3 border-dark shadow-lg p-3">
<!-- Audio Player -->
<div class="audio-player text-center">
<audio controls>
<source src="/audio/the-alpha-flame-discovery-chapter-2.mp3" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
<p class="text-center text-muted small">
Listen to Maggie narrating the complete Chapter 2 - The Last Lesson,
</p>
</div>
<!-- Text Content -->
<div class="chapter-text">
<p class="chapter-title">The Last Lesson - Maggie</p>
<p><em>There was a knock at the door.</em></p>
<p>“Colins here,” called mum.</p>
<p>“Okay, Ill be there in a second,” I replied.</p>
<p><em>I was so nervous. I wasnt normally the nervous type, but today was important. Who would book their driving test on Christmas Eve for Gods sake, I must be crazy. Its going to be manic out there,</em> I thought to myself, <em>trying to stay calm.</em></p>
<p><em>I checked my look in the mirror: Hair yes perfect, makeup spot on, my shirt white and business like, possibly a bit thin but necessarily so, skirt perfect length, stockings I always wore stockings, shoes practical.</em> <em>Okay lets do this</em>, I thought.</p>
<p>“Maggie,” called Mum, “youre going to be late.”</p>
<p>“Coming.”</p>
<p><em>I walked into the hallway where mum was talking to Colin.</em></p>
<p>“How do I look?” I asked.</p>
<p>Mum looked at me. “No bra?” she queried.</p>
<p><em>I just looked at her, and mum smiled in response.</em></p>
<p>Colin ran his eyes up and down my body. “Wow, you scrub up well,” he said.</p>
<p>“Good luck, Sweetie,” said Mum.</p>
<p><em>Colin led the way to the car. I dont know why, its not like it was my first lesson. Hopefully, it was my last. We arrived at the car and even more bizarrely he opened the drivers door for me.</em></p>
<p>“Thank you,” I said. “Whats with the chivalry?”</p>
<p>“No reason,” he replied, scurrying around to the passenger side and getting into the front seat.</p>
<p><em>I started performing all my learner checks, seat belt, mirror, all that bosh, then started the car and put it into reverse.</em></p>
<p>“Just take your time,” said Colin.</p>
<p><em>As if totally ignoring him, I revved the engine far too fast and slipped my foot off the clutch. The car leapt backwards in a tight right-hand arc.</em></p>
<p>“Jesus!” exclaimed Colin. “What are you doing?” as he stamped on the brake pedal.</p>
<p>“Sorry, Im rather nervous,” I said, looking over my shoulder at Mum, who was stood waving.</p>
<p><em>Colin looked at me closely, clearly wondering why the cool, calm, and collected girl hed been teaching to drive for the past four months was suddenly driving like a complete idiot. I didnt know what had come over me. I had been waiting for this day for so long and was so ready, but for some reason I was shaking.</em> <em>Pull yourself together</em>, I thought.</p>
<p>Calmly, he said, “Dont worry. Lets just try that again...”</p>
</div>
</div>
</div>
</div>
</div>
@section Scripts{
<script>
const player = new Plyr('audio');
</script>
}
@section Meta {
<MetaTag meta-title="Chapter 2: Maggie - The Alpha Flame by Catherine Lynwood"
meta-description="Explore Chapter 2 of 'The Alpha Flame' by Catherine Lynwood. Discover Maggie's captivating story, full of determination and secrets, set in the vivid 1980s."
meta-keywords="The Alpha Flame, Chapter 2, Maggie, Catherine Lynwood, 1980s fiction, family secrets, strong female characters, captivating novels, fiction by Catherine Lynwood"
meta-author="Catherine Lynwood"
meta-url="https://www.catherinelynwood.com/the-alpha-flame/discovery/chapters/chapter-2-maggie"
meta-image="https://www.catherinelynwood.com/images/webp/maggie-with-her-tr6-2-600.webp"
meta-image-alt="Maggie from 'The Alpha Flame' by Catherine Lynwood"
og-site-name="Catherine Lynwood - The Alpha Flame"
article-published-time="@new DateTime(2024,11,20)"
article-modified-time="@new DateTime(2024,11,20)"
twitter-card-type="player"
twitter-site-handle="@@CathLynwood"
twitter-creator-handle="@@CathLynwood"
twitter-player-width="480"
twitter-player-height="80" />
<script type="application/ld+json">
{
"@@context": "https://schema.org",
"@@type": "Chapter",
"name": "Chapter 2: The Last Lesson Maggie",
"url": "https://www.catherinelynwood.com/the-alpha-flame/discovery/chapters/chapter-2-maggie",
"description": "Maggie Grant bursts onto the page with wit, confidence, and a fiery spirit. As she faces challenges at college and flirts with independence, her strength and secrets begin to unfold.",
"position": 2,
"inLanguage": "en-GB",
"isPartOf": {
"@@type": "Book",
"name": "The Alpha Flame: Discovery",
"author": {
"@@type": "Person",
"name": "Catherine Lynwood",
"url": "https://www.catherinelynwood.com"
},
"publisher": {
"@@type": "Organization",
"name": "Catherine Lynwood"
},
"inLanguage": "en-GB",
"workExample": [
{
"@@type": "Book",
"bookFormat": "https://schema.org/Hardcover",
"isbn": "978-1-0682258-0-2",
"name": "The Alpha Flame: Discovery Hardback"
},
{
"@@type": "Book",
"bookFormat": "https://schema.org/Paperback",
"isbn": "978-1-0682258-1-9",
"name": "The Alpha Flame: Discovery Softback"
},
{
"@@type": "Book",
"bookFormat": "https://schema.org/Paperback",
"isbn": "978-1-0682258-2-6",
"name": "The Alpha Flame: Discovery Amazon Edition"
},
{
"@@type": "Book",
"bookFormat": "https://schema.org/EBook",
"isbn": "978-1-0682258-3-3",
"name": "The Alpha Flame: Discovery eBook"
}
]
}
}
</script>
}

View File

@ -0,0 +1,160 @@
@{
ViewData["Title"] = "The Alpha Flame: Discovery Epilogue";
}
<div class="row">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Extras">Extras</a></li>
<li class="breadcrumb-item active" aria-current="page">Epilogue</li>
</ol>
</nav>
</div>
</div>
<!-- Header -->
<div class="text-center mb-5">
<h1 class="fw-bold">The Alpha Flame: Discovery</h1>
</div>
<!-- Excerpt Content -->
<div class="row gx-5">
<!-- Audio and Text -->
<div class="col-12">
<div class="bg-white rounded-5 border border-3 border-dark shadow-lg p-3">
<responsive-image src="discovery-epilogue.png" class="card-img-top" alt="The Gang Having a Drink at The Barnt Green Inn" display-width-percentage="100"></responsive-image>
<!-- Audio Player -->
<div class="audio-player text-center">
<audio controls>
<source src="/audio/discovery-epilogue.mp3" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
</div>
<div class="text-black text-center">
<h2>Epilogue</h2>
<h3>Narrated by Maggie Late May 1983</h3>
</div>
<!-- Text Content -->
<div class="chapter-text pt-3">
<p><em>Beth and I pulled into the car park of the Barnt Green Inn with the roof down, the late afternoon sun warming our faces. Everything looked more vivid lately, the colours brighter, the air lighter. It was like the whole world had released a breath.</em></p>
<p><em>I locked the car, and we wandered round to the beer garden. Rob and Zoe were already there, sat close, chatting like old friends. Even now, after everything, it still surprised me to see them so relaxed together.</em></p>
<p>Rob spotted us and raised a hand. “Hey!”</p>
<p>I leaned down and kissed him on the cheek. “Hiya. Can you get us a drink?”</p>
<p>“Of course. What do you fancy?”</p>
<p>“Coke for me. Beth?”</p>
<p>She gave a small smile. “Cider, please.”</p>
<p><em>Rob nodded and headed for the bar.</em></p>
<p><em>Beth and I took the seats opposite Zoe.</em></p>
<p>“Howve you both been?” Zoe asked, giving us a once-over.</p>
<p>I glanced at Beth. “Good… I think. Still getting used to it all.”</p>
<p>Beth nodded. “Yeah. Im alright. Put some weight on. Im nearly as fat as Maggie now,” she grinned.</p>
<p>I laughed, nudging her. “Nearly? That ship has sailed, love.”</p>
<p>“Mums fault. Too many sausage sandwiches.”</p>
<p>“You both look good,” Zoe said, eyes lingering just a little longer on Beth. “Bruises gone?”</p>
<p><em>We nodded.</em></p>
<p>“So, what have you two been doing with yourselves?”</p>
<p>Beth perked up. “Helping Maggie with her designs. Turns out Im not bad with a needle.”</p>
<p>“And shes not a bad model either,” I added. “Definitely helps having someone try things on.”</p>
<p>Zoe smirked. “Do you make her twirl like they do on those fashion shows?”</p>
<p>Beth put on a mock pout. “She makes me pose like Im in Vogue. Arms here, head there.”</p>
<p>“Only when youre being annoying,” I said, laughing. “Besides, you love it.”</p>
<p>“True,” Beth admitted. “Its actually fun. I never thought Id enjoy something like that.”</p>
<p><em>Laughter echoed behind us as Rosie and Rebecca came round the corner.</em></p>
<p>“Shes deadly,” Rebecca giggled. “I had my eyes closed the whole way here.”</p>
<p>“It wasnt that bad!” Rosie huffed, trying to look offended. “Not my fault people kept jumping out in front of me.”</p>
<p>“You might try braking instead of shouting. Its far more effective,” Rebecca teased.</p>
<p>“Wheres Rob?” Rosie asked.</p>
<p>“Hes at the bar. If youre quick, you might get a free drink,” I said.</p>
<p><em>Right on cue, Rob returned with a tray.</em></p>
<p>He clocked the new arrivals and sighed. “Im going back to the bar, arent I?”</p>
<p>He placed the drinks down, then turned with theatrical weariness. “The usual, girls?”</p>
<p>“Yes please,” said Rebecca sweetly.</p>
<p><em>Once he was back and settled, we all relaxed into the rhythm of easy chatter.</em></p>
<p>“So, whos got summer plans then?” Rosie asked. “Please tell me someones going somewhere glamorous.”</p>
<p>“Barnt Green counts as glamorous, right?” I said.</p>
<p>“Only if you squint,” said Zoe. “And ignore the smell of wet dog that always seems to hang around the canal.”</p>
<p>“I might take Beth to London,” I said. “Show her the big city.”</p>
<p>Beth raised an eyebrow. “What for?”</p>
<p>“So, you can be horrified at the price of everything and then come home saying how much nicer Birmingham is.”</p>
<p>“Sold,” she laughed.</p>
<p>Zoe leaned forward, lowering her voice slightly. “I heard from Graham. The case against Ricks progressing.”</p>
<p><em>Beth stiffened slightly but nodded.</em></p>
<p>“Are you two going to be alright when it comes to court?”</p>
<p><em>Beth and I exchanged a look. Wed talked about it endlessly in private.</em></p>
<p>“We think so,” Beth said. “Its black and white for us. We just have to tell the truth.”</p>
<p><em>Rob had gone quiet, his brow furrowed.</em></p>
<p>“You alright?” I asked.</p>
<p>He hesitated, then said, “Its just… I dont believe in coincidences. Not ones like that.”</p>
<p>Rebecca cocked her head. “Is he always this cryptic?”</p>
<p>“Most of the time,” I said. “This ones new, though. Go on, Rob. Spit it out.”</p>
<p>“Rick,” he said. “The night Beth ended up under the flyover. Him just being there… it never sat right with me.”</p>
<p>Beths face fell still. “You think he was following me?”</p>
<p>Rob gave a half-shrug. “Feels too neat to be chance.”</p>
<p>Beth looked down. “He did have the flat, though. Cindy lived there too, remember. Maybe he was nearby anyway.”</p>
<p>“Maybe,” Rob said, but not like he believed it.</p>
<p>Rosie clapped her hands lightly. “Well, on a brighter note, Gregs birthday is next week. Hes throwing a party. Youre all invited.”</p>
<p>Beths eyes lit up. “Ive never been to a grown-up party. Not really. Im in.”</p>
<p>“Youll need something to wear,” I said. “And no, youre not stealing that red skirt again.”</p>
<p>“I only borrowed it.”</p>
<p>“For three weeks.”</p>
<p>Rosie smirked. “Are you two dressing the same again?”</p>
<p>“Obviously,” we said in unison.</p>
<p>Zoe groaned. “Have you noticed they do that constantly now?” She turned to Rob. “Seriously, get out while you still can.”</p>
<p><em>Beth and I grinned at each other.</em></p>
<p>“I really dont know what theyre…”</p>
<p>“…on about. Do you?” we said, still in sync.</p>
<p>Rebecca had been quiet, but now her voice was cautious. “Are you going to look for your dad?”</p>
<p><em>We both paused.</em></p>
<p>“We would like to,” I replied. “We havent really got too much to go on. There are a few things weve got to follow up in Beths memory tin. Theres a bank book for one. Its only got one pound seventy-nine in it, but it might give us a clue.”</p>
<p>Beth added. “Weve also got the wedding photos… and a date. Oh, and even the name of the church. So, we might be able to find something there, if we can find the right church.”</p>
<p><em>The table fell quiet for a moment.</em></p>
<p>“Did your mum ever talk about him?” Rebecca asked.</p>
<p>“Not really,” Beth said. “She used to say Hes nobody worth knowing. But Im not sure she meant it.”</p>
<p>Rosie glanced up. “Its such a lovely day. We should go to the beach one weekend.”</p>
<p>Beths eyes went wide. “Yes! All of us. Itd be amazing.”</p>
<p>“Ill bring sandwiches,” said Rob. “Beth, youre banned from cooking anything though.”</p>
<p>“Hey! That cheese toastie was only slightly burnt.”</p>
<p>“It was charcoal.”</p>
<p>“I quite liked it,” I added. “Adds crunch.”</p>
<p><em>General agreement followed.</em></p>
<p>“I need the loo,” said Rebecca.</p>
<p>“Me too. Ill grab drinks on the way back. Same again?”</p>
<p>“Perfect,” said Zoe.</p>
<p>Once they were out of earshot, Zoe leaned in. “She really doesnt know, does she? About Sophie.”</p>
<p><em>We shook our heads.</em></p>
<p>“Theyre nothing alike,” I said. “Different planets.”</p>
<p>“Sophie still avoiding you?” Zoe asked.</p>
<p>I nodded. “Mostly. I saw her once. She didnt stop.”</p>
<p>Beths face darkened. “I can still see her face. That night. Just after she gave the order.”</p>
<p>Zoe blinked. “You think she did?”</p>
<p>Beth didnt hesitate. “She knew what Rick was going to do. That club… theres more to it. You think that, dont you?”</p>
<p>Zoe nodded slowly. “Graham thinks so too. Says something stinks, but he hasnt nailed it yet.”</p>
<p>Beths voice dropped to a whisper. “Mum wasnt killed for nothing. Adam either. Rick even said so.”</p>
<p>“Has Graham got anything else out of Rick?” I asked.</p>
<p>“Nothing solid,” Zoe replied. “Hes clammed up. Hes gone No comment on everything.”</p>
<p>“And our aunt? Still scared?” Beth asked.</p>
<p>“Hes working on her. Shes terrified. Hes got to go gently.”</p>
<p>Beths voice hardened. “What about Baker?”</p>
<p>Zoes face tightened. “Hes not in the frame. Yet. Grahams gathering evidence, but if he pushes too soon, Baker will cover his tracks.”</p>
<p><em>Rebecca and Rosie returned, laughing.</em></p>
<p>“What have you two done now?” I asked.</p>
<p>“She dropped a tray of drinks on some poor guy,” Rosie grinned. “Shes worse than Rob.”</p>
<p>“Oi!” Rob protested. “That was once.”</p>
<p>“Twice,” I said, grinning, and leaned in to kiss him.</p>
<p>Beth rolled her eyes. “Theyre going to be unbearable if they stay this happy.”</p>
<p>“Jealous?” I teased.</p>
<p>“A bit,” she admitted, then smiled. “But in the good way.”</p>
<p><em>The sun dipped lower, the sky glowing amber. For now, we had peace. But we hadnt finished. Not yet. The flame had only just started to burn.</em></p>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,113 @@
@{
ViewData["Title"] = "The Alpha Flame: Reckoning Extras";
int? accessLevel = Context.Session.GetInt32("BookAccessLevel");
int? accessBook = Context.Session.GetInt32("BookAccessMax");
}
<div class="row">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
<li class="breadcrumb-item active" aria-current="page"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
<li class="breadcrumb-item active" aria-current="page"><a asp-controller="Reckoning" asp-action="Index">Reckoning</a></li>
<li class="breadcrumb-item active" aria-current="page">Extras</li>
</ol>
</nav>
</div>
</div>
<div class="container mt-5">
<h1 class="extras-header">The Alpha Flame Reckoning Exclusive Extras</h1>
<div class="extras-grid mt-4">
@if (accessLevel >= 1)
{
<div class="card extra-card">
<div class="card-body">
<h5 class="card-title">Epilogue</h5>
<p class="card-text">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</p>
<a asp-action="Epilogue" class="btn btn-dark btn-sm">Read or Listen</a>
</div>
</div>
}
@if (accessLevel >= 2)
{
<div class="card extra-card">
<div class="card-body">
<h5 class="card-title">Discovery Scrap Book</h5>
<p class="card-text">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.</p>
<a asp-action="ScrapBook" class="btn btn-dark btn-sm">View Scrapbook</a>
</div>
</div>
<div class="card extra-card">
<div class="card-body">
<h5 class="card-title">Discovery Soundtrack</h5>
<p class="card-text">Have a listen to The Alpha Flame soundtrack. A selection of original songs written by me and put to music.</p>
<a asp-action="Soundtrack" class="btn btn-dark btn-sm">Soundtrack</a>
</div>
</div>
}
@if (accessLevel >= 3)
{
<div class="card extra-card">
<div class="card-body">
<h5 class="card-title">Listen to The Alpha Flame: Discovery</h5>
<p class="card-text">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.</p>
<a asp-action="Listen" class="btn btn-dark btn-sm">Listen to the Book</a>
</div>
</div>
<div class="card extra-card">
<div class="card-body">
<h5 class="card-title">Scrapbook: Maggies Designs</h5>
<p class="card-text">Flip through Maggies sketches, fashion notes, and photos from her original designs including the infamous red skirt.</p>
<a asp-action="MaggiesDesigns" class="btn btn-dark btn-sm">View Scrapbook</a>
</div>
</div>
}
</div>
</div>
@section Meta {
<style>
.extras-header {
text-align: center;
margin-top: 3rem;
margin-bottom: 2rem;
}
.extra-card {
border: none;
border-radius: 1rem;
box-shadow: 0 0 15px rgba(0,0,0,0.1);
transition: transform 0.2s ease;
}
.extra-card:hover {
transform: translateY(-5px);
}
.card-title {
font-weight: bold;
font-size: 1.25rem;
}
.card-text {
font-size: 0.95rem;
}
.extras-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
}
.access-label {
font-size: 0.8rem;
font-style: italic;
color: #888;
}
</style>
}

View File

@ -0,0 +1,165 @@
@{
ViewData["Title"] = "How to Buy The Alpha Flame: Discovery";
}
<div class="container">
<div class="row">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
<li class="breadcrumb-item active" aria-current="page">How to Buy</li>
</ol>
</nav>
</div>
</div>
<h1 class="mb-4">How to Buy <span class="fw-light">The Alpha Flame: Discovery</span></h1>
<p class="lead">There are several ways to enjoy the book — whether you prefer digital, print, or audio. If you'd like to support the author directly, the <strong>direct links</strong> below are the best way to do so.</p>
<!-- eBook -->
<div class="card mb-4">
<div class="card-header">
<i class="fad fa-tablet-alt text-primary me-2"></i> eBook (Kindle)
</div>
<div class="card-body">
<p>The Kindle edition is available via your local Amazon store:</p>
<a id="kindleLink" href="https://www.amazon.com/dp/B0FBS427VD" class="btn btn-outline-dark mb-2" target="_blank">
Buy Kindle Edition
</a>
<p class="small text-muted">Automatically redirects based on your country.</p>
</div>
</div>
<!-- Paperback -->
<div class="card mb-4">
<div class="card-header">
<i class="fad fa-book text-success me-2"></i> Paperback (Bookshop Edition)
</div>
<div class="card-body">
<p>
This version is designed for local bookstores and global retailers.
</p>
<!-- IngramSpark direct paperback link placeholder -->
<a id="paperbackLinkSelf" href="https://shop.ingramspark.com/b/084?params=6easpH54PaugzXFKdF4Tu4Izb0cvkMqbj3ZNlaYBKMJ" class="btn btn-outline-dark mb-2" target="_blank">
📦 Buy Direct (Save & Support Author)
</a>
<a id="paperbackLink" href="https://www.amazon.com/dp/1068225815" class="btn btn-outline-dark mb-2" target="_blank">
Buy on Amazon
</a>
<p class="small text-muted mb-0">ISBN 978-1-0682258-1-9</p>
<p class="small text-muted" id="extraRetailers"></p>
</div>
</div>
<!-- Hardback -->
<div class="card mb-4">
<div class="card-header">
<i class="fad fa-gem text-danger me-2"></i> Collectors Edition (Hardback)
</div>
<div class="card-body">
<p>
A premium collectors hardback edition, available via bookstores and online.
</p>
<!-- IngramSpark direct hardback link placeholder -->
<a id="hardbackLinkSelf" href="https://shop.ingramspark.com/b/084?params=GC1p1c8b66Rhfoy6Tq97SJmmhdZSEYuxBcCY5zxNstO" class="btn btn-outline-dark mb-2" target="_blank">
💎 Buy Direct (Save & Support Author)
</a>
<a id="hardbackLink" href="https://www.amazon.com/dp/1068225807" class="btn btn-outline-dark mb-2" target="_blank">
Buy on Amazon
</a>
<p class="small text-muted mb-0">ISBN 978-1-0682258-0-2</p>
<p class="small text-muted" id="extraRetailersHardback"></p>
</div>
</div>
<!-- Audiobook -->
<div class="card mb-4">
<div class="card-header">
<i class="fad fa-headphones-alt text-info me-2"></i> Audiobook (AI-Read)
</div>
<div class="card-body">
<p class="mb-2">Listen to the entire book for free on ElevenLabs (Elevenlabs subscription required):</p>
<a href="https://elevenreader.io/audiobooks/the-alpha-flame/e4Ppi7wLTLGOLrWe3Y6q?voiceId=Xb7hH8MSUJpSbSDYk0k2" class="btn btn-outline-dark mb-3" target="_blank">
🎧 Listen on ElevenLabs
</a>
<br />
</div>
</div>
</div>
@section Scripts{
<!-- Geo-based link adjustment -->
<script>
fetch('https://ipapi.co/json/')
.then(response => response.json())
.then(data => {
const country = data.country_code;
let kindleLink = "https://www.amazon.com/dp/B0FBS427VD";
let paperbackLink = "https://www.amazon.com/dp/1068225815";
let hardbackLink = "https://www.amazon.com/dp/1068225807";
let extraRetailers = "";
let extraRetailersHardback = "";
switch (country) {
case "GB":
kindleLink = "https://www.amazon.co.uk/dp/B0FBS427VD";
paperbackLink = "https://www.amazon.co.uk/dp/1068225815";
hardbackLink = "https://www.amazon.co.uk/dp/1068225807";
extraRetailers = 'Also available at <a href="https://www.waterstones.com/book/the-alpha-flame/catherine-lynwood/9781068225819" target="_blank">Waterstones</a>';
extraRetailersHardback = 'Also available at <a href="https://www.waterstones.com/book/the-alpha-flame/catherine-lynwood/9781068225802" target="_blank">Waterstones</a>';
break;
case "US":
kindleLink = "https://www.amazon.com/dp/B0FBS427VD";
paperbackLink = "https://www.amazon.com/dp/1068225815";
hardbackLink = "https://www.amazon.com/dp/1068225807";
extraRetailers = 'Also available at <a href="https://www.barnesandnoble.com/s/9781068225819" target="_blank">Barnes & Noble</a>';
extraRetailersHardback = 'Also available at <a href="https://www.barnesandnoble.com/s/9781068225802" target="_blank">Barnes & Noble</a>';
break;
case "CA":
kindleLink = "https://www.amazon.ca/dp/B0FBS427VD";
paperbackLink = "https://www.amazon.ca/dp/1068225815";
hardbackLink = "https://www.amazon.ca/dp/1068225807";
break;
case "AU":
kindleLink = "https://www.amazon.com.au/dp/B0FBS427VD";
paperbackLink = "https://www.amazon.com.au/dp/1068225815";
hardbackLink = "https://www.amazon.com.au/dp/1068225807";
break;
}
// Set Amazon + retailer content
const elKindle = document.getElementById("kindleLink");
const elPbAmazon = document.getElementById("paperbackLink");
const elHbAmazon = document.getElementById("hardbackLink");
const elExtra = document.getElementById("extraRetailers");
const elExtraHb = document.getElementById("extraRetailersHardback");
if (elKindle) elKindle.setAttribute("href", kindleLink);
if (elPbAmazon) elPbAmazon.setAttribute("href", paperbackLink);
if (elHbAmazon) elHbAmazon.setAttribute("href", hardbackLink);
if (elExtra) elExtra.innerHTML = extraRetailers;
if (elExtraHb) elExtraHb.innerHTML = extraRetailersHardback;
// Show IngramSpark only in GB/US; hide elsewhere
const showIngram = country === "GB" || country === "US";
["paperbackLinkSelf", "hardbackLinkSelf"].forEach(id => {
const el = document.getElementById(id);
if (!el) return;
el.classList.toggle("d-none", !showIngram); // add when false, remove when true
});
})
.catch(() => {
// If the geo lookup fails, leave links as-is
});
</script>
}

View File

@ -0,0 +1,264 @@
@model CatherineLynwood.Models.TitlePageViewModel
@{
ViewData["Title"] = "The Alpha Flame: Reckoning";
}
<div class="reckoning-page-shell">
<!-- Detective desk / case file section -->
<section class="reckoning-desk-section">
<div class="reckoning-desk-surface">
<div class="reckoning-desk-props">
<div class="reckoning-desk-photos">
<div class="reckoning-desk-photo reckoning-desk-photo-1">
<responsive-image src="flyover-at-night.png" class="img-fluid" alt="The Rubery flyover at night time" display-width-percentage="50"></responsive-image>
</div>
<div class="reckoning-desk-photo reckoning-desk-photo-2">
<responsive-image src="sophie-jones-nightclub.png" class="img-fluid" alt="Sophie Jones" display-width-percentage="50"></responsive-image>
</div>
<div class="reckoning-desk-photo reckoning-desk-photo-3">
<responsive-image src="the-alpha-flame-reckoning-cover.png" class="img-fluid" alt="Maggie Grant" display-width-percentage="50"></responsive-image>
</div>
</div>
<div class="reckoning-desk-postit">
<span>Fashion show<br />Scandals,<br />tonight!</span>
</div>
</div>
<div class="reckoning-casefile-page">
<div class="casefile-stage">
<div class="casefile" id="caseFile">
<div class="casefile-rear">
<img src="/images/webp/folder-rear.webp" alt="Case file interior" class="casefile-rear-img" />
<div class="casefile-flap-shadow"></div>
<div class="casefile-inner">
<div class="casefile-paper-stack">
<div class="casefile-paper-stack-layer casefile-paper-stack-layer-1"></div>
<div class="casefile-paper-stack-layer casefile-paper-stack-layer-2"></div>
<div class="casefile-content">
<div class="casefile-panel active" data-tab="summary">
<partial name="_ReckoningSummary" model="Model"></partial>
</div>
<div class="casefile-panel" data-tab="subjects">
<partial name="_ReckoningSubjects" model="Model"></partial>
</div>
<div class="casefile-panel" data-tab="evidence">
<partial name="_ReckoningEvidence" model="Model"></partial>
</div>
<div class="casefile-panel" data-tab="notes">
<partial name="_ReckoningReview" model="Model"></partial>
</div>
<div class="casefile-panel" data-tab="buy">
<partial name="_ReckoningBuy" model="Model"></partial>
</div>
<div class="casefile-panel restricted-panel" data-tab="restricted">
<partial name="_ReckoningRestricted" model="Model"></partial>
</div>
</div>
</div>
<div class="casefile-tabs">
<button type="button" class="casefile-tab active" data-tab="summary">Summary</button>
<button type="button" class="casefile-tab" data-tab="subjects">Subjects</button>
<button type="button" class="casefile-tab" data-tab="evidence">Evidence</button>
<button type="button" class="casefile-tab" data-tab="notes">Review</button>
<button type="button" class="casefile-tab" data-tab="buy">Purchase</button>
<button type="button" class="casefile-tab restricted" data-tab="restricted">Restricted</button>
</div>
</div>
</div>
<div class="casefile-front" id="caseFileFront" role="button" tabindex="0" aria-label="Open case file">
<div class="casefile-front-face casefile-front-face-front">
<img src="/images/webp/folder-front.webp" alt="Case file cover" class="casefile-front-img" />
<div class="casefile-front-overlay">
<div class="casefile-stamp">
RESTRICTED
</div>
<div class="casefile-cover-text">
<div class="casefile-cover-kicker">WEST MIDLANDS POLICE</div>
<h1>ALPHA FLAME: RECKONING</h1>
<p>Case Ref: AFR-1983-06</p>
<p>Status: Active Investigation</p>
</div>
<div class="casefile-open-hint">Click to open file</div>
</div>
</div>
<div class="casefile-front-face casefile-front-face-back">
<img src="/images/webp/folder-front-inside.webp" alt="" class="casefile-front-img casefile-front-img-back" />
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
@section BackgroundVideo {
<div></div>
}
@section CSS {
<link href="~/css/reckoning.css" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Permanent+Marker&family=Shadows+Into+Light&display=swap" rel="stylesheet">
}
@section Scripts {
<script>
document.addEventListener("DOMContentLoaded", function () {
const trigger = document.querySelector(".casefile-audio-trigger");
const audio = document.getElementById("evidenceTapeAudio");
if (!trigger || !audio) return;
trigger.addEventListener("click", function () {
if (audio.paused) {
audio.play().then(function () {
trigger.classList.add("is-playing");
}).catch(function () {
});
} else {
audio.pause();
trigger.classList.remove("is-playing");
}
});
audio.addEventListener("pause", function () {
if (!audio.ended) {
trigger.classList.remove("is-playing");
}
});
audio.addEventListener("ended", function () {
trigger.classList.remove("is-playing");
audio.currentTime = 0;
});
});
</script>
<script>
document.addEventListener("DOMContentLoaded", function () {
const caseFile = document.getElementById("caseFile");
const frontCover = document.getElementById("caseFileFront");
const tabs = caseFile.querySelectorAll(".casefile-tab");
const panels = caseFile.querySelectorAll(".casefile-panel");
const images = caseFile.querySelectorAll("img");
let loadedCount = 0;
let assetsReady = false;
let openTimer = null;
let isOpening = false;
let hasEnteredView = false;
function openCaseFile() {
if (!assetsReady || isOpening || caseFile.classList.contains("open")) {
return;
}
isOpening = true;
caseFile.classList.add("revealed");
caseFile.classList.add("open");
if (openTimer) {
clearTimeout(openTimer);
openTimer = null;
}
}
function startAutoOpenTimer() {
if (!assetsReady || hasEnteredView === false || openTimer || caseFile.classList.contains("open")) {
return;
}
openTimer = setTimeout(() => {
openCaseFile();
}, 5000);
}
function onAssetLoaded() {
loadedCount++;
if (loadedCount === images.length) {
assetsReady = true;
caseFile.classList.add("assets-ready");
setTimeout(() => {
caseFile.classList.add("ready");
}, 250);
startAutoOpenTimer();
}
}
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && !hasEnteredView) {
hasEnteredView = true;
startAutoOpenTimer();
observer.disconnect();
}
});
}, {
threshold: 0.35
});
observer.observe(caseFile);
images.forEach(img => {
if (img.complete) {
onAssetLoaded();
} else {
img.addEventListener("load", onAssetLoaded, { once: true });
img.addEventListener("error", onAssetLoaded, { once: true });
}
});
frontCover.addEventListener("click", function () {
openCaseFile();
});
frontCover.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
openCaseFile();
}
});
tabs.forEach(tab => {
tab.addEventListener("click", function () {
const tabName = this.dataset.tab;
tabs.forEach(t => t.classList.remove("active"));
panels.forEach(p => p.classList.remove("active"));
this.classList.add("active");
const targetPanel = caseFile.querySelector(`.casefile-panel[data-tab="${tabName}"]`);
if (targetPanel) {
targetPanel.classList.add("active");
}
});
});
});
</script>
}

View File

@ -0,0 +1,143 @@
@model CatherineLynwood.Models.TitlePageViewModel
@{
ViewData["Title"] = "The Alpha Flame: Reckoning";
bool showReviews = Model.Reviews.Items.Any();
}
<div class="reckoning-mobile-page">
<header class="reckoning-mobile-hero">
<div class="reckoning-mobile-hero-inner">
<div class="reckoning-mobile-stamp">RESTRICTED</div>
<div class="reckoning-mobile-kicker">WEST MIDLANDS POLICE</div>
<h1>The Alpha Flame: Reckoning</h1>
<p class="reckoning-mobile-ref">Case Ref: AFR-1983-06</p>
<p class="reckoning-mobile-status">Status: Active Investigation</p>
</div>
</header>
<section class="reckoning-mobile-props">
<div class="reckoning-mobile-props-scroll">
<div class="reckoning-mobile-polaroid polaroid-tilt-left">
<responsive-image src="the-alpha-flame-reckoning-cover.png"
class="img-polaroid"
alt="The Alpha Flame: Reckoning book cover"
display-width-percentage="50"></responsive-image>
</div>
<div class="reckoning-mobile-polaroid polaroid-tilt-right">
<responsive-image src="sophie-jones-nightclub.png"
class="img-polaroid"
alt="Sophie Jones"
display-width-percentage="50"></responsive-image>
</div>
<div class="reckoning-mobile-polaroid polaroid-tilt-slight">
<responsive-image src="flyover-at-night.png"
class="img-polaroid"
alt="The Rubery flyover at night time"
display-width-percentage="50"></responsive-image>
</div>
</div>
</section>
<nav class="reckoning-mobile-nav">
<a href="#summary">Summary</a>
<a href="#subjects">Subjects</a>
<a href="#evidence">Evidence</a>
<a href="#review">Review</a>
<a href="#purchase">Buy</a>
<a href="#restricted">Restricted</a>
</nav>
<main class="reckoning-mobile-file">
<section id="summary" class="reckoning-mobile-section">
<div class="reckoning-mobile-tab reckoning-tab-summary">Summary</div>
<div class="reckoning-mobile-paper">
@await Html.PartialAsync("_ReckoningSummary", Model)
</div>
</section>
<section id="subjects" class="reckoning-mobile-section">
<div class="reckoning-mobile-tab reckoning-tab-subjects">Subjects</div>
<div class="reckoning-mobile-paper">
@await Html.PartialAsync("_ReckoningSubjects", Model)
</div>
</section>
<section id="evidence" class="reckoning-mobile-section">
<div class="reckoning-mobile-tab reckoning-tab-evidence">Evidence</div>
<div class="reckoning-mobile-paper">
@await Html.PartialAsync("_ReckoningEvidence", Model)
</div>
</section>
<section id="review" class="reckoning-mobile-section">
<div class="reckoning-mobile-tab reckoning-tab-review">Review</div>
<div class="reckoning-mobile-paper">
@await Html.PartialAsync("_ReckoningReview", Model)
</div>
</section>
<section id="purchase" class="reckoning-mobile-section">
<div class="reckoning-mobile-tab reckoning-tab-purchase">Purchase</div>
<div class="reckoning-mobile-paper">
@await Html.PartialAsync("_ReckoningBuy", Model)
</div>
</section>
<section id="restricted" class="reckoning-mobile-section">
<div class="reckoning-mobile-tab reckoning-tab-restricted">Restricted</div>
<div class="reckoning-mobile-paper">
@await Html.PartialAsync("_ReckoningRestricted", Model)
</div>
</section>
</main>
</div>
@section BackgroundVideo {
<div></div>
}
@section CSS {
<link href="~/css/reckoning-mobile.css" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Permanent+Marker&family=Shadows+Into+Light&display=swap" rel="stylesheet">
}
@section Scripts {
<script>
document.addEventListener("DOMContentLoaded", function () {
const trigger = document.querySelector(".casefile-audio-trigger");
const audio = document.getElementById("evidenceTapeAudio");
if (!trigger || !audio) return;
trigger.addEventListener("click", function () {
if (audio.paused) {
audio.play().then(function () {
trigger.classList.add("is-playing");
}).catch(function () {
});
} else {
audio.pause();
trigger.classList.remove("is-playing");
}
});
audio.addEventListener("pause", function () {
if (!audio.ended) {
trigger.classList.remove("is-playing");
}
});
audio.addEventListener("ended", function () {
trigger.classList.remove("is-playing");
audio.currentTime = 0;
});
});
</script>
}

View File

@ -0,0 +1,375 @@
@*
For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@{
}
<div class="reckoning-page-shell">
<!-- Hero / standard book layout -->
<section class="reckoning-hero-section">
<div class="reckoning-hero-video">
<video id="siteBackgroundVideo"
class="reckoning-hero-video-element"
autoplay
muted
loop
playsinline
preload="none">
</video>
<div class="reckoning-hero-video-overlay"></div>
</div>
<div class="container-xxl">
<div class="row reckoning-hero-row align-items-stretch">
<div class="col-lg-5 col-xl-4 d-flex reckoning-hero-cover-column">
<div class="reckoning-hero-cover-panel">
<div class="card character-card h-100 flex-fill" id="cover-card">
<responsive-image src="the-alpha-flame-reckoning-cover.png"
class="card-img-top"
alt="The Alpha Flame: Reckoning book cover"
display-width-percentage="50"></responsive-image>
<div class="card-body border-top border-3 border-dark">
<h3 class="card-title h5 mb-1">The Alpha Flame: <span class="fw-light">Reckoning</span></h3>
<p class="card-text mb-0">
Book 2 of the Alpha Flame trilogy.
The danger didnt end with <em>Discovery</em>. It got smarter.
</p>
</div>
</div>
</div>
</div>
<div class="col-lg-7 col-xl-8 d-flex reckoning-hero-info-column">
<div class="reckoning-hero-info-panel">
<div class="card character-card h-100 flex-fill" id="hero-media-card">
<div class="card-body d-flex flex-column">
<div>
<h3 class="card-title">The Alpha Flame: <span class="fw-light">Reckoning</span></h3>
<p class="fst-italic">
Some secrets stay buried. Others demand a reckoning.
</p>
<p>
After everything they survived in <em>Discovery</em>, Maggie and Beth should finally be free.
Instead, the nightmares follow them into daylight, and the past starts tugging at them with intent.
Beth is still rebuilding herself piece by piece. Maggie is trying to hold everything together.
And somewhere in the background, the people who benefitted from Beths silence are quietly noticing that she is no longer alone.
</p>
<p>
As the truth about their mother begins to surface, Maggie and Beth are drawn deeper into a web of lies,
hidden money, and dangerous men who would do anything to keep old secrets buried. The closer they get to the
truth, the clearer it becomes that someone has been watching them all along.
</p>
<p>
And this time, walking away isnt an option.
</p>
</div>
<!-- Call to action row -->
<div class="d-flex gap-3 flex-wrap">
<a asp-controller="Discovery" asp-action="Index" class="btn btn-outline-dark">
Start with Discovery
</a>
<a asp-controller="TheAlphaFlame" asp-action="Index" class="btn btn-outline-secondary">
Explore the trilogy
</a>
</div>
<!-- Small continuity note -->
<div class="mt-3 small text-muted">
<strong>Tip:</strong> <em>Reckoning</em> follows directly after <em>Discovery</em>.
If youre new to the series, start there for maximum emotional damage.
</div>
@await Component.InvokeAsync("BuyPanel", new { ISO2 = Model.UserIso2, Src = Model.Src, Title = Model.Title })
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Detective desk / case file section -->
<section class="reckoning-desk-section">
<div class="reckoning-desk-surface">
<div class="reckoning-desk-props">
<div class="reckoning-desk-photos">
<div class="reckoning-desk-photo reckoning-desk-photo-1">
<responsive-image src="flyover-at-night.png" class="img-fluid" alt="The Rubery flyover at night time" display-width-percentage="50"></responsive-image>
</div>
<div class="reckoning-desk-photo reckoning-desk-photo-2">
<responsive-image src="sophie-jones-nightclub.png" class="img-fluid" alt="Sophie Jones" display-width-percentage="50"></responsive-image>
</div>
<div class="reckoning-desk-photo reckoning-desk-photo-3">
<responsive-image src="the-alpha-flame-reckoning-cover.png" class="img-fluid" alt="Maggie Grant" display-width-percentage="50"></responsive-image>
</div>
</div>
<div class="reckoning-desk-postit">
<span>Maggie<br />Scandals,<br />tonight!</span>
</div>
</div>
<div class="reckoning-casefile-page">
<div class="casefile-stage">
<div class="casefile" id="caseFile">
<div class="casefile-rear">
<img src="/images/webp/folder-rear.webp" alt="Case file interior" class="casefile-rear-img" />
<div class="casefile-flap-shadow"></div>
<div class="casefile-inner">
<div class="casefile-paper-stack">
<div class="casefile-paper-stack-layer casefile-paper-stack-layer-1"></div>
<div class="casefile-paper-stack-layer casefile-paper-stack-layer-2"></div>
<div class="casefile-content">
<div class="casefile-panel active" data-tab="summary">
<h2>Case Summary</h2>
<p>
They thought they were searching for the past. They didnt realise the past was searching for them.
</p>
<p>
As Maggie and Beth begin to piece together the fragments of their lives, the line between truth and danger begins to blur. Every answer leads to another question. Every step forward pulls them deeper into something neither of them fully understands.
</p>
<p>
What began as a search for identity has uncovered a pattern… one that stretches back years, touching lives that were never meant to be connected.
</p>
<p>
Witnesses are unreliable. Records are incomplete. And certain names keep appearing where they shouldnt.
</p>
<p>
Someone has been watching.
</p>
<p>
And now, theyre getting closer.
</p>
</div>
<div class="casefile-panel" data-tab="subjects">
<h2>Subjects</h2>
<p class="reckoning-case-subtitle">Primary persons connected to ongoing enquiries.</p>
<div class="reckoning-subjects-scroll">
<div class="reckoning-subject-entry">
<h3>SUBJECT: MAGGIE</h3>
<p><strong>Age:</strong> Early 20s</p>
<p><strong>Status:</strong> Civilian</p>
<p><strong>Notes:</strong> Confident, observant, and increasingly proactive in ongoing events. Demonstrates strong protective instincts toward Beth and shows a marked tendency to pursue answers independently. Appears stronger than initial assessments suggested. Extent of involvement remains unclear.</p>
</div>
<div class="reckoning-subject-entry">
<h3>SUBJECT: BETH</h3>
<p><strong>Age:</strong> Early 20s</p>
<p><strong>Status:</strong> Victim / Person of Interest</p>
<p><strong>Notes:</strong> Known to have suffered sustained trauma and coercive control. Holds key links to several individuals under observation. Emotional condition fragile, though recent behaviour suggests increasing resilience. May possess critical information, whether knowingly or otherwise.</p>
</div>
<div class="reckoning-subject-entry">
<h3>PERSON OF INTEREST: SIMON JONES</h3>
<p><strong>Occupation:</strong> Business owner</p>
<p><strong>Status:</strong> Under informal scrutiny</p>
<p><strong>Notes:</strong> Name appears repeatedly across multiple lines of enquiry. Public profile respectable, though several associations remain unresolved. No direct action taken to date. Influence believed to extend further than officially recorded.</p>
<p class="reckoning-redacted">Known associates: ███████████████</p>
</div>
<div class="reckoning-subject-entry">
<h3>PERSON OF INTEREST: SOPHIE JONES</h3>
<p><strong>Age:</strong> Early 20s</p>
<p><strong>Status:</strong> Associated party</p>
<p><strong>Notes:</strong> Outwardly composed and cooperative, but behaviour raises concern. Presence noted around several incidents without formal connection being established. Considered potentially significant. Further attention advised.</p>
</div>
</div>
</div>
<div class="casefile-panel" data-tab="evidence">
<h2>Evidence</h2>
<p>Recovered fragments. Missing pieces. Unanswered questions.</p>
</div>
<div class="casefile-panel" data-tab="notes">
<h2>File Notes</h2>
<p>Edition details, formats, release notes, and purchase information.</p>
</div>
<div class="casefile-panel restricted-panel" data-tab="restricted">
<h2>Restricted</h2>
<p class="restricted-label">Authorised Access Only</p>
<p>Bonus archive material is locked.</p>
<a href="#buy-now" class="restricted-button">Unlock Access</a>
</div>
</div>
</div>
<div class="casefile-tabs">
<button type="button" class="casefile-tab active" data-tab="summary">Summary</button>
<button type="button" class="casefile-tab" data-tab="subjects">Subjects</button>
<button type="button" class="casefile-tab" data-tab="evidence">Evidence</button>
<button type="button" class="casefile-tab" data-tab="notes">File Notes</button>
<button type="button" class="casefile-tab restricted" data-tab="restricted">Restricted</button>
</div>
</div>
</div>
<div class="casefile-front" id="caseFileFront" role="button" tabindex="0" aria-label="Open case file">
<div class="casefile-front-face casefile-front-face-front">
<img src="/images/webp/folder-front.webp" alt="Case file cover" class="casefile-front-img" />
<div class="casefile-front-overlay">
<div class="casefile-stamp">
RESTRICTED
</div>
<div class="casefile-cover-text">
<div class="casefile-cover-kicker">WEST MIDLANDS POLICE</div>
<h1>ALPHA FLAME: RECKONING</h1>
<p>Case Ref: AFR-1983-06</p>
<p>Status: Active Investigation</p>
</div>
<div class="casefile-open-hint">Click to open file</div>
</div>
</div>
<div class="casefile-front-face casefile-front-face-back">
<img src="/images/webp/folder-front-inside.webp" alt="" class="casefile-front-img casefile-front-img-back" />
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
@section BackgroundVideo {
<div></div>
}
@section CSS {
<link href="~/css/reckoning.css" rel="stylesheet" />
}
@section Scripts {
<script>
document.addEventListener("DOMContentLoaded", function () {
const caseFile = document.getElementById("caseFile");
const frontCover = document.getElementById("caseFileFront");
const tabs = caseFile.querySelectorAll(".casefile-tab");
const panels = caseFile.querySelectorAll(".casefile-panel");
const images = caseFile.querySelectorAll("img");
let loadedCount = 0;
let assetsReady = false;
let openTimer = null;
let isOpening = false;
let hasEnteredView = false;
function openCaseFile() {
if (!assetsReady || isOpening || caseFile.classList.contains("open")) {
return;
}
isOpening = true;
caseFile.classList.add("revealed");
caseFile.classList.add("open");
if (openTimer) {
clearTimeout(openTimer);
openTimer = null;
}
}
function startAutoOpenTimer() {
if (!assetsReady || hasEnteredView === false || openTimer || caseFile.classList.contains("open")) {
return;
}
openTimer = setTimeout(() => {
openCaseFile();
}, 10000);
}
function onAssetLoaded() {
loadedCount++;
if (loadedCount === images.length) {
assetsReady = true;
caseFile.classList.add("assets-ready");
setTimeout(() => {
caseFile.classList.add("ready");
}, 250);
startAutoOpenTimer();
}
}
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && !hasEnteredView) {
hasEnteredView = true;
startAutoOpenTimer();
observer.disconnect();
}
});
}, {
threshold: 0.35
});
observer.observe(caseFile);
images.forEach(img => {
if (img.complete) {
onAssetLoaded();
} else {
img.addEventListener("load", onAssetLoaded, { once: true });
img.addEventListener("error", onAssetLoaded, { once: true });
}
});
frontCover.addEventListener("click", function () {
openCaseFile();
});
frontCover.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
openCaseFile();
}
});
tabs.forEach(tab => {
tab.addEventListener("click", function () {
const tabName = this.dataset.tab;
tabs.forEach(t => t.classList.remove("active"));
panels.forEach(p => p.classList.remove("active"));
this.classList.add("active");
const targetPanel = caseFile.querySelector(`.casefile-panel[data-tab="${tabName}"]`);
if (targetPanel) {
targetPanel.classList.add("active");
}
});
});
});
</script>
}

View File

@ -0,0 +1,287 @@
@model CatherineLynwood.Models.TitlePageViewModel
@{
var releaseDate = new DateTime(2026, 4, 1);
ViewData["Title"] = $"The Alpha Flame: Reckoning — Pre-Release (out {releaseDate: d MMMM yyyy})";
}
<div class="row">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
<li class="breadcrumb-item active" aria-current="page">Reckoning</li>
</ol>
</nav>
</div>
</div>
<!-- HERO: Cover + Coming Soon panel -->
<section class="mb-4">
<div class="row g-3 align-items-stretch">
<!-- Book Cover -->
<div class="col-lg-5 d-flex">
<div class="card character-card h-100 flex-fill" id="cover-card">
<responsive-image src="the-alpha-flame-reckoning-cover.png"
class="card-img-top"
alt="The Alpha Flame: Reckoning book cover"
display-width-percentage="50"></responsive-image>
<div class="card-body border-top border-3 border-dark">
<h3 class="card-title h5 mb-1">The Alpha Flame: <span class="fw-light">Reckoning</span></h3>
<p class="card-text mb-0">
Book 2 of the Alpha Flame trilogy.
The danger didnt end with <em>Discovery</em>. It got smarter.
</p>
</div>
</div>
</div>
<!-- Launch panel -->
<div class="col-lg-7 d-flex">
<div class="card character-card h-100 flex-fill" id="hero-media-card">
<div class="card-body d-flex flex-column">
<div>
<h3 class="card-title">The Alpha Flame: <span class="fw-light">Reckoning</span></h3>
<p class="fst-italic">
Two flames, once divided, now burn as one. Bound by blood, scarred by secrets, they rise to face the fire that made them.
</p>
<p>
After everything they survived in <em>Discovery</em>, Maggie and Beth should finally be free.
Instead, the nightmares follow them into daylight, and the past starts tugging at them with intent.
Beth is still rebuilding herself piece by piece. Maggie is trying to hold everything together.
And somewhere in the background, the people who benefitted from Beths silence are quietly noticing that she is no longer alone.
</p>
<hr class="my-4" />
<h4 class="mb-3"><i class="fa-solid fa-fire-flame-curved me-2"></i>The search for truth</h4>
<p>
The girls return to the fragments their mother left behind: the memory tin, the poem, and the wedding photograph.
What once felt like sentimental debris starts to look like a trail. A puzzle built on purpose.
Maggie, driven and sharp, refuses to accept that their mothers life ended without meaning.
Beth, fragile but braver than she realises, wants answers even if they hurt.
</p>
<p>
Their search pulls them toward Wales, toward names and places their mother never explained, and toward people who remember her
not as a ghost story, but as a living woman with fire in her bones.
The closer they get, the more they realise they are not the only ones searching.
</p>
<div class="alert alert-secondary mt-4" role="alert">
<div class="d-flex">
<div class="me-3">
<i class="fa-solid fa-triangle-exclamation"></i>
</div>
<div>
<strong>Things escalate fast.</strong> A familiar car appears. Old fear becomes new danger. Running stops being a metaphor.
</div>
</div>
</div>
</div>
<!-- Call to action row -->
<div class="d-flex gap-3 flex-wrap">
@* <a asp-controller="TheAlphaFlame" asp-action="MailingList" class="btn btn-dark">
Notify me on release
</a> *@
<a asp-controller="Discovery" asp-action="Index" class="btn btn-outline-dark">
Start with Discovery
</a>
<a asp-controller="TheAlphaFlame" asp-action="Index" class="btn btn-outline-secondary">
Explore the trilogy
</a>
</div>
<!-- Small continuity note -->
<div class="mt-3 small text-muted">
<strong>Tip:</strong> <em>Reckoning</em> follows directly after <em>Discovery</em>.
If youre new to the series, start there for maximum emotional damage.
</div>
@await Component.InvokeAsync("BuyPanel", new { ISO2 = Model.UserIso2, Src = Model.Src, Title = Model.Title })
</div>
</div>
</div>
</div>
</section>
<!-- Synopsis (pre-release: shorter, tighter) -->
<section id="synopsis" class="mb-4">
<div class="card character-card text-white"
style="background: url('/images/webp/bridge-over-the-river-960.webp'); background-position: center; background-size: cover;">
<div class="card-header">
<h2 class="card-title h1 mb-0">The Alpha Flame: <span class="fw-light">Reckoning</span></h2>
<p class="mb-0">When the past returns, it doesnt knock. It takes.</p>
</div>
<div class="card-body">
<h4 class="mb-3"><i class="fa-solid fa-car-burst me-2"></i>The watcher returns</h4>
<p class="card-text">
Back in Birmingham, the threat stops lurking and starts moving.
A blue Ford Escort appears outside a safe doorstep and turns a tense conversation into a race for survival.
The girls flee through back roads and service lanes, only to be hunted in the open, the Escort glued to their rear bumper like fate.
It is not random. It is not a coincidence. Someone has been sent.
</p>
<p class="card-text">
Worse still, the fallout reaches the police. Mrs Patterson, the neighbour who knows too much, turns up dead.
Graham arrives with questions he does not want to ask, and DI Baker arrives with the kind of presence that poisons a room.
The message is clear: even authority cannot be assumed safe.
</p>
<hr class="my-4" />
<h4 class="mb-3"><i class="fad fa-mountain me-2"></i>Wales, family, and the key</h4>
<p class="card-text">
Wales offers a temporary illusion of peace: wide skies, old farms, warm kitchens, and people who speak your mothers name
like she mattered. For the first time, Beth and Maggie experience something dangerously close to ordinary.
But the ordinary does not last.
</p>
<p class="card-text">
The poem changes. Literally. A second version appears, with new stanzas that reframe everything.
What the girls believed was a single trail is revealed as a deliberate split: one path for the wrong eyes,
another for the right ones. Their mother planned for predators. She built decoys.
And she hid the real direction in plain sight, waiting for the day her daughters would be ready to understand it.
</p>
<p class="card-text">
A deposit box key is found, tucked away where it was never supposed to be noticed.
Now the question is not whether their mother left something behind, but what she was protecting, and from whom.
</p>
<hr class="my-4" />
<h4 class="mb-3"><i class="fad fa-user-secret me-2"></i>Names that change everything</h4>
<p class="card-text">
Answers arrive in the worst way: through memory, confession, and people who were there when the damage began.
Gareth finally tells the story of Annie, Elen, and the night the trap was set.
He names the man at the centre of it all: <strong>Simon Jones</strong>.
Not a faceless monster, but a real figure with reach, money, and a network built on fear.
</p>
<p class="card-text">
The revelation detonates Beths fragile stability. The past is not behind her.
It is connected to everything still happening now, and to Sophies world in Birmingham.
The girls are not just searching for identity anymore.
They are standing on a fault line that runs straight through power, crime, and corruption.
</p>
<hr class="my-4" />
<h4 class="mb-3">The boxes open</h4>
<p class="card-text">
When the second deposit box finally opens, it is not comfort inside. It is evidence.
Their mother left them a ledger of real accounts, receipts, photos, recordings, and a copied key tied to Sophies safe.
It is a package designed to destroy Simon Jones, but it comes with a brutal catch:
the final identities, the protected names, the ones behind the coded initials, are stored elsewhere,
locked in Sophies personal safe in a red case.
</p>
<p class="card-text">
Their mother did not leave them a neat answer.
She left them a weapon, and a warning.
</p>
<hr class="my-4" />
<h4 class="mb-3"><i class="fad fa-heart me-2"></i>The reckoning hits home</h4>
<p class="card-text">
As the net tightens, the violence stops being distant. It becomes intimate.
During a final clash, Rob steps into the line of fire for Maggie.
In the silence of a ruined place, he bleeds out in her arms, and the future they were building collapses in seconds.
His death is not just loss. It is consequence.
It is the cost of standing beside someone the darkness wants back.
</p>
<p class="card-text">
Grief does not pause the danger. If anything, it sharpens it.
Maggie and Beth are no longer just survivors.
They are holding proof that could bring down an empire, and every powerful person connected to it.
</p>
<hr class="my-4" />
<h4 class="mb-3"><i class="fad fa-door-open me-2"></i>The knock</h4>
<p class="card-text">
The story closes with the world catching up.
The authorities circle. Bakers shadow stretches.
And just when it seems the girls might get a breath, duty arrives on the doorstep.
The past has claimed another body, and someone needs a suspect.
The final beat lands exactly as it should:
a knock that changes everything, and the unmistakable sense that what comes next will not be gentle.
</p>
<div class="mt-3 d-flex gap-2 flex-wrap">
<a asp-controller="Discovery" asp-action="Extras" class="btn btn-outline-light btn-sm">
Unlock Discovery extras
</a>
<a asp-controller="TheAlphaFlame" asp-action="Index" class="btn btn-outline-light btn-sm">
The trilogy overview
</a>
</div>
</div>
</div>
</section>
<!-- Pre-release: No fake previews / no empty reviews -->
@* <section class="mt-4">
<div class="card character-card">
<div class="card-body">
<h2 class="h5 fw-bold mb-2">Preview content</h2>
<p class="mb-3">
Previews, sample chapters, and reader reviews will appear here once theyre ready.
Until then, <em>Discovery</em> is available now.
</p>
<div class="d-flex gap-2 flex-wrap">
<a asp-controller="Discovery" asp-action="Index" class="btn btn-dark">Explore Discovery</a>
<a asp-controller="Discovery" asp-action="Reviews" class="btn btn-outline-dark">See Discovery reviews</a>
</div>
</div>
</div>
</section> *@
@section Scripts {
<!-- Plyr for audio (only needed if you keep the audio block) -->
<script>
const player = new Plyr('audio');
</script>
}
@section Meta {
<MetaTag meta-title="The Alpha Flame: Reckoning by Catherine Lynwood (Pre-Release)"
meta-description="Reckoning is the upcoming second book in The Alpha Flame trilogy. 1983 Birmingham. Consequences, secrets, and survival… Coming 1 April 2026."
meta-keywords="The Alpha Flame Reckoning, Catherine Lynwood, 1983 novel, Birmingham fiction, suspense fiction, family secrets, psychological crime, historical drama"
meta-author="Catherine Lynwood"
meta-url="https://www.catherinelynwood.com/the-alpha-flame/reckoning"
meta-image="https://www.catherinelynwood.com/images/webp/the-alpha-flame-reckoning-cover-1200.webp"
meta-image-png="https://www.catherinelynwood.com/images/the-alpha-flame-reckoning-cover.png"
meta-image-alt="The Alpha Flame: Reckoning by Catherine Lynwood"
og-site-name="Catherine Lynwood - The Alpha Flame: Reckoning"
article-published-time="@new DateTime(2026, 04, 01)"
article-modified-time="@DateTime.UtcNow"
twitter-card-type="summary_large_image"
twitter-site-handle="@@CathLynwood"
twitter-creator-handle="@@CathLynwood" />
}

View File

@ -0,0 +1,674 @@
@{
ViewData["Title"] = "Listen to The Alpha Flame: Discovery";
}
<div class="row">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Extras">Extras</a></li>
<li class="breadcrumb-item active" aria-current="page">Listen</li>
</ol>
</nav>
</div>
</div>
<div class="container">
<div class="row">
<div class="col">
<p class="h3 text-center" id="status">Loading...</p>
<ul class="pagination justify-content-center" id="chapter-list"></ul>
<audio id="audio" preload="none"></audio>
<div class="input-group border border-2 border-dark rounded-3">
<!-- Progress Bar (styled to look like an input) -->
<div class="form-control p-0" style="height: 40px;">
<div id="chapter-progress" class="progress rounded-end-0" style="height: 100%; margin: 0;">
<div id="chapter-progress-bar" class="progress-bar rounded-end-0" role="progressbar" style="width: 0%"></div>
<div id="chapter-ticks" class="d-none d-md-block"></div>
</div>
</div>
<!-- Play / Pause Button -->
<button id="toggle-play" class="btn btn-primary" style="height: 40px;">
<i class="fad fa-pause"></i>
</button>
</div>
<div class="mt-1 mb-2">
<div class="progress border border-2 border-dark rounded-3" style="height: 12px;">
<div id="buffer-progress-bar" class="progress-bar bg-primary" style="width: 0%"></div>
</div>
</div>
<div id="chapter-timestamp" class="my-2 small text-center"></div>
<div id="chapter-text-container" class="chapter-text-scroll">
<div id="chapter-text-content"></div>
</div>
</div>
</div>
</div>
@section Scripts {
<script>
const audioBlobCache = new Map(); // id => Blob
const audioBlobSizes = new Map(); // id => size in bytes
let totalBlobSize = 0;
const MAX_BLOB_CACHE_SIZE = 100 * 1024 * 1024; // 100 MB
const BUFFER_TARGET_SECONDS = 30 * 60; // 30 minutes
let masterPlaylist = [];
let chapterStartIndices = [];
let currentIndex = 0;
let preloadedAudio = null;
const audio = document.getElementById("audio");
const status = document.getElementById("status");
const toggleButton = document.getElementById("toggle-play");
// Load the playlist
fetch("/api/audio/playlist/all")
.then(res => res.json())
.then(data => {
// ✅ Sort chapters numerically by chapter number
data.sort((a, b) => {
const aNum = parseInt(a.chapter.match(/Chapter_(\d+)/i)?.[1] || "0", 10);
const bNum = parseInt(b.chapter.match(/Chapter_(\d+)/i)?.[1] || "0", 10);
return aNum - bNum;
});
let flatIndex = 0;
chapterStartIndices = []; // global or higher scoped variable
masterPlaylist = []; // reset in case of reload
data.forEach(chapter => {
chapterStartIndices.push({ index: flatIndex, name: chapter.chapter });
chapter.segments.forEach(segment => {
masterPlaylist.push({
id: segment.id,
display: segment.display,
chapter: chapter.chapter,
duration: parseDuration(segment.display),
text: segment.text || ""
});
flatIndex++;
});
});
// ✅ Determine saved/resume position
const savedIndex = parseInt(localStorage.getItem("lastAudioIndex") || "0", 10);
const currentSeg = masterPlaylist[savedIndex] || {};
const currentChapter = currentSeg.chapter || chapterStartIndices[0].name;
// ✅ Ensure pagination is rendered properly
renderChapterPagination(currentChapter);
status.textContent = `Ready ${masterPlaylist.length} segments total.`;
// ✅ Preload first segments of each chapter (for snappy chapter switches)
preloadFirstSegments();
// ✅ Preload 10 minutes of sequential audio starting from resume position or beginning
const preloadStart = (savedIndex >= 0 && savedIndex < masterPlaylist.length)
? savedIndex
: 0;
preloadSegmentsAhead(preloadStart, BUFFER_TARGET_SECONDS); // preload 10 mins ahead
// ✅ Resume if index is valid
if (savedIndex >= 0 && savedIndex < masterPlaylist.length) {
const seg = masterPlaylist[savedIndex];
renderChapterText(seg.chapter);
initChapterProgress(seg.chapter);
loadSegment(savedIndex);
}
});
function fetchBlobWithRetry(url, retries = 3, delayMs = 1000) {
return new Promise((resolve, reject) => {
const attempt = (n) => {
fetch(url)
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.blob();
})
.then(resolve)
.catch(err => {
if (n > 0) {
setTimeout(() => attempt(n - 1), delayMs);
} else {
console.warn(`Failed to fetch blob ${url}:`, err);
reject(err);
}
});
};
attempt(retries);
});
}
function renderChapterPagination(currentChapter) {
let listHtml = '';
const currentPos = chapterStartIndices.findIndex(c => c.name === currentChapter);
const totalChapters = chapterStartIndices.length;
// Dynamically determine window size based on screen width
const windowSize = getWindowSize();
const halfWindow = Math.floor(windowSize / 2);
// Previous button
listHtml += `<li class="page-item ${currentPos <= 0 ? 'disabled' : ''}">
<a class="page-link" href="#" onclick="goToPrevChapter(${currentPos})"><i class="fad fa-step-backward"></i></a>
</li>`;
const addPageItem = (i, label = null) => {
const chapterNumber = chapterStartIndices[i].name.match(/Chapter_(\d+)/i)?.[1] || "?";
const isActive = i === currentPos;
listHtml += `<li class="page-item ${isActive ? 'active' : ''}">
<a class="page-link" href="#" onclick="startFrom(${chapterStartIndices[i].index})">${label || chapterNumber}</a>
</li>`;
};
// First button
addPageItem(0);
// Ellipsis after first if needed
if (currentPos - halfWindow > 1) {
listHtml += `<li class="page-item disabled d-none d-md-block"><span class="page-link">…</span></li>`;
}
// Sliding window
const start = Math.max(1, currentPos - halfWindow);
const end = Math.min(totalChapters - 2, currentPos + halfWindow);
for (let i = start; i <= end; i++) {
if (i !== 0 && i !== totalChapters - 1) {
addPageItem(i);
}
}
// Ellipsis before last if needed
if (currentPos + halfWindow < totalChapters - 2) {
listHtml += `<li class="page-item disabled d-none d-md-block"><span class="page-link">…</span></li>`;
}
// Last button
if (totalChapters > 1) {
addPageItem(totalChapters - 1);
}
// Next button
listHtml += `<li class="page-item ${currentPos >= totalChapters - 1 ? 'disabled' : ''}">
<a class="page-link" href="#" onclick="goToNextChapter(${currentPos})"><i class="fad fa-step-forward"></i></a>
</li>`;
document.getElementById("chapter-list").innerHTML = listHtml;
}
function getWindowSize() {
const width = window.innerWidth;
if (width < 576) {
return 5; // Extra small (mobile)
} else if (width < 768) {
return 7; // Small (phones / small tablets)
} else if (width < 992) {
return 9; // Medium (tablets)
} else if (width < 1200) {
return 13; // Large (small desktops)
} else {
return 19; // Extra large (wide desktop screens)
}
}
function goToPrevChapter(currentPos) {
if (currentPos <= 0) return;
const newIndex = chapterStartIndices[currentPos - 1].index;
const newChapter = chapterStartIndices[currentPos - 1].name;
renderChapterPagination(newChapter);
startFrom(newIndex);
}
function goToNextChapter(currentPos) {
if (currentPos >= chapterStartIndices.length - 1) return;
const newIndex = chapterStartIndices[currentPos + 1].index;
const newChapter = chapterStartIndices[currentPos + 1].name;
renderChapterPagination(newChapter);
startFrom(newIndex);
}
function parseDuration(display) {
const parts = display.split(":").map(Number);
return parts[0] * 60 + (parts[1] || 0);
}
// Call this in startFrom()
function startFrom(index) {
const seg = masterPlaylist[index];
// Ensure autoplay works (helps with Chrome mobile restrictions)
audio.muted = false;
audio.autoplay = true;
renderChapterPagination(seg.chapter);
renderChapterText(seg.chapter);
initChapterProgress(seg.chapter);
loadSegment(index);
}
function loadSegment(index) {
if (index < 0 || index >= masterPlaylist.length) return;
currentIndex = index;
updateBufferBar(index);
localStorage.setItem("lastAudioIndex", index.toString());
const seg = masterPlaylist[index];
// ✅ Scroll and highlight current paragraph
document.querySelectorAll(".chapter-text-scroll p").forEach(p => p.classList.remove("active"));
const activePara = document.getElementById(`text-seg-${index}`);
if (activePara) {
activePara.classList.add("active");
const container = document.querySelector(".chapter-text-scroll");
const paraTop = activePara.offsetTop;
const paraHeight = activePara.offsetHeight;
const containerHeight = container.clientHeight;
const scrollTarget = paraTop - container.offsetTop - (containerHeight / 2) + (paraHeight / 2);
container.scrollTo({ top: scrollTarget, behavior: "smooth" });
}
const tryPlay = async (blob, retryCount = 0) => {
try {
const blobUrl = URL.createObjectURL(blob);
audio.src = blobUrl;
await audio.play();
status.textContent = `Playing: ${seg.chapter.replace(/_/g, ' ')}`;
toggleButton.innerHTML = '<i class="fad fa-pause"></i>';
} catch (err) {
if (retryCount < 10) {
console.warn(`Retrying audio playback (${retryCount + 1})...`);
setTimeout(() => tryPlay(blob, retryCount + 1), 3000);
} else {
console.error("Playback failed permanently:", err);
status.textContent = "Error loading audio segment.";
toggleButton.innerHTML = '<i class="fad fa-play"></i>';
}
}
};
const fetchAndPlay = async () => {
try {
const response = await fetchWithRetry(`/api/audio/token-url/${seg.id}`, 5, 2000);
const blob = await fetch(response).then(r => r.blob());
audioBlobCache.set(seg.id, blob);
tryPlay(blob);
} catch (err) {
console.error("Initial fetch failed, will retry in background:", err);
status.textContent = "Waiting for signal...";
let retries = 0;
const maxRetries = 12;
const retryLoop = setInterval(async () => {
try {
const response = await fetchWithRetry(`/api/audio/token-url/${seg.id}`, 3, 2000);
const blob = await fetch(response).then(r => r.blob());
clearInterval(retryLoop);
audioBlobCache.set(seg.id, blob);
tryPlay(blob);
} catch (err) {
retries++;
console.warn(`Retry ${retries}/${maxRetries} failed`);
if (retries >= maxRetries) {
clearInterval(retryLoop);
status.textContent = "Unable to load segment.";
}
}
}, 10000);
}
};
if (audioBlobCache.has(seg.id)) {
tryPlay(audioBlobCache.get(seg.id));
} else {
fetchAndPlay();
}
maybeTriggerPreload();
}
function maybeTriggerPreload() {
if (audio.paused) return; // only preload while playing
const bufferedPercent = getBufferedPercent(currentIndex);
if (bufferedPercent < 90) {
preloadSegmentsAhead(currentIndex + 1, BUFFER_TARGET_SECONDS);
}
}
function getBufferedPercent(currentIdx) {
let bufferedSeconds = 0;
let totalChecked = 0;
let i = currentIdx + 1;
while (i < masterPlaylist.length && totalChecked < BUFFER_TARGET_SECONDS) {
const seg = masterPlaylist[i];
totalChecked += seg.duration;
if (audioBlobCache.has(seg.id)) {
bufferedSeconds += seg.duration;
}
i++;
}
return Math.min((bufferedSeconds / BUFFER_TARGET_SECONDS) * 100, 100);
}
function preloadSegmentsAhead(startIndex, targetSeconds) {
let totalDuration = 0;
let i = startIndex;
while (i < masterPlaylist.length && totalDuration < targetSeconds) {
const seg = masterPlaylist[i];
if (!audioBlobCache.has(seg.id)) {
fetch(`/api/audio/token-url/${seg.id}`)
.then(res => res.text())
.then(url => fetchBlobWithRetry(url))
.then(blob => {
const blobSize = blob.size;
audioBlobCache.set(seg.id, blob);
audioBlobSizes.set(seg.id, blobSize);
totalBlobSize += blobSize;
enforceBlobCacheLimit();
updateBufferBar(currentIndex);
})
.catch(err => console.warn("Preload failed:", err));
}
totalDuration += seg.duration;
i++;
}
}
function fetchWithRetry(url, retries = 3, delayMs = 1000) {
return new Promise((resolve, reject) => {
const attempt = (n) => {
fetch(url)
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.text();
})
.then(resolve)
.catch(err => {
if (n > 0) {
setTimeout(() => attempt(n - 1), delayMs);
} else {
console.warn(`Failed to fetch ${url} after retries:`, err);
reject(err);
}
});
};
attempt(retries);
});
}
function renderChapterText(chapterName) {
const container = document.getElementById("chapter-text-content");
container.innerHTML = "";
masterPlaylist.forEach((seg, i) => {
if (seg.chapter === chapterName) {
const p = document.createElement("p");
p.textContent = seg.text || "";
p.id = `text-seg-${i}`;
container.appendChild(p);
}
});
}
let currentChapter = "";
let chapterSegments = [];
let chapterTotalDuration = 0;
function initChapterProgress(chapterName) {
currentChapter = chapterName;
chapterSegments = masterPlaylist.filter(s => s.chapter === chapterName);
chapterTotalDuration = chapterSegments.reduce((sum, seg) => sum + seg.duration, 0);
updateProgressUI(0);
renderChapterTicks(); // ✅ Added here
}
function updateProgressUI(elapsedSeconds) {
const percent = (elapsedSeconds / chapterTotalDuration) * 100;
document.getElementById("chapter-progress-bar").style.width = `${percent}%`;
const mins = Math.floor(elapsedSeconds / 60).toString().padStart(2, "0");
const secs = Math.floor(elapsedSeconds % 60).toString().padStart(2, "0");
const totalMins = Math.floor(chapterTotalDuration / 60).toString().padStart(2, "0");
const totalSecs = Math.floor(chapterTotalDuration % 60).toString().padStart(2, "0");
document.getElementById("chapter-timestamp").textContent = `${mins}:${secs} / ${totalMins}:${totalSecs}`;
}
function getElapsedInChapter(currentIdx, audioCurrentTime) {
let sum = 0;
for (let i = 0; i < currentIdx; i++) {
if (masterPlaylist[i].chapter === currentChapter) {
sum += masterPlaylist[i].duration;
}
}
return sum + audioCurrentTime;
}
function renderChapterTicks() {
const ticksContainer = document.getElementById("chapter-ticks");
ticksContainer.innerHTML = "";
let elapsed = 0;
chapterSegments.forEach(seg => {
const percent = (elapsed / chapterTotalDuration) * 100;
const tick = document.createElement("div");
tick.className = "chapter-tick";
tick.style.left = `${percent}%`;
ticksContainer.appendChild(tick);
// const label = document.createElement("div");
// label.className = "chapter-tick-label";
// label.style.left = `${percent}%`;
// label.textContent = seg.display;
// ticksContainer.appendChild(label);
elapsed += seg.duration;
});
}
function updatePaginationHighlight(currentChapter) {
document.querySelectorAll("#chapter-list .page-item").forEach((li, idx) => {
const chapterNum = parseInt(li.textContent);
const currentNum = parseInt(currentChapter.match(/Chapter_(\d+)/i)?.[1]);
if (chapterNum === currentNum) {
li.classList.add("active");
} else {
li.classList.remove("active");
}
});
}
function preloadFirstSegments() {
chapterStartIndices.forEach(({ index }) => {
const seg = masterPlaylist[index];
if (!audioBlobCache.has(seg.id)) {
fetch(`/api/audio/token-url/${seg.id}`)
.then(res => res.text())
.then(url => fetchBlobWithRetry(url))
.then(blob => {
const blobSize = blob.size;
audioBlobCache.set(seg.id, blob);
audioBlobSizes.set(seg.id, blobSize);
totalBlobSize += blobSize;
enforceBlobCacheLimit();
updateBufferBar(currentIndex);
})
.catch(err => console.warn(`Preload of chapter start failed (${seg.id}):`, err));
}
});
}
function enforceBlobCacheLimit() {
const iterator = audioBlobCache.keys();
while (totalBlobSize > MAX_BLOB_CACHE_SIZE && audioBlobCache.size > 0) {
const oldestKey = iterator.next().value;
const size = audioBlobSizes.get(oldestKey) || 0;
audioBlobCache.delete(oldestKey);
audioBlobSizes.delete(oldestKey);
totalBlobSize -= size;
console.log(`Evicted ${oldestKey} to maintain cache size. Remaining size: ${totalBlobSize} bytes.`);
}
}
function updateBufferBar(currentIdx) {
let bufferedSeconds = 0;
let totalChecked = 0;
let i = currentIdx + 1;
while (i < masterPlaylist.length && totalChecked < BUFFER_TARGET_SECONDS) {
const seg = masterPlaylist[i];
totalChecked += seg.duration;
if (audioBlobCache.has(seg.id)) {
bufferedSeconds += seg.duration;
}
i++;
}
const percent = Math.min((bufferedSeconds / BUFFER_TARGET_SECONDS) * 100, 100);
const bar = document.getElementById("buffer-progress-bar");
bar.style.width = `${percent.toFixed(1)}%`;
bar.setAttribute("aria-valuenow", percent.toFixed(1));
// ✅ Set Bootstrap colour classes
bar.classList.remove("bg-primary", "bg-warning", "bg-danger");
if (percent >= 75) {
bar.classList.add("bg-primary");
} else if (percent >= 30) {
bar.classList.add("bg-warning");
} else {
bar.classList.add("bg-danger");
}
}
audio.addEventListener("ended", () => {
if (currentIndex < masterPlaylist.length - 1) {
const nextIndex = currentIndex + 1;
const nextSeg = masterPlaylist[nextIndex];
const currentChapter = masterPlaylist[currentIndex].chapter;
const nextChapter = nextSeg.chapter;
if (nextChapter !== currentChapter) {
renderChapterPagination(nextChapter); // ✅ Update pagination
renderChapterText(nextChapter);
initChapterProgress(nextChapter);
}
loadSegment(nextIndex);
} else {
status.textContent = "Playback complete.";
}
});
// Update during playback
audio.addEventListener("timeupdate", () => {
if (!chapterSegments.length || !audio.duration) return;
const elapsed = getElapsedInChapter(currentIndex, audio.currentTime);
updateProgressUI(elapsed);
});
toggleButton.addEventListener("click", () => {
if (audio.paused) {
audio.play();
toggleButton.innerHTML = '<i class="fad fa-pause"></i>';
} else {
audio.pause();
toggleButton.innerHTML = '<i class="fad fa-play"></i>';
}
});
audio.addEventListener("play", () => {
toggleButton.innerHTML = '<i class="fad fa-pause"></i>';
});
audio.addEventListener("pause", () => {
toggleButton.innerHTML = '<i class="fad fa-play"></i>';
});
// ✅ These are additional listeners — do not replace existing ones
audio.addEventListener("play", maybeTriggerPreload);
audio.addEventListener("ended", maybeTriggerPreload);
document.getElementById("chapter-progress").addEventListener("click", (e) => {
const container = e.currentTarget;
const rect = container.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const clickPercent = clickX / rect.width;
const targetTime = clickPercent * chapterTotalDuration;
// Find approximate segment
let cumulative = 0;
for (let i = 0; i < chapterSegments.length; i++) {
const seg = chapterSegments[i];
if (cumulative + seg.duration >= targetTime) {
const globalIndex = masterPlaylist.findIndex(s => s.id === seg.id);
if (globalIndex >= 0) loadSegment(globalIndex);
break;
}
cumulative += seg.duration;
}
});
</script>
}

View File

@ -0,0 +1,375 @@
@{
ViewData["Title"] = "The Alpha Flame: Discovery Maggie's Designs";
}
<div class="row">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Extras">Extras</a></li>
<li class="breadcrumb-item active" aria-current="page">Maggie's Designs</li>
</ol>
</nav>
</div>
</div>
<div class="container">
<h1 class="mb-5 text-center">Maggie's Designs</h1>
<p>
I love fashion and I trued to weave that into Maggie's charactger. Helped by AI I really enjoyed coming up with outfits
for Maggie to wear in the various scenes of the book. Lots were so far out there that they didn't make it to the final edit,
or the descriptions were so detailed that you've had nodded off.
</p>
<p>
That said, no matter what the outfit was I found working from a visual prompt enable me to write more vividly about it.
</p>
<!-- Section 1 -->
<div class="scrapbook-section">
<div class="scrapbook-image rotate-1">
<responsive-image src="maggie-fashion-6.png" class="img-fluid" alt="The Elle Dress" display-width-percentage="50"></responsive-image>
<blockquote class="text-black" style="font-style: italic; margin-top: 1em;">
“She didnt dress to impress. She dressed to become, a force wrapped in colour, stitched in self-belief.”
</blockquote>
</div>
<div class="scrapbook-text">
<h5>“The Elle Dress” Fully Formed</h5>
<p>This is it, the dress that stopped hearts and turned pages. The one Maggie wore for *Elle*, unapologetic and unforgettable. Theres nothing shy about this piece. Its all precision and provocation; a sculpted plunge, a defiant cut-out, a hemline that dances between lingerie and high fashion. Every detail speaks Maggies language, fire without fuss, beauty without permission. She didnt just wear this dress… she claimed it. And in doing so, she claimed herself.</p>
<p>The deep teal shimmered like danger in low light, somewhere between mermaid and menace. I remember writing the scene and thinking, *this isnt about being sexy, its about being seen*. This dress is armour. Soft in texture, sharp in purpose. The fishnet tights? That was Maggies touch. A reminder that no matter how glossy the set, she brought her own edge with her. Always.</p>
<p>There may be another version of this dress later, refined, reimagined. But this one? This is the original. The raw truth. The moment the world turned to look… and Maggie didnt flinch.</p>
</div>
</div>
<!-- Section 2 -->
<div class="scrapbook-section flex-row-reverse">
<div class="scrapbook-image rotate-2">
<responsive-image src="maggie-fashion-12.png" class="img-fluid" alt="Forest Siren" display-width-percentage="50"></responsive-image>
<blockquote class="text-black" style="font-style: italic; margin-top: 1em;">
“She didnt rise from ashes. She bloomed from the soil, raw, rooted, and wild beyond taming.”
</blockquote>
</div>
<div class="scrapbook-text">
<h5>“Forest Siren” The Evolution of Elle</h5>
<p>This is what happens when a woman finds her voice, when the flame that once flickered is now fully ablaze, controlled, focused, and utterly arresting. This dress is the next chapter after the *Elle* shoot, not just an outfit, but a statement. A transformation. A whispered prophecy realised. The original was power through seduction… but this? This is power through presence.</p>
<p>I imagined Maggie walking barefoot across stone in this. Not for a crowd. Not even for the camera. Just for herself. The soft, trailing green, almost moss-like in motion, conjures wild things and whispered rebellions. The floral lace still clings, but it no longer begs to be noticed. It simply exists, unafraid, untamed. The high slit and fishnets remain, a signature. A nod to the girl who fought to survive. But the gown? That belongs to the woman shes become.</p>
<p>Theres a reason this version was never photographed for the magazine. It wasnt fashion anymore. It was folklore. A garment you glimpse once and never forget. And like Maggie, it doesnt ask you to understand it. It only asks that you remember.</p>
</div>
</div>
<!-- Section 3 -->
<div class="scrapbook-section">
<div class="scrapbook-image rotate-3">
<responsive-image src="maggie-fashion-3.png" class="img-fluid" alt="Ivory Fire" display-width-percentage="50"></responsive-image>
<blockquote class="text-black" style="font-style: italic; margin-top: 1em;">
“She didnt wear it to be wanted. She wore it to be undeniable.”
</blockquote>
</div>
<div class="scrapbook-text">
<h5>“Ivory Fire” The One That Smouldered Off-Page</h5>
<p>This dress never appeared in the book, but it haunted the edges of it. I imagined Maggie in it once, just once, and the image never left. It was lace, yes, but not gentle. The kind of lace that clung like memory and cut like truth. It wasnt designed to cover; it was designed to challenge. Petals traced over her chest like secrets too dangerous to speak aloud, their edges dipping into skin with a kind of deliberate precision that made it impossible to ignore her.</p>
<p>She didnt wear this to seduce. She wore it to reclaim. I pictured her standing still in it while the world moved around her, daring it to catch up. The neckline plunged, but it was her gaze that undressed the room. In the end, it was too much for the scene I was writing, too sharp, too strong, too unforgettable. So I left it behind. But like so much of Maggie, it lived on in the shadows between pages. Not forgotten. Just waiting.</p>
</div>
</div>
<!-- Section 4 -->
<div class="scrapbook-section flex-row-reverse">
<div class="scrapbook-image rotate-4">
<responsive-image src="maggie-fashion-4.png" class="img-fluid" alt="Ember Veins" display-width-percentage="50"></responsive-image>
<blockquote class="text-black" style="font-style: italic; margin-top: 1em;">
“It burned beneath the fabric, not just colour, but something molten and unspoken. She was the match and the flame.”
</blockquote>
</div>
<div class="scrapbook-text">
<h5>“Ember Veins” The Prototype</h5>
<p>This was the dress that nearly made it, an early vision of what would eventually evolve into Maggies infamous jumpsuit in Chapter 7. I called it *Ember Veins* because the pattern reminded me of fire trapped under silk, alive just beneath the surface. The fabric moves like its breathing. Every flick of orange, red, and teal threads through the design like molten emotion stitched into form. Its bold, unapologetic… and yet, it never quite felt like the right fit for the scene. Too regal. Too statuesque. Not enough of Maggies street-born swagger.</p>
<p>But the essence is here. The deep plunge that dares the room to judge her. The sculpted waist that declares power, not permission. And the colours, oh, the colours. They felt like Maggies moods during that chapter: blazing, unpredictable, and impossible to ignore. This wasnt something she would wear to blend in; it was what shed wear to set a room on fire.</p>
<p>Ultimately, the jumpsuit won, it had grit, mobility, and a whisper of rebellion. But *Ember Veins* still has a place in Maggies story. Not on the page, perhaps… but in the margins, smouldering quietly, waiting to be remembered.</p>
</div>
</div>
<!-- Section 5 -->
<div class="scrapbook-section">
<div class="scrapbook-image rotate-1">
<responsive-image src="maggie-fashion-5.png" class="img-fluid" alt="Midnight Teal" display-width-percentage="50"></responsive-image>
<blockquote class="text-black" style="font-style: italic; margin-top: 1em;">
“It wasnt about seduction. It was about control, about saying, You can look… but you dont get to touch.’”
</blockquote>
</div>
<div class="scrapbook-text">
<h5>“Midnight Teal” The Dress Before Elle</h5>
<p>Before Maggie stunned the world in the pages of *Elle*, there was this. A raw, electric concept that pulsed with possibility. I called it *Midnight Teal*, a piece that sat somewhere between lingerie and defiance, stitched not for comfort but for confrontation. This was Maggie untamed, unfiltered, unapologetically herself. It wasnt designed for the high street or a Paris runway… it was born for shadows and stares, for flickering candlelight and whispered thoughts.</p>
<p>The lace, tangled like secrets, reveals more than it hides. The fishnets ground her in the kind of grit only Maggie could carry. And that choker? A black ribbon that says “you can look, but you dont get to own.” I always imagined her wearing this not to seduce, but to reclaim. Not to tease, but to dare. This wasnt about being pretty. It was about being powerful.</p>
<p>Ultimately, it was too much for the magazine shoot. Elle needed elegance. This was rebellion. But I keep it here, because it mattered. Because somewhere in Maggies soul, this dress still lives, wild, sensual, fearless. A dress not worn for an audience, but for herself.</p>
</div>
</div>
<!-- Section 6 -->
<div class="scrapbook-section flex-row-reverse">
<div class="scrapbook-image rotate-2">
<responsive-image src="maggie-fashion-7.png" class="img-fluid" alt="Sleek Intentions" display-width-percentage="50"></responsive-image>
<blockquote class="text-black" style="font-style: italic; margin-top: 1em;">
“I can be soft. But dont mistake me for breakable.”
</blockquote>
</div>
<div class="scrapbook-text">
<h5>“Sleek Intentions” The Outfit That Almost Was</h5>
<p>Before the white two-piece took its place in Chapter 15, this was the look. A vision in midnight black, bold, sculpted, unforgiving. I imagined Maggie in this as a weapon disguised as elegance. The high neck and sheer sleeves gave it structure, control… but the body-hugging lines spoke of something else entirely. Power. Restraint. And maybe a little hunger too. She wasnt dressing for flirtation; she was dressing for impact.</p>
<p>This was meant to be her first appearance at Ricardos, a table full of family, wine, and a quiet undercurrent of testing the waters. But the outfit changed because the tone did. White softened the edges. A two-piece gave her room to move, to breathe, to step into that moment with grace rather than dominance. And yet I still love this version. It shows the side of Maggie that doesnt compromise, the girl who grew up armoured in silence and attitude.</p>
<p>*Sleek Intentions* never made it to the page, but it belonged to the story all the same. The decision not to wear it says as much as if she had. Sometimes, power is in the pivot, in choosing softness when the world expects sharp.</p>
</div>
</div>
<!-- Section 1 -->
<div class="scrapbook-section">
<div class="scrapbook-image rotate-1">
<responsive-image src="maggie-fashion-8.png" class="img-fluid" alt="Where the Poppies Burn" display-width-percentage="50"></responsive-image>
<blockquote class="text-black" style="font-style: italic; margin-top: 1em;">
“She didnt escape the fire. She walked through it, and the flowers grew behind her.”
</blockquote>
</div>
<div class="scrapbook-text">
<h5>“Where the Poppies Burn” A Dream of Freedom</h5>
<p>This one was never meant for the story itself, not directly. It was more of a whisper behind the writing. A vision I carried with me in quiet moments: Maggie, walking barefoot through a field of fire-tipped poppies, the world golden and glowing around her. Shes not looking back. She doesnt need to. Whatever held her is gone. Whatever comes next is hers to decide.</p>
<p>The dress is barely there, a gauze of lace and suggestion, soft as breath, flowing like memory. Its not about seduction, not here. Its about shedding. About choosing vulnerability in a world that demanded armour. Her hair is wild, her steps silent, and the light clings to her like it knows shes survived something most never would.</p>
<p>I once considered using this for the cover. It would have been unconventional, maybe too symbolic, but it captured a truth. Not about what Maggie wears, or where she walks, but who she is when no one is watching. This image was never part of the story on the page… but its part of the soul underneath it.</p>
</div>
</div>
<!-- Section 2 -->
<div class="scrapbook-section flex-row-reverse">
<div class="scrapbook-image rotate-2">
<responsive-image src="maggie-fashion-9.png" class="img-fluid" alt="Verdant Fury" display-width-percentage="50"></responsive-image>
<blockquote class="text-black" style="font-style: italic; margin-top: 1em;">
“You dont rise from fire without learning how to burn.”
</blockquote>
</div>
<div class="scrapbook-text">
<h5>“Verdant Fury” The Goddess Unleashed</h5>
<p>This was never meant to be subtle. This was me asking, *what if Maggie didnt just survive… what if she ruled?* What if all the hurt, all the hunger, all the fire she'd kept bottled up was no longer something she hid, but something she wore? The result was this: *Verdant Fury*. A vision in deep emerald and gold, clinging to her like ivy laced with flame.</p>
<p>Theres a mythic quality to this look, part forest queen, part fallen angel, all defiance. The sculpted bodice doesnt just highlight her form, it *armours* it. The gloves, shredded and clinging, feel like echoes of a battle already won. She stands here not as a girl escaping the past, but as a woman whos scorched the path behind her. And those eyes, theyre not asking for permission. Theyre issuing a challenge.</p>
<p>This outfit never had a scene. It was too much to contain. Too electric, too dangerous. But it lives in the spirit of Maggie all the same. In the moments where she turns, chin lifted, and dares the world to tell her she cant. Its not fashion. Its a reckoning draped in green.</p>
</div>
</div>
<!-- Section 3 -->
<div class="scrapbook-section">
<div class="scrapbook-image rotate-3">
<responsive-image src="maggie-fashion-10.png" class="img-fluid" alt="Unveiling Desires" display-width-percentage="50"></responsive-image>
<blockquote class="text-black" style="font-style: italic; margin-top: 1em;">
“She wore it like a dare, and Zoe answered with a kiss.”
</blockquote>
</div>
<div class="scrapbook-text">
<h5>“Unveiling Desires” The Painted Flame Jumpsuit</h5>
<p>This was never a maybe. This jumpsuit was destined to be worn. From the moment I imagined Maggie walking into that nightclub on New Years Eve, I saw her in this, a riot of colour, molten silk clinging to her with intent. Gold, vermilion, violet, turquoise… each hue brushed like fire across fabric, alive with movement, bold without apology. She didnt just wear it, she ignited it.</p>
<p>Everything about it was deliberate. The plunging neckline, the fitted waist, the shimmer that caught the light every time she turned, it was a siren song, yes, but also a declaration. She made it herself, of course. Maggies talent always came from that raw place inside her, where fire met finesse. And in this look, her artistry wasnt just visible, it was undeniable.</p>
<p>The jumpsuit became more than fashion. It was foreplay. Power. The spark that lit the fuse between her and Zoe, making their chemistry explosive and immediate. This outfit had presence. It was the flame before the kiss, the stroke before the sigh. And it belongs in this scrapbook because it didnt just make it into the book, it helped define it.</p>
</div>
</div>
<!-- Section 4 -->
<div class="scrapbook-section flex-row-reverse">
<div class="scrapbook-image rotate-4">
<responsive-image src="maggie-fashion-11.png" class="img-fluid" alt="Whispers in Lace" display-width-percentage="50"></responsive-image>
<blockquote class="text-black" style="font-style: italic; margin-top: 1em;">
“She didnt wear it… but for a moment, she almost did.”
</blockquote>
</div>
<div class="scrapbook-text">
<h5>“Whispers in Lace” The Dress That Was Too Much</h5>
<p>This gown was a daydream. An experiment in elegance. I imagined Maggie twirling through Ricardos grand hallway, all lace and light, every step sweeping the floor like she was born to haunt ballrooms. But as soon as I saw it fully realised, I knew, this wasnt her moment for that. This dress belonged to a different story. One where Maggie danced, yes, but not in a world as grounded as hers. It was too refined, too ethereal, too… not quite right.</p>
<p>Still, theres something about it that I loved. The off-shoulder cut, the illusion of fragility, the way it billowed with every motion. It captured the romantic part of Maggie that often hides behind her fire, the dreamer, the artist, the girl who still, despite everything, believes in beauty. But I needed her to walk into Ricardos with strength, not softness. Confidence, not fantasy. This was grace when what I needed was poise with a bit of punch.</p>
<p>So the dress stayed in the wings. It never made it onto the page. But like many of Maggies almost-moments, it still deserves to be seen. Because sometimes, even the wrong outfit tells us something honest about who she is underneath it all.</p>
</div>
</div>
<!-- Section 5 -->
<div class="scrapbook-section">
<div class="scrapbook-image rotate-1">
<responsive-image src="maggie-fashion-2.png" class="img-fluid" alt="Lace & Lager" display-width-percentage="50"></responsive-image>
<blockquote class="text-black" style="font-style: italic; margin-top: 1em;">
“She wore hope in the shape of a dress. And for a moment, she almost believed it would be enough.”
</blockquote>
</div>
<div class="scrapbook-text">
<h5>“Lace & Lager” The First Date Dress</h5>
<p>This was the one. The dress Maggie wore the night she finally let herself hope for something real, and got two pints of beer tipped over her for the trouble. I designed it around contradiction: soft lace sleeves clinging like whispered promises, paired with a defiant red skirt that billowed like a dare. It wasnt subtle. It wasnt supposed to be. It was Maggie stepping into the world not as someone surviving, but as someone choosing to be seen.</p>
<p>The top was reworked from a more elegant concept, too much for Ricardos, but perfect once grounded by the rough texture and boldness of that crumpled scarlet skirt. I loved that about her. How she blended grace with grit. The result was vulnerable and fearless all at once, which is probably why the moment hurt so much when it all went wrong.</p>
<p>She looked stunning that night. I remember writing that scene with a tightness in my chest, knowing exactly how much that outfit meant to her, not just as a creation, but as a risk. And seeing it ruined, soaked in lager and shame, broke my heart. But that was the point. This dress, like Maggie, deserved better. And eventually… she gets it.</p>
</div>
</div>
<!-- Section 6 -->
<div class="scrapbook-section flex-row-reverse">
<div class="scrapbook-image rotate-2">
<responsive-image src="maggie-fashion-13.png" class="img-fluid" alt="Velvet Resolve" display-width-percentage="50"></responsive-image>
<blockquote class="text-black" style="font-style: italic; margin-top: 1em;">
“It made her feel powerful… but not like herself.”
</blockquote>
</div>
<div class="scrapbook-text">
<h5>“Velvet Resolve” The One That Stayed Behind</h5>
<p>This was a contender, a serious one. There was a moment when I imagined Maggie standing at the top of the stairs in this, every line of the dress perfectly composed, every eye in the restaurant turning to look. The plunging black velvet bodice, the way it folded into that crimson skirt… it was elegance incarnate. Mature. Commanding. And, in the end, just a little too much.</p>
<p>Because Maggie, for all her power, wasnt trying to impress that night, not with poise. She wanted to feel beautiful, yes, but she also wanted to feel *real*. To be herself, raw edges and all. This dress was breathtaking, but it was armour. And what she needed then was something that breathed with her, not something that held her still.</p>
<p>But I still keep it here, in the scrapbook. Because this was the version of Maggie who might have walked into that meal pretending she wasnt scared. The one who hid every bruise behind glamour. It didnt make it to the page, but it came very close.</p>
</div>
</div>
<!-- Section 5 -->
<div class="scrapbook-section">
<div class="scrapbook-image rotate-1">
<responsive-image src="maggie-fashion.png" class="img-fluid" alt="Threadbare Ghost" display-width-percentage="50"></responsive-image>
<blockquote class="text-black" style="font-style: italic; margin-top: 1em;">
“She didnt need a scene to wear this. Just a window, and a reason to breathe.”
</blockquote>
</div>
<div class="scrapbook-text">
<h5>“Threadbare Ghosts” The Dress Without a Scene</h5>
<p>This one never had a chapter. No nightclub. No first date. No dramatic spill or kiss behind a curtain. It wasnt made for anything, and maybe thats why I love it. Because sometimes, Maggie just exists… not as a character in motion, but as a feeling. A breath. A girl wrapped in sunlight and silence, not trying to fight or impress or survive, just being. And this is what that moment looked like in my head.</p>
<p>The bodice is intricate, almost antique, like something stolen from a forgotten theatre. Lace, delicate and curling like memory. Faint threads of rust red and soft bone hues bleeding into the fabric, as if it once knew passion and never quite let go. She looks like something out of time. Something haunting and utterly alive. And thats the magic of it, she doesnt need to move to hold you. She just needs to look up, like this, and youre caught.</p>
<p>I never found the right place to write this dress in. But I never let it go, either. It stayed on my desk, on a scrap of paper, next to notes that never became dialogue. Because some images dont need stories. They *are* the story… just quietly, beautifully, waiting.</p>
</div>
</div>
<!-- Section 6 -->
<div class="scrapbook-section flex-row-reverse">
<div class="scrapbook-image rotate-2">
<responsive-image src="maggie-fashion-14.png" class="img-fluid" alt="Summer Soft" display-width-percentage="50"></responsive-image>
<blockquote class="text-black" style="font-style: italic; margin-top: 1em;">
“She wore it first. Beth made it hers. But what stitched them together was never fabric.”
</blockquote>
</div>
<div class="scrapbook-text">
<h5>“Summer Soft” The Outfit They Shared</h5>
<p>This outfit was never meant to be a showstopper. It wasnt fire or lace or velvet. It was light, deliberately so. The white cotton skirt, simple and sun-washed, the top just delicate enough to feel like a whisper. This was the first outfit Maggie wore to Ricardos, when she needed to feel both presentable and herself. It wasnt designed to turn heads… and yet it did, quietly, effortlessly. She wore it with that rare kind of grace that doesnt try, and so becomes unforgettable.</p>
<p>Later, she lent it to Beth, and thats when it truly earned its place in the story. Because clothes carry energy. They hold memory. And in that moment, Maggie wasnt just lending an outfit. She was offering safety, trust, sisterhood before either of them even knew the word for it. Beth, wrapped in Maggies confidence, stepping into her own space, her own choices, it made me cry when I wrote it. Still does.</p>
<p>Its not the most elaborate design in the scrapbook, not by a long shot. But maybe thats what makes it so important. Because sometimes, what matters most isnt how it looks… but who it becomes part of.</p>
</div>
</div>
</div>
@section Meta {
<style>
.scrapbook-section {
margin-bottom: 4rem;
display: flex;
flex-wrap: wrap;
align-items: center;
}
.scrapbook-image {
background: white;
padding: 10px 10px 30px 10px;
box-shadow: 2px 4px 12px rgba(0, 0, 0, 0.15);
border: 1px solid #ccc;
width: 100%;
max-width: 300px;
margin: 1rem;
transform: rotate(var(--angle));
}
.scrapbook-text {
flex: 1;
min-width: 250px;
margin: 1rem;
}
.rotate-1 {
--angle: -3deg;
}
.rotate-2 {
--angle: 2deg;
}
.rotate-3 {
--angle: -2deg;
}
.rotate-4 {
--angle: 3deg;
}
@@media (max-width: 768px) {
.scrapbook-section {
flex-direction: column;
}
.scrapbook-image {
margin-bottom: 1rem;
}
}
</style>
}

View File

@ -0,0 +1,101 @@
@model CatherineLynwood.Models.Reviews
@{
ViewData["Title"] = "Reader Reviews The Alpha Flame: Discovery";
}
<div class="container my-5">
<div class="row">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
<li class="breadcrumb-item active" aria-current="page">Reviews</li>
</ol>
</nav>
</div>
</div>
<div class="row justify-content-center">
<div class="col-lg-10">
<section class="mb-5 text-center">
<h1 class="display-5 fw-bold">Reader Reviews</h1>
<p class="lead">Heres what readers are saying about <em>The Alpha Flame: Discovery</em>. If youve read the book, wed love for you to share your thoughts too.</p>
</section>
<section class="reader-reviews">
@if (Model?.Items?.Any() == true)
{
foreach (var review in Model.Items)
{
var fullStars = (int)Math.Floor(review.RatingValue);
var hasHalfStar = review.RatingValue - fullStars >= 0.5;
var emptyStars = 5 - fullStars - (hasHalfStar ? 1 : 0);
var reviewDate = review.DatePublished.ToString("d MMMM yyyy");
<div class="card mb-3">
<div class="card-body">
<blockquote class="blockquote ms-3 me-3 mt-3">
<span class="mb-2 text-warning">
@for (int i = 0; i < fullStars; i++)
{
<i class="fad fa-star"></i>
}
@if (hasHalfStar)
{
<i class="fad fa-star-half-alt"></i>
}
@for (int i = 0; i < emptyStars; i++)
{
<i class="fad fa-star" style="--fa-primary-opacity: 0.2; --fa-secondary-opacity: 0.2;"></i>
}
</span>
@Html.Raw(review.ReviewBody)
<footer class="blockquote-footer mt-2">
@review.AuthorName on
<cite title="@review.SiteName">
@if (string.IsNullOrEmpty(review.URL))
{
@review.SiteName
}
else
{
<a href="@review.URL" target="_blank" rel="noopener">@review.SiteName</a>
}
</cite> &mdash; <span class="text-muted small">@reviewDate</span>
</footer>
</blockquote>
</div>
</div>
}
}
else
{
<p class="text-muted text-center">There are no reviews to display yet. Be the first to leave one!</p>
}
</section>
</div>
</div>
</div>
@section Meta{
<MetaTag meta-title="The Alpha Flame: Discovery by Catherine Lynwood"
meta-description="A gritty 1980s Birmingham crime novel about twin sisters uncovering dark family secrets and surviving abuse. Realistic, powerful, and unflinching — discover The Alpha Flame today."
meta-keywords="The Alpha Flame Discovery, Catherine Lynwood, 1983 novel, twin sisters, suspense fiction, Rubery, Birmingham fiction, historical drama, family secrets"
meta-author="Catherine Lynwood"
meta-url="https://www.catherinelynwood.com/the-alpha-flame/discovery"
meta-image="https://www.catherinelynwood.com/images/webp/the-alpha-flame-discovery-cover-1200.webp"
meta-image-alt="Maggie from 'The Alpha Flame: Discovery' by Catherine Lynwood"
og-site-name="Catherine Lynwood - The Alpha Flame: Discovery"
article-published-time="@new DateTime(2024, 11, 20)"
article-modified-time="@new DateTime(2025, 06, 07)"
twitter-card-type="summary_large_image"
twitter-site-handle="@@CathLynwood"
twitter-creator-handle="@@CathLynwood" />
<script type="application/ld+json">
@Html.Raw(Model.SchemaJsonLd)
</script>
}

View File

@ -0,0 +1,224 @@
@{
ViewData["Title"] = "The Alpha Flame: Discovery Scrap Book";
}
<div class="row">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Extras">Extras</a></li>
<li class="breadcrumb-item active" aria-current="page">Discovery Scrap Book</li>
</ol>
</nav>
</div>
</div>
<div class="container">
<h1 class="mb-5 text-center">Discover Scrapbook</h1>
<p>
On this page I've included some of the images that helped inspire me to write The Alpha Flame: Discovery.
</p>
<p>
Some of them are fictional, generated by AI, others are real, both recent and archive photos.
</p>
<p>
I find that when I'm writing I like to imagine the scene. So having a visual guide helps me tremendously.
So I might, for example, ask AI to generate an image of a luxurious restaraunt. The I would look at the result
and tweak it, until what I was looking at matches what was in my imagination. I also find it useful to do the reverse.
I might write a description of a particular location, Beth's flat was one of them, and then feed that into AI to see
if it generated what I'd described. This was really useful because it helped me work out whether there was enough detail.
My thoughts were that if AI can draw what I've described, then I'm sure you as a reader will be able to imagine something
similar. Hopefully it's worked.
</p>
<!-- Section 1 -->
<div class="scrapbook-section">
<div class="scrapbook-image rotate-1">
<responsive-image src="flyover-at-night.png" class="img-fluid" alt="The Rubery flyover at night time" display-width-percentage="50"></responsive-image>
</div>
<div class="scrapbook-text">
<h5>The Rubery Flyover</h5>
<p>
A number of scenes in The Alpha Flame: Discovery are set, or at least start here. The Rubery flyover carries the A38 Bristol Road
down to meet the M5. It's the main road in and out of the city from the South West. Quite why the floyover exists is something of
a mystery to me. Apart from allowing traffic to pass underneath it, past "Beth's Bench", it seems to serve no purpose. It doesn't
bridge any sort of gap in the landscape. Neither does it provide access to a higher ground further along.
</p>
<p>
Many years ago I used to live in this area and travelling underneath the flyover really gave me the creeps. As I describe in the book
there were often many unsavoury characters seated on the benches. And thre genuinely were women offering their bodies to whoever
happened to drive past.
</p>
</div>
</div>
<!-- Section 2 -->
<div class="scrapbook-section flex-row-reverse">
<div class="scrapbook-image rotate-2">
<responsive-image src="beth-in-her-flat.png" class="img-fluid" alt="Beth in her flat" display-width-percentage="50"></responsive-image>
</div>
<div class="scrapbook-text">
<h5>Beth and Her Flat</h5>
<p>
In the beginning Beth's flat was even more depressing that it is described in the book. The idea of a sterotypical
early 1980s council flat was too hard to resist. However once I had developed her personality a little further it
became obvious that she would have a lot more pride in where she lived, even if she had no money.
</p>
<p>
In reality the block of flats that I put Beth in are only two storeys on top of a row of shops. But for the
purpose of the book I made them into one of the taller blocks that can be found all over the area.
</p>
</div>
</div>
<!-- Section 3 -->
<div class="scrapbook-section">
<div class="scrapbook-image rotate-3">
<responsive-image src="rubery-hill-hospital.png" class="img-fluid" alt="Rubery Hill Hospital" display-width-percentage="50"></responsive-image>
</div>
<div class="scrapbook-text">
<h5>Rubery Hill Hospital</h5>
<p>
Now this place used to genuinely give me the creeps. It was built way back in the 1800s and as I say in the book,
was origonally known as "The City of Birmingham Lunatic Asylum". It was renamed many times over it's long life
but always had links to what we would call "mental health" these days. From what I have found out the only deviation
from this was during the war, when it was a hospital for injured soldiers.
</p>
</div>
</div>
<!-- Section 4 -->
<div class="scrapbook-section flex-row-reverse">
<div class="scrapbook-image rotate-4">
<div class="hero-video-container">
<video autoplay muted loop playsinline poster="/images/maggie-in-her-jumpsuit.png">
<source src="/videos/maggie-in-her-jumpsuit.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
</div>
</div>
<div class="scrapbook-text">
<h5>Maggie in Her Jumpsuit</h5>
<p>
So, the scene where Maggie goes to Limelight and walks across the dance floor to go and say hello to Zoe and Graham. Is this what you imagined?
I doubt it, but it was what was in my mind. The original description of this outfit went on for over a page, and I think you can see why.
Unfortuantely it got severaly cut in the edit. You're probably quite thankful for that!
</p>
<p>
But wouldn't you just love to wear it!
</p>
</div>
</div>
<!-- Section 5 -->
<div class="scrapbook-section">
<div class="scrapbook-image rotate-1">
<responsive-image src="ricardos.png" class="img-fluid" alt="Ricardos" display-width-percentage="50"></responsive-image>
</div>
<div class="scrapbook-text">
<h5>Ricardo's</h5>
<p>
The extremely lavish Riscrod's restaraunt went through many changes, and evloved into something more luxurious every
single time. I'm not sure this image does it justice, but I hope by my descriptions you got the feeling of the sheer
opulance within it.
</p>
<p>
In reality Northfield never had any such restaraunt, it's simply not that sort of place. But it's nice to think that
it could have.
</p>
</div>
</div>
<!-- Section 6 -->
<div class="scrapbook-section flex-row-reverse">
<div class="scrapbook-image rotate-2">
<responsive-image src="beth-in-her-flat.png" class="img-fluid" alt="Beth in her flat" display-width-percentage="50"></responsive-image>
</div>
<div class="scrapbook-text">
<h5>Beth and Her Flat</h5>
<p>
In the beginning Beth's flat was even more depressing that it is described in the book. The idea of a sterotypical
early 1980s council flat was too hard to resist. However once I had developed her personality a little further it
became obvious that she would have a lot more pride in where she lived, even if she had no money.
</p>
<p>
In reality the block of flats that I put Beth in are only two storeys on top of a row of shops. But for the
purpose of the book I made them into one of the taller blocks that can be found all over the area.
</p>
</div>
</div>
<!-- Section 7 -->
<div class="scrapbook-section">
<div class="scrapbook-image rotate-3">
<responsive-image src="rubery-hill-hospital.png" class="img-fluid" alt="Rubery Hill Hospital" display-width-percentage="50"></responsive-image>
</div>
<div class="scrapbook-text">
<h5>Rubery Hill Hospital</h5>
<p>
Now this place used to genuinely give me the creeps. It was built way back in the 1800s and as I say in the book,
was origonally known as "The City of Birmingham Lunatic Asylum". It was renamed many times over it's long life
but always had links to what we would call "mental health" these days. From what I have found out the only deviation
from this was during the war, when it was a hospital for injured soldiers.
</p>
</div>
</div>
</div>
@section Meta {
<style>
.scrapbook-section {
margin-bottom: 4rem;
display: flex;
flex-wrap: wrap;
align-items: center;
}
.scrapbook-image {
background: white;
padding: 10px 10px 30px 10px;
box-shadow: 2px 4px 12px rgba(0, 0, 0, 0.15);
border: 1px solid #ccc;
width: 100%;
max-width: 300px;
margin: 1rem;
transform: rotate(var(--angle));
}
.scrapbook-text {
flex: 1;
min-width: 250px;
margin: 1rem;
}
.rotate-1 {
--angle: -3deg;
}
.rotate-2 {
--angle: 2deg;
}
.rotate-3 {
--angle: -2deg;
}
.rotate-4 {
--angle: 3deg;
}
@@media (max-width: 768px) {
.scrapbook-section {
flex-direction: column;
}
.scrapbook-image {
margin-bottom: 1rem;
}
}
</style>
}

View File

@ -0,0 +1,198 @@
@model List<CatherineLynwood.Models.SoundtrackTrackModel>
@{
ViewData["Title"] = "Alpha Flame • Soundtrack";
}
<section class="container my-4" id="soundtrack">
<header class="mb-4">
<h1 class="h2">The Alpha Flame • Soundtrack</h1>
<p class="text-muted mb-0">Eight original tracks inspired by key chapters; listen while you read…</p>
</header>
<div class="row gy-4">
@if (Model != null && Model.Any())
{
var index = 0;
foreach (var track in Model)
{
var id = $"track-{index++}";
<div class="col-12">
<article class="card shadow-sm h-100">
<div class="row g-0 align-items-stretch">
<!-- Image + Play/Pause -->
<div class="col-12 col-md-5 col-lg-3">
<div class="position-relative h-100">
<responsive-image src="@track.ImageUrl" class="img-fluid w-100 h-100 object-fit-cover rounded-start" alt="@track.Title image" display-width-percentage="50"></responsive-image>
<button type="button"
class="btn btn-light btn-lg rounded-circle position-absolute top-50 start-50 translate-middle track-toggle"
aria-label="Play @track.Title"
data-audio-id="@id">
<i class="fad fa-play"></i>
</button>
</div>
</div>
<!-- Text -->
<div class="col-12 col-md-7 col-lg-9">
<div class="card-body d-flex flex-column">
<h2 class="h4 mb-2">@track.Title</h2>
@if (!string.IsNullOrWhiteSpace(track.Chapter) || !string.IsNullOrWhiteSpace(track.Description))
{
<p class="text-muted small mb-3">
@if (!string.IsNullOrWhiteSpace(track.Chapter))
{
<span><strong>Chapter:</strong> @track.Chapter</span>
}
@if (!string.IsNullOrWhiteSpace(track.Chapter) && !string.IsNullOrWhiteSpace(track.Description))
{
<span> • </span>
}
@if (!string.IsNullOrWhiteSpace(track.Description))
{
<span>@track.Description</span>
}
</p>
}
<div class="lyrics border rounded p-3 mb-3 overflow-auto"
style="max-height: 300px;">
@if (!string.IsNullOrWhiteSpace(track.LyricsHtml))
{
@Html.Raw(track.LyricsHtml)
}
</div>
<div class="mt-auto">
<button type="button"
class="btn btn-outline-dark me-2 track-toggle"
aria-label="Play @track.Title"
data-audio-id="@id">
<i class="fad fa-play me-1"></i> <span>Play</span>
</button>
<span class="text-muted small" data-duration-for="@id"></span>
</div>
<!-- Hidden audio element -->
<audio id="@id"
preload="metadata"
src="\audio\soundtrack\@track.AudioUrl"
data-title="@track.Title"></audio>
</div>
</div>
</div>
</article>
</div>
}
}
else
{
<div class="col-12">
<div class="alert alert-info">
Tracks will appear here soon.
</div>
</div>
}
</div>
<noscript>
<div class="alert alert-warning mt-4">Enable JavaScript to play the soundtrack.</div>
</noscript>
</section>
<style>
/* Keep images nicely cropped */
.object-fit-cover { object-fit: cover; }
/* Make the overlay button stand out on varied artwork */
.track-toggle.btn-light {
--bs-btn-bg: rgba(255,255,255,.9);
--bs-btn-border-color: rgba(0,0,0,.05);
box-shadow: 0 0.5rem 1rem rgba(0,0,0,.15);
width: 3.25rem;
height: 3.25rem;
display: grid;
place-items: center;
}
.track-toggle .fa-play, .track-toggle .fa-pause { font-size: 1.25rem; }
</style>
@section Scripts {
<script>
(function () {
const cards = document.querySelectorAll('#soundtrack article.card');
const toggles = document.querySelectorAll('.track-toggle');
const audios = Array.from(document.querySelectorAll('#soundtrack audio'));
function setAllToStopped(exceptId) {
audios.forEach(a => {
if (a.id !== exceptId) {
a.pause();
a.currentTime = a.currentTime; // stop updating without resetting
updateUI(a, false);
}
});
}
function formatTime(seconds) {
const s = Math.round(seconds);
const m = Math.floor(s / 60);
const r = s % 60;
return `${m}:${r.toString().padStart(2, '0')}`;
}
function updateUI(audio, isPlaying) {
// Update both toggles for this track
const buttons = document.querySelectorAll(`.track-toggle[data-audio-id="${audio.id}"]`);
buttons.forEach(btn => {
const icon = btn.querySelector('i');
const labelSpan = btn.querySelector('span');
if (isPlaying) {
btn.setAttribute('aria-label', `Pause ${audio.dataset.title}`);
if (icon) { icon.classList.remove('fa-play'); icon.classList.add('fa-pause'); }
if (labelSpan) { labelSpan.textContent = 'Pause'; }
} else {
btn.setAttribute('aria-label', `Play ${audio.dataset.title}`);
if (icon) { icon.classList.remove('fa-pause'); icon.classList.add('fa-play'); }
if (labelSpan) { labelSpan.textContent = 'Play'; }
}
});
}
// Wire up buttons
toggles.forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.getAttribute('data-audio-id');
const audio = document.getElementById(id);
if (!audio) return;
if (audio.paused) {
setAllToStopped(id);
audio.play().then(() => updateUI(audio, true)).catch(() => { /* ignore */ });
} else {
audio.pause();
updateUI(audio, false);
}
});
});
// Keep UI in sync with media events
audios.forEach(audio => {
audio.addEventListener('play', () => {
setAllToStopped(audio.id);
updateUI(audio, true);
});
audio.addEventListener('pause', () => updateUI(audio, false));
audio.addEventListener('ended', () => {
audio.currentTime = 0;
updateUI(audio, false);
});
audio.addEventListener('loadedmetadata', () => {
const slot = document.querySelector(`[data-duration-for="${audio.id}"]`);
if (slot && isFinite(audio.duration)) {
slot.textContent = `Length: ${formatTime(audio.duration)}`;
}
});
});
})();
</script>
}

View File

@ -0,0 +1,488 @@
@model CatherineLynwood.Models.FlagSupportViewModel
@{
ViewData["Title"] = "The Alpha Flame - Coming Soon";
}
<!-- Your existing video container: unchanged -->
<div class="container">
<div class="row">
<div class="col-md-12">
<!-- H1 for SEO and accessibility -->
<header class="mb-2 text-center">
<h1 class="h3 mb-1">The Alpha Flame: <span class="fw-light">Discovery</span></h1>
<p class="mb-1">A gritty Birmingham crime novel set in 1983</p>
</header>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="trailer-wrapper">
<video id="trailerVideo" playsinline preload="none"></video>
<button id="trailerPlayBtn" class="trailer-play-btn">
<i class="fad fa-play"></i>
</button>
</div>
</div>
</div>
</div>
@if (DateTime.Now < new DateTime(2025, 8, 21))
{
<section>
<div class="row justify-content-center">
<div class="col-10 col-md-4 text-center bg-white text-dark border border-dark border-3 rounded-5 mt-3 p-3">
<h3 class="h4">Released in:</h3>
<div class="release-countdown mb-2" data-release="2025-08-21T00:00:00+01:00" data-out-text="Out now">
<span class="rcd d"><span class="num">0</span><span class="label">d</span></span>
<span class="rcd h"><span class="num">00</span><span class="label">h</span></span>
<span class="rcd m"><span class="num">00</span><span class="label">m</span></span>
<span class="rcd s"><span class="num">00</span><span class="label">s</span></span>
</div>
<div class="d-grid px-3"><a asp-controller="Discovery" asp-action="Index" class="btn btn-dark btn-pulse">Pre-order Now!</a></div>
<noscript>Releases on 21 Aug 2025</noscript>
</div>
</div>
</section>
}
<!-- Quick interaction: flags -->
<section class="container py-3">
<div class="row">
<div class="col-12 text-center">
<h2 class="h4 mb-2">Please show your support</h2>
<p class="mb-3">Tap your flag to show your support.</p>
</div>
<div class="col-12">
<div class="flag-grid" role="group" aria-label="Choose your country">
@foreach (var kv in Model.FlagCounts)
{
string pulse = "";
var code = kv.Key;
var count = kv.Value;
var name = code switch
{
"UK" => "UK",
"US" => "US",
"CA" => "Canada",
"AU" => "Australia",
"IE" => "Ireland",
"NZ" => "New&nbsp;Zealand",
_ => code
};
var flagFile = code.ToLower() switch
{
"uk" => "gb",
_ => code.ToLower()
};
if (kv.Selected)
{
pulse = "btn-pulse";
}
<button class="flag-btn @pulse" data-country="@code">
<img src="/images/flags/@($"{flagFile}.svg")" alt="@name flag">
<span class="flag-name">@Html.Raw(name)</span>
<span class="flag-count">(@count)</span>
</button>
}
</div>
<!-- Toast/message area -->
<div id="flagToast" class="flag-toast text-center" role="status" aria-live="polite" style="display:none;">
<p id="flagMessage" class="mb-2"></p>
<!-- Hidden release notification form -->
<div id="releaseForm" style="display:none;">
<p>Want me to let you know when the book is released?</p>
<form id="notifyForm" class="mt-2">
<input type="text" id="notifyName" name="name" placeholder="Your name (optional)" class="form-control form-control-sm mb-2">
<input type="email" id="notifyEmail" name="email" placeholder="Enter your email" class="form-control form-control-sm mb-2" required>
<button type="submit" class="btn btn-sm btn-primary">Notify Me</button>
</form>
</div>
</div>
</div>
</div>
</section>
<section class="container py-3">
<div class="row justify-content-center">
<div class="col-md-6 text-dark border border-dark border-3 rounded-5 p-4" style="background-image:url('/images/sheet-music.png'); background-position: center; background-size: cover;">
<h3 class="h5 text-center text-dark pb-3">Listen to The Alpha Flame theme tune<br />The Flame We Found</h3>
<audio controls="controls">
<source src="~/audio/the-flame-we-found-original-song-inspired-by-alpha-flame_teaser.mp3" type="audio/mpeg" />
</audio>
</div>
</div>
</section>
<!-- Teaser reel -->
<section class="container py-3">
<div class="row">
<div class="col-12">
<h2 class="h4 mb-3 text-center">A glimpse inside</h2>
</div>
<div class="col-md-4">
<article class="teaser-card border border-3 border-dark mb-3">
<div class="teaser-bg" style="background-image:url('/images/webp/the-alpha-flame-discovery-back-cover-400.webp');"></div>
<div class="teaser-copy">
<div>
<p class="h1 text-warning">
The Alpha Flame: Discovery
</p>
<p>
Some girls survive. Others set the world on fire.
</p>
<p>
She didnt go looking for trouble. But when she found Beth, bruised, broken, and terrified, Maggie couldnt walk away.
</p>
<p>
But nothing prepares her for Beth.
</p>
<p>
As she digs deeper into Beths world, Maggie finds herself pulled into the shadows, a seedy underworld of secrets, survival, and control, where loyalty is rare and nothing is guaranteed. The more she uncovers, the more she realises this isnt someone elses nightmare. Its her own.
</p>
<p>
The Alpha Flame: Discovery is a gritty, emotionally charged thriller that pulls no punches. Raw, real, and anything but a fairy tale, its a story of survival, sisterhood, and fire.
</p>
<div class="teaser-actions">
<button class="btn btn-sm btn-light" data-audio="#aud4"><i class="fad fa-play"></i> Listen 58s</button>
</div>
</div>
</div>
<audio id="aud4" preload="none" src="/audio/book-synopsis.mp3"></audio>
</article>
</div>
<div class="col-md-4">
<article class="teaser-card border border-3 border-dark mb-3">
<div class="teaser-bg" style="background-image:url('/images/webp/teaser-city-400.webp');"></div>
<div class="teaser-copy">
<div>
<p>
I eased the TR6 down a side street, the headlights sweeping over a figure shifting in the shadows. A movement to my left. A woman, young, her face pale beneath the heavy makeup, stepped forward as I slowed at the junction. She leaned down to my passenger window, so close I could see the faint smudge of lipstick at the corner of her mouth.
</p>
<p>
A loud knock on the glass made me jump.
</p>
<p>
“You looking for something, love?” she asked, her voice soft but direct. Her lips were parted just slightly, her breath misting against the cold window.
</p>
<p>
My stomach tightened.
</p>
<p>
I wasnt looking for anything. Not really. But I didnt drive away either.
</p>
<p>
She was close now, close enough that I could see the dark liner smudged beneath her eyes, the glint of something unreadable in her gaze. Not quite curiosity. Not quite suspicion. Just a quiet knowing.
</p>
<div class="teaser-actions">
<button class="btn btn-sm btn-light" data-audio="#aud1"><i class="fad fa-play"></i> Listen 50s</button>
</div>
</div>
</div>
<audio id="aud1" preload="none" src="/audio/snippets/clip-1.mp3"></audio>
</article>
</div>
<div class="col-md-4">
<article class="teaser-card border border-3 border-dark mb-3">
<div class="teaser-bg" style="background-image:url('/images/webp/teaser-hospital-400.webp');"></div>
<div class="teaser-copy">
<div>
<p>
“Maggie… wait.”
</p>
<p>
She turned as I crouched down. My stomach dropped.
</p>
<p>
It was a sweatshirt. Pink. Faded. Cartoon print on the front, cracked with age and wear. Garfield, grinning.
</p>
<p>
I reached out slowly, fingertips brushing the fabric. The left sleeve was soaked, stiff with something dark.
</p>
<p>
Blood.
</p>
<p>
“Maggie…” My voice broke. “Its hers. She used to wear this all the time. She was wearing it the last time I saw her.”
</p>
<p>
Maggie dropped to her knees beside me, torch trembling in her grip. “Bloody hell. Youre right.”
</p>
<p>
For a second neither of us moved. The building suddenly felt tighter, like it was watching us.
</p>
<div class="teaser-actions">
<button class="btn btn-sm btn-light" data-audio="#aud2"><i class="fad fa-play"></i> Listen 28s</button>
</div>
</div>
</div>
<audio id="aud2" preload="none" src="/audio/snippets/clip-2.mp3"></audio>
</article>
</div>
<div class="col-md-4 d-md-none">
<article class="teaser-card border border-3 border-dark mb-2">
<div class="teaser-bg" style="background-image:url('/images/webp/teaser-beach-400.webp');"></div>
<div class="teaser-copy">
<div>
<p>
She turned in the water, soaked to the waist, flinging droplets everywhere.
</p>
<p>
“Maggie! Come on!” she shouted, laughing. “Youve got to feel this!”
</p>
<p>
I didnt hesitate.
</p>
<p>
I peeled off my hoody and shorts, left them in a heap on the rocks, and sprinted after her, my bikini clinging tight to my skin in the salty breeze. The sand stung slightly as I ran, then came the cold slap of the sea, wrapping around my legs and dragging a breathless laugh out of me.
</p>
<p>
Beth was already dancing through the waves like a lunatic.
</p>
<p>
We collided mid-splash, both of us soaked, screaming and laughing like we were eight years old again, like wed somehow got all those childhood summers back in one moment.
The sea was freezing, but we didnt care.
</p>
<div class="teaser-actions">
<button class="btn btn-sm btn-light" data-audio="#aud3"><i class="fad fa-play"></i> Listen 37s</button>
</div>
</div>
</div>
<audio id="aud3" preload="none" src="/audio/snippets/clip-3.mp3"></audio>
</article>
</div>
</div>
</section>
<!-- Footer note -->
<section class="container pb-4">
<div class="row justify-content-center">
<div class="col-md-6 text-center">
<h3 class="h4 mb-3"><strong>Coming 21st August 2025</strong> to major retailers.</h3>
<div class="d-grid"><a asp-controller="Discovery" asp-action="Index" class="btn btn-dark btn-pulse">Find Out More</a></div>
</div>
</div>
</section>
@*
<responsive-image src="the-alpha-flame-discovery-trailer-landscape.png" class="img-fluid" alt="The Alpha Flame book cover — gritty 1980s Birmingham crime novel about twin sisters uncovering secrets and surviving abuse" display-width-percentage="100"></responsive-image>
<responsive-image src="the-alpha-flame-discovery-trailer-portrait.png" class="img-fluid" alt="The Alpha Flame book cover — gritty 1980s Birmingham crime novel about twin sisters uncovering secrets and surviving abuse" display-width-percentage="50"></responsive-image>
<responsive-image src="the-alpha-flame-discovery-back-cover.png" class="img-fluid" alt="The Alpha Flame book cover — gritty 1980s Birmingham crime novel about twin sisters uncovering secrets and surviving abuse" display-width-percentage="50"></responsive-image>
*@
@section Scripts {
<script>
const player = new Plyr('audio');
</script>
<script>
document.addEventListener("DOMContentLoaded", () => {
const video = document.getElementById("trailerVideo");
const playBtn = document.getElementById("trailerPlayBtn");
// Pick correct source and poster before loading
const isDesktop = window.matchMedia("(min-width: 400px)").matches;
video.poster = isDesktop
? "/images/webp/the-alpha-flame-discovery-trailer-landscape-1400.webp"
: "/images/webp/the-alpha-flame-discovery-trailer-portrait-400.webp";
const src = isDesktop
? "/videos/the-alpha-flame-discovery-trailer-landscape.mp4"
: "/videos/the-alpha-flame-discovery-trailer-portrait.mp4";
const sourceEl = document.createElement("source");
sourceEl.src = src;
sourceEl.type = "video/mp4";
video.appendChild(sourceEl);
// Play button click handler
playBtn.addEventListener("click", () => {
video.muted = false;
video.volume = 1.0;
video.play().then(() => {
playBtn.style.display = "none"; // hide button once playing
}).catch(err => {
console.warn("Video play failed:", err);
});
});
// --- Teaser audio logic ---
const btnForAudio = new Map();
function anyTeaserPlaying() {
return Array.from(btnForAudio.keys()).some(a => !a.paused && !a.ended);
}
document.querySelectorAll("[data-audio]").forEach(btn => {
const aud = document.querySelector(btn.getAttribute("data-audio"));
if (!aud) return;
btn.dataset.orig = btn.innerHTML;
btnForAudio.set(aud, btn);
aud.addEventListener("ended", () => {
btn.innerHTML = btn.dataset.orig;
if (!anyTeaserPlaying()) {
video.play().catch(() => {});
}
});
});
document.addEventListener("click", e => {
const btn = e.target.closest("[data-audio]");
if (!btn) return;
const aud = document.querySelector(btn.getAttribute("data-audio"));
if (!aud) return;
// Stop others
btnForAudio.forEach((b, a) => {
if (a !== aud) {
a.pause();
a.currentTime = 0;
b.innerHTML = b.dataset.orig;
}
});
if (aud.paused) {
video.pause();
aud.currentTime = 0;
aud.play().then(() => {
btn.innerHTML = '<i class="fad fa-pause"></i> Pause';
});
} else {
aud.pause();
btn.innerHTML = btn.dataset.orig;
if (!anyTeaserPlaying()) {
video.play().catch(() => {});
}
}
});
});
</script>
<script>
let selectedCountry = null;
window.addEventListener("click", function (e) {
const flag = e.target.closest('.flag-btn');
if (!flag) return;
selectedCountry = flag.getAttribute("data-country") || "Your country";
const key = "taf_support_" + selectedCountry;
if (!localStorage.getItem(key)) {
localStorage.setItem(key, "1");
}
// Send click to server and update count
fetch("/api/support/flag", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ country: selectedCountry })
})
.then(res => res.json())
.then(data => {
if (data.ok) {
// Update the flag's count
const countEl = flag.querySelector(".flag-count");
if (countEl) countEl.textContent = `(${data.total})`;
}
});
// Show thank-you + form
document.getElementById("flagMessage").textContent = `Thanks for the love, ${selectedCountry}!`;
document.getElementById("flagToast").style.display = "block";
document.getElementById("releaseForm").style.display = "block";
// Tap animation
if (flag.animate) {
flag.animate(
[{ transform: 'scale(1)' }, { transform: 'scale(1.06)' }, { transform: 'scale(1)' }],
{ duration: 260, easing: 'ease-out' }
);
}
});
document.addEventListener("submit", function (e) {
if (e.target && e.target.id === "notifyForm") {
e.preventDefault();
const emailInput = document.getElementById("notifyEmail");
if (!emailInput.value) return;
fetch("/api/support/subscribe", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
country: selectedCountry,
name: document.getElementById("notifyName").value || null,
email: document.getElementById("notifyEmail").value
})
});
document.getElementById("releaseForm").innerHTML = "<p>Thanks! We'll email you when the book is released.</p>";
}
});
</script>
@if (DateTime.Now < new DateTime(2025, 8, 21))
{
<script>
document.addEventListener('DOMContentLoaded', function () {
function initCountdown(el) {
if (!el) return;
var iso = el.getAttribute('data-release');
var ts = Date.parse(iso);
if (isNaN(ts)) return;
var dEl = el.querySelector('.rcd.d .num');
var hEl = el.querySelector('.rcd.h .num');
var mEl = el.querySelector('.rcd.m .num');
var sEl = el.querySelector('.rcd.s .num');
var outText = el.getAttribute('data-out-text') || 'Out now';
function pad(n) { return n < 10 ? '0' + n : '' + n; }
function tick() {
var now = Date.now();
var diff = ts - now;
if (diff <= 0) {
el.textContent = outText;
clearInterval(timer);
return;
}
var secs = Math.floor(diff / 1000);
var days = Math.floor(secs / 86400); secs -= days * 86400;
var hrs = Math.floor(secs / 3600); secs -= hrs * 3600;
var mins = Math.floor(secs / 60); secs -= mins * 60;
if (dEl) dEl.textContent = days;
if (hEl) hEl.textContent = pad(hrs);
if (mEl) mEl.textContent = pad(mins);
if (sEl) sEl.textContent = pad(secs);
}
tick();
var timer = setInterval(tick, 1000);
}
// Support multiple countdowns on a page
var timers = document.querySelectorAll('.release-countdown');
for (var i = 0; i < timers.length; i++) {
initCountdown(timers[i]);
}
});
</script>
}
}

View File

@ -0,0 +1,42 @@
@{
Layout = "/Views/Shared/_Layout.cshtml";
}
@section Meta {
@RenderSection("Meta", required: false)
}
@section CSS {
@RenderSection("CSS", required: false)
}
@if (IsSectionDefined("BackgroundVideo"))
{
@section BackgroundVideo {
@RenderSection("BackgroundVideo", required: false)
}
}
else
{
@section BackgroundVideo {
<div id="background-wrapper">
<div class="video-background">
<video id="siteBackgroundVideo"
autoplay
muted
loop
playsinline
preload="none">
<!-- Source will be injected by JS -->
</video>
<div class="video-overlay"></div>
</div>
</div>
}
}
@RenderBody()
@section Scripts {
@RenderSection("Scripts", required: false)
}

View File

@ -0,0 +1,3 @@
@model CatherineLynwood.Models.TitlePageViewModel
@await Component.InvokeAsync("BuyPanel", new { ISO2 = Model.UserIso2, Src = Model.Src, Title = Model.Title })

View File

@ -0,0 +1,105 @@
@model CatherineLynwood.Models.TitlePageViewModel
<h2>Evidence</h2>
<p class="casefile-evidence-intro">
Recovered materials connected to the case. Several items remain incomplete, damaged, or subject to restricted review.
</p>
<div class="casefile-evidence-list">
<section class="casefile-evidence-item casefile-evidence-item-featured">
<div class="casefile-evidence-item-header">
<div class="casefile-evidence-item-id">Item 01</div>
<h3>Audio Recordings</h3>
</div>
<div class="casefile-evidence-media">
<button type="button"
class="casefile-audio-trigger"
data-audio-target="evidenceTapeAudio"
aria-label="Play recovered tape audio">
<span class="casefile-audio-image-wrapper">
<responsive-image src="cassette-tape.png"
class="casefile-evidence-image"
alt="Cassette tape evidence in plastic case"
display-width-percentage="30"></responsive-image>
<span class="casefile-audio-overlay"></span>
</span>
</button>
<audio id="evidenceTapeAudio" preload="auto">
<source src="/audio/cassette-tape.mp3" type="audio/mpeg" />
</audio>
</div>
<p>
Recovered from secured deposit storage. Handwritten labels inconsistent. Audio quality poor in places.
</p>
<p>
Fragmented conversation captured between unidentified voices. Repeated references to money, locations, and an unnamed female subject.
</p>
<p class="casefile-evidence-note">
Transcript not included.
</p>
</section>
<section class="casefile-evidence-item">
<div class="casefile-evidence-item-header">
<div class="casefile-evidence-item-id">Item 02</div>
<h3>Deposit Record Extract</h3>
</div>
<div class="casefile-evidence-media">
<responsive-image src="bank-book.png"
class="casefile-evidence-image"
alt="Deposit book page containing an encoded number"
display-width-percentage="30"></responsive-image>
</div>
<p>
Final page of deposit record book. Contains handwritten notation believed to reference a coded box number.
</p>
<p>
Meaning not immediately apparent. Pattern only becomes visible when cross-referenced with accompanying material.
</p>
</section>
<section class="casefile-evidence-item">
<div class="casefile-evidence-item-header">
<div class="casefile-evidence-item-id">Item 03</div>
<h3>Handwritten Note</h3>
</div>
<div class="casefile-evidence-paperclip">
<responsive-image src="blackmail-note.png"
class="casefile-evidence-image"
alt="Partially folded handwritten note"
display-width-percentage="30"></responsive-image>
</div>
<p>
Small folded note recovered during follow-up enquiries. Believed to have been used to direct movement between locations.
</p>
<p>
Full wording withheld from this file. Relevance considered significant.
</p>
</section>
<section class="casefile-evidence-item">
<div class="casefile-evidence-item-header">
<div class="casefile-evidence-item-id">Item 04</div>
<h3>Redacted Document Extract</h3>
</div>
<div class="casefile-redacted-block">
<p>██████████████████████████████████████</p>
<p>██████████████████ ███████████████████</p>
<p>Reference to individual removed from file.</p>
<p>Access restricted pending review.</p>
</div>
</section>
</div>

View File

@ -0,0 +1,12 @@
@model CatherineLynwood.Models.TitlePageViewModel
<h2>Restricted</h2>
<p class="restricted-label">Authorised Access Only</p>
<p>Bonus archive material is locked.</p>
<p>
When you've purchased an edition of The Alpha Flame: Reckoning come back to gain access to all sort of hidden extras.
</p>
<p>
New material is being added all the time.
</p>
<a asp-action="Extras" class="restricted-button">Unlock Access</a>

View File

@ -0,0 +1,89 @@
@model CatherineLynwood.Models.TitlePageViewModel
@{
bool showReviews = Model.Reviews.Items.Any();
}
<h2>External Review</h2>
<p>
Supplementary observations and external responses filed in relation to this case.
</p>
<p>
Public reaction remains limited but notable. The following extract has been selected for inclusion.
</p>
@if (showReviews)
{
var top = Model.Reviews.Items
.Where(x => x.RatingValue == 5)
.OrderByDescending(y => y.DatePublished)
.First();
var fullStars = (int)Math.Floor(top.RatingValue);
var hasHalfStar = top.RatingValue - fullStars >= 0.5;
var emptyStars = 5 - fullStars - (hasHalfStar ? 1 : 0);
var reviewDate = top.DatePublished.ToString("d MMMM yyyy");
<section class="casefile-reviews">
<h3 class="casefile-section-heading">External Commentary</h3>
<div class="casefile-review-entry">
<div class="casefile-review-stars" aria-label="@top.RatingValue out of 5 stars">
@for (int i = 0; i < fullStars; i++)
{
<i class="fad fa-star"></i>
}
@if (hasHalfStar)
{
<i class="fad fa-star-half-alt"></i>
}
@for (int i = 0; i < emptyStars; i++)
{
<i class="fad fa-star casefile-review-star-empty"></i>
}
</div>
<blockquote class="casefile-review-body">
@Html.Raw(top.ReviewBody)
</blockquote>
<p class="casefile-review-source">
<strong>@top.AuthorName</strong> on
@if (string.IsNullOrEmpty(top.URL))
{
<span>@top.SiteName</span>
}
else
{
<a href="@top.URL" target="_blank" rel="noopener noreferrer">@top.SiteName</a>
}
<span class="casefile-review-date">, @reviewDate</span>
</p>
</div>
@if (Model.Reviews.Items.Count > 1)
{
<div class="casefile-review-more">
<a asp-action="Reviews" class="casefile-inline-link">Read more reviews</a>
</div>
}
</section>
}
else
{
<section class="casefile-reviews">
<h3 class="casefile-section-heading">External Commentary</h3>
<p>
No reader responses have yet been added to this file.
</p>
</section>
}
<p class="casefile-notes-meta">
<strong>Classification:</strong> Circulation ongoing<br />
<strong>Reference:</strong> AF-R/02
</p>
<p class="casefile-notes-margin">
<em>Margin note:</em> “Keep watching this one.”
</p>

View File

@ -0,0 +1,80 @@
@model CatherineLynwood.Models.TitlePageViewModel
<h2>Subjects</h2>
<p class="reckoning-case-subtitle">Primary persons connected to ongoing enquiries.</p>
<div class="reckoning-subjects-scroll">
<div class="reckoning-subject-entry">
<div class="row">
<div class="col-4">
<responsive-image src="maggie-grant-36.png"
class="img-polaroid"
alt="Maggie Grant"
display-width-percentage="30"></responsive-image>
</div>
<div class="col-8">
<h3>SUBJECT: GRANT, MAGGIE</h3>
<p><strong>AKA:</strong> Fletcher, Grace</p>
<p><strong>DoB:</strong> 01/09/1965</p>
</div>
</div>
<p><strong>Status:</strong> Victim</p>
<p><strong>Notes:</strong> Confident, observant, and increasingly proactive in ongoing events. Demonstrates strong protective instincts toward Beth and shows a marked tendency to pursue answers independently. Appears stronger than initial assessments suggested. Extent of involvement remains unclear.</p>
</div>
<div class="reckoning-subject-entry">
<div class="row">
<div class="col-4">
<responsive-image src="beth-6.png"
class="img-polaroid"
alt="Beth Fletcher"
display-width-percentage="30"></responsive-image>
</div>
<div class="col-8">
<h3>SUBJECT: FLETCHER, BETH</h3>
<p><strong>DoB:</strong> 31/08/1965</p>
</div>
</div>
<p><strong>Status:</strong> Victim</p>
<p><strong>Notes:</strong> Known to have suffered sustained trauma and coercive control. Holds key links to several individuals under observation. Emotional condition fragile, though recent behaviour suggests increasing resilience. May possess critical information, whether knowingly or otherwise.</p>
</div>
<div class="reckoning-subject-entry">
<div class="row">
<div class="col-4">
<responsive-image src="simon-jones-mug-shot.png"
class="img-polaroid"
alt="Simon Jones"
display-width-percentage="30"></responsive-image>
</div>
<div class="col-8">
<h3>PERSON OF INTEREST: JONES, SIMON</h3>
<p><strong>DoB:</strong> 15/010/1935</p>
<p><strong>Occupation:</strong> Business owner</p>
</div>
</div>
<p><strong>Status:</strong> Under informal scrutiny</p>
<p><strong>Notes:</strong> Name appears repeatedly across multiple lines of enquiry. Public profile respectable, though several associations remain unresolved. No direct action taken to date. Influence believed to extend further than officially recorded.</p>
<p class="reckoning-redacted">Known associates: ███████████████</p>
</div>
<div class="reckoning-subject-entry">
<div class="row">
<div class="col-4">
<responsive-image src="sophie-jones-street-shot.png"
class="img-polaroid"
alt="Sophie Jones"
display-width-percentage="30"></responsive-image>
</div>
<div class="col-8">
<h3>PERSON OF INTEREST: JONES, SOPHIE</h3>
<p><strong>DoB:</strong> 07/03/1962</p>
<p><strong>Status:</strong> Under investigation</p>
</div>
</div>
<p><strong>Notes:</strong> Outwardly composed and cooperative, but behaviour raises concern. Presence noted around several incidents without formal connection being established. Considered potentially significant. Further attention advised.</p>
<p class="reckoning-redacted">Known associates: Harris, Richard Arthur</p>
</div>
</div>

View File

@ -0,0 +1,21 @@
@model CatherineLynwood.Models.TitlePageViewModel
<h2>Case Summary</h2>
<p>
They thought they were searching for the past. They didnt realise the past was searching for them.
</p>
<p>
As Maggie and Beth begin to piece together the fragments of their lives, the line between truth and danger begins to blur. Every answer leads to another question. Every step forward pulls them deeper into something neither of them fully understands.
</p>
<p>
What began as a search for identity has uncovered a pattern… one that stretches back years, touching lives that were never meant to be connected.
</p>
<p>
Witnesses are unreliable. Records are incomplete. And certain names keep appearing where they shouldnt.
</p>
<p>
Someone has been watching.
</p>
<p>
And now, theyre getting closer.
</p>

Some files were not shown because too many files have changed in this diff Show More