This commit is contained in:
Nick 2025-09-13 21:04:48 +01:00
parent 4759fbbd69
commit 3ce32496e8
10 changed files with 895 additions and 524 deletions

View File

@ -1,11 +1,10 @@
using System.Globalization;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
using System.Text;
using CatherineLynwood.Models;
using CatherineLynwood.Services;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
namespace CatherineLynwood.Controllers
@ -16,31 +15,40 @@ namespace CatherineLynwood.Controllers
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 DiscoveryController(DataAccess dataAccess, ICountryContext country)
{
_dataAccess = dataAccess;
_country = country;
}
// -------------------------
// Pages
// -------------------------
[Route("")]
public async Task<IActionResult> Index()
{
// 1) Resolve country ISO2 (already set by middleware)
var iso2 = (_country.Iso2 ?? "GB").ToUpperInvariant();
if (iso2 == "UK") iso2 = "GB"; // normalise
// Prevent caches or CDNs from cross-serving variants
Response.Headers["Vary"] = "Cookie, User-Agent";
// 2) Build server-side link slugs for this user
// 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";
// Buy links
var buyLinks = BuildBuyLinksFor(iso2);
// 3) Load reviews (unchanged)
// Reviews
Reviews reviews = await _dataAccess.GetReviewsAsync();
reviews.SchemaJsonLd = GenerateBookSchemaJsonLd(reviews, 3);
// 4) Compose page VM
// VM
var vm = new DiscoveryPageViewModel
{
Reviews = reviews,
@ -48,8 +56,17 @@ namespace CatherineLynwood.Controllers
Buy = buyLinks
};
// Use your existing view (rename if you prefer): Index1.cshtml
return View("Index1", vm);
// 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 => "IndexMobileB", // mobile layout B
_ => "IndexDesktop" // desktop layout C
};
return View(viewName, vm);
}
[Route("reviews")]
@ -57,7 +74,6 @@ namespace CatherineLynwood.Controllers
{
Reviews reviews = await _dataAccess.GetReviewsAsync();
reviews.SchemaJsonLd = GenerateBookSchemaJsonLd(reviews, 100);
// If you prefer a strongly-typed page VM here too, you can wrap, but returning Reviews keeps your existing view working
return View(reviews);
}
@ -101,9 +117,146 @@ namespace CatherineLynwood.Controllers
return View(soundtrackTrackModels);
}
// -------------------------
// Private helpers
// -------------------------
// =========================
// 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 static BuyLinksViewModel BuildBuyLinksFor(string iso2)
{
@ -149,7 +302,7 @@ namespace CatherineLynwood.Controllers
LinkChoice? mk(string? slug) => string.IsNullOrEmpty(slug) ? null : new LinkChoice { Slug = slug, Url = BuyCatalog.Url(slug) };
return new BuyLinksViewModel
var vm = new BuyLinksViewModel
{
Country = cc,
IngramHardback = mk(ingHbSlug),
@ -163,8 +316,20 @@ namespace CatherineLynwood.Controllers
Apple = mk(appleSlug)!,
Kobo = mk(koboSlug)!
};
}
if (cc == "GB")
{
vm.IngramHardbackPrice = "£16.99";
vm.IngramPaperbackPrice = "£12.99";
}
else if (cc == "US")
{
vm.IngramHardbackPrice = "$24.99";
vm.IngramPaperbackPrice = "$16.99";
}
return vm;
}
private string GenerateBookSchemaJsonLd(Reviews reviews, int take)
{
@ -190,13 +355,12 @@ namespace CatherineLynwood.Controllers
["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 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",
["inLanguage"] = "en-GB",
["url"] = baseUrl
};
// Reviews
if (reviews?.Items?.Any() == true)
{
var reviewObjects = new List<Dictionary<string, object>>();
@ -233,7 +397,6 @@ namespace CatherineLynwood.Controllers
};
}
// Work examples
schema["workExample"] = new List<Dictionary<string, object>>
{
new Dictionary<string, object>

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

@ -4,26 +4,26 @@
{
#region Public Properties
// Amazon (always)
public LinkChoice AmazonHardback { get; set; } = new();
public LinkChoice AmazonKindle { get; set; } = new();
public LinkChoice AmazonPaperback { get; set; } = new();
// eBooks (always)
public LinkChoice Apple { get; set; } = new();
public string Country { get; set; } = "GB";
// Direct (optional)
public LinkChoice? IngramHardback { get; set; }
public string? IngramHardbackPrice { get; set; }
public LinkChoice? IngramPaperback { get; set; }
public string? IngramPaperbackPrice { get; set; }
public LinkChoice Kobo { get; set; } = new();
// National (optional)
public LinkChoice? NationalHardback { get; set; }
public string? NationalLabel { get; set; }

View File

@ -1,4 +1,5 @@
using CatherineLynwood.Middleware;
using CatherineLynwood.Models;
using CatherineLynwood.Services;
using Microsoft.AspNetCore.HttpOverrides;
@ -83,6 +84,8 @@ namespace CatherineLynwood
});
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"));
// HTML minification
builder.Services.AddWebMarkupMin(o =>

View File

@ -1,383 +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">
<!-- Buy Section -->
<div id="buy-now" class="mb-4">
<a asp-action="HowToBuy" class="btn btn-dark">
<i class="fad fa-books"> </i> Buy the Book
</a>
</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,338 @@
@model CatherineLynwood.Models.DiscoveryPageViewModel
@{
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>
<!-- Buy Box -->
@* buyBox: server-side slugs + <a ping> tracking *@
@* Model: CatherineLynwood.Models.DiscoveryPageViewModel *@
@{
var L = Model.Buy;
string pingBase = "/track/click";
string countryIso2 = Model.UserIso2 ?? "GB";
string flagPathSvg = $"/images/flags/{countryIso2}.svg";
string flagPathPng = $"/images/flags/{countryIso2}.png";
}
<partial name="_BuyBox" />
</div>
</div>
</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 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>
<!-- Teaser -->
<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

@ -20,29 +20,15 @@
<!-- HERO: Cover + Trailer + Buy Box -->
<section class="mb-4">
<div class="row g-3 align-items-stretch">
<!-- Book Cover -->
<div class="col-md-5 d-flex d-none d-md-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-md-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 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 d-block d-lg-none"
class="w-100"
playsinline
preload="none"
poster="/images/webp/the-alpha-flame-discovery-trailer-portrait-400.webp"
@ -51,17 +37,6 @@
Sorry, your browser doesn't support embedded video.
</video>
<!-- Desktop: LANDSCAPE -->
<video id="trailerLandscape"
class="w-100 d-none d-lg-block"
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>
@ -188,20 +163,8 @@
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>
<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>
@ -266,82 +229,47 @@
const player = new Plyr('audio');
</script>
<!-- Trailer source selection + play button -->
<!-- Trailer play/pause via custom button or video click (single video) -->
<script>
document.addEventListener("DOMContentLoaded", () => {
const vPortrait = document.getElementById("trailerPortrait");
const vLandscape = document.getElementById("trailerLandscape");
const playBtn = document.getElementById("trailerPlayBtn");
if (!playBtn) return;
document.addEventListener("DOMContentLoaded", () => {
const v = document.getElementById("trailerPortrait");
const playBtn = document.getElementById("trailerPlayBtn");
if (!v || !playBtn) return;
// Which video is currently visible according to Bootstraps lg breakpoint (>=992px)?
const isDesktop = () => window.matchMedia("(min-width: 992px)").matches;
const activeVideo = () => isDesktop() ? vLandscape : vPortrait;
// Hide native controls when JS is active; use custom button instead
v.controls = false;
// Hide native controls when JS is active; custom button will start playback
[vPortrait, vLandscape].forEach(v => { if (v) 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));
};
// Start playback on whichever video is visible
playBtn.addEventListener("click", () => {
const v = activeVideo();
if (!v) return;
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";
}
};
// Toggle pause/play when user clicks on the video itself
[vPortrait, vLandscape].forEach(v => {
if (!v) return;
v.addEventListener("click", () => {
if (v.paused) {
v.play().then(() => {
playBtn.style.display = "none";
});
} else {
v.pause();
playBtn.style.display = "block";
}
});
});
// Events
playBtn.addEventListener("click", (e) => { e.preventDefault(); startPlayback(); });
v.addEventListener("click", togglePlayback);
// If the user starts via OS overlay/natives, also hide the custom button
[vPortrait, vLandscape].forEach(v => {
if (!v) return;
v.addEventListener("play", () => { playBtn.style.display = "none"; });
v.addEventListener("pause", () => { 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);
// 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 {

View File

@ -0,0 +1,300 @@
@model CatherineLynwood.Models.DiscoveryPageViewModel
@{
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>
<!-- 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>
<!-- Teaser -->
<p class="card-text">
Set in 1983 Birmingham, nearby Redditch, and Barmouth in Wales, The Alpha Flame: Discovery follows 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>
<!-- One copy of the full synopsis -->
<div>
<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>
<!-- 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">
@* buyBox: server-side slugs + <a ping> tracking *@
@* Model: CatherineLynwood.Models.DiscoveryPageViewModel *@
@{
var L = Model.Buy;
string pingBase = "/track/click";
string countryIso2 = Model.UserIso2 ?? "GB";
string flagPathSvg = $"/images/flags/{countryIso2}.svg";
string flagPathPng = $"/images/flags/{countryIso2}.png";
}
<partial name="_BuyBox" />
</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

@ -40,10 +40,13 @@
<div class="col-12 col-sm-6">
<a class="btn btn-dark w-100"
href="@L.IngramHardback.Url"
ping="@($"{pingBase}?slug={L.IngramHardback.Slug}&src=discovery")"
ping="@($"/track/click?slug={L.IngramHardback.Slug}&src=discovery")"
rel="nofollow noindex">
<i class="fad fa-gem me-1"></i> Hardback, direct
<span class="price-chip d-none" aria-hidden="true"></span>
@if (!string.IsNullOrWhiteSpace(L.IngramHardbackPrice))
{
<span class="price-chip ms-2">@L.IngramHardbackPrice</span>
}
</a>
</div>
}
@ -52,13 +55,17 @@
<div class="col-12 col-sm-6">
<a class="btn btn-dark w-100"
href="@L.IngramPaperback.Url"
ping="@($"{pingBase}?slug={L.IngramPaperback.Slug}&src=discovery")"
ping="@($"/track/click?slug={L.IngramPaperback.Slug}&src=discovery")"
rel="nofollow noindex">
<i class="fad fa-book me-1"></i> Paperback, direct
<span class="price-chip d-none" aria-hidden="true"></span>
@if (!string.IsNullOrWhiteSpace(L.IngramPaperbackPrice))
{
<span class="price-chip ms-2">@L.IngramPaperbackPrice</span>
}
</a>
</div>
}
</div>
</div>
}

View File

@ -19,6 +19,9 @@
"IndexNow": {
"ApiKey": "cc6ff72c3d1a48d0b0b7c2c2b543f15f"
},
"AbTest": {
"DiscoveryBPercent": 50
},
"Logging": {
"LogLevel": {
"Default": "Information",