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.
@ -182,9 +182,94 @@ namespace CatherineLynwood.Controllers
|
||||
Retailer = "Barnes & Noble",
|
||||
Format = "Paperback",
|
||||
CountryGroup = "US"
|
||||
},
|
||||
|
||||
// --- Apple Books (GB/US/CA/AU/IE) ---
|
||||
["apple-ebook-gb"] = new BuyLink
|
||||
{
|
||||
Slug = "apple-ebook-gb",
|
||||
Url = "https://books.apple.com/gb/book/the-alpha-flame/id6747852729",
|
||||
Retailer = "Apple Books",
|
||||
Format = "eBook",
|
||||
CountryGroup = "GB"
|
||||
},
|
||||
["apple-ebook-us"] = new BuyLink
|
||||
{
|
||||
Slug = "apple-ebook-us",
|
||||
Url = "https://books.apple.com/us/book/the-alpha-flame/id6747852729",
|
||||
Retailer = "Apple Books",
|
||||
Format = "eBook",
|
||||
CountryGroup = "US"
|
||||
},
|
||||
["apple-ebook-ca"] = new BuyLink
|
||||
{
|
||||
Slug = "apple-ebook-ca",
|
||||
Url = "https://books.apple.com/ca/book/the-alpha-flame/id6747852729",
|
||||
Retailer = "Apple Books",
|
||||
Format = "eBook",
|
||||
CountryGroup = "CA"
|
||||
},
|
||||
["apple-ebook-au"] = new BuyLink
|
||||
{
|
||||
Slug = "apple-ebook-au",
|
||||
Url = "https://books.apple.com/au/book/the-alpha-flame/id6747852729",
|
||||
Retailer = "Apple Books",
|
||||
Format = "eBook",
|
||||
CountryGroup = "AU"
|
||||
},
|
||||
["apple-ebook-ie"] = new BuyLink
|
||||
{
|
||||
Slug = "apple-ebook-ie",
|
||||
Url = "https://books.apple.com/ie/book/the-alpha-flame/id6747852729",
|
||||
Retailer = "Apple Books",
|
||||
Format = "eBook",
|
||||
CountryGroup = "IE"
|
||||
},
|
||||
|
||||
// --- Kobo (GB/US/CA/AU/IE) ---
|
||||
["kobo-ebook-gb"] = new BuyLink
|
||||
{
|
||||
Slug = "kobo-ebook-gb",
|
||||
Url = "https://www.kobo.com/gb/en/ebook/the-alpha-flame",
|
||||
Retailer = "Kobo",
|
||||
Format = "eBook",
|
||||
CountryGroup = "GB"
|
||||
},
|
||||
["kobo-ebook-us"] = new BuyLink
|
||||
{
|
||||
Slug = "kobo-ebook-us",
|
||||
Url = "https://www.kobo.com/us/en/ebook/the-alpha-flame",
|
||||
Retailer = "Kobo",
|
||||
Format = "eBook",
|
||||
CountryGroup = "US"
|
||||
},
|
||||
["kobo-ebook-ca"] = new BuyLink
|
||||
{
|
||||
Slug = "kobo-ebook-ca",
|
||||
Url = "https://www.kobo.com/ca/en/ebook/the-alpha-flame",
|
||||
Retailer = "Kobo",
|
||||
Format = "eBook",
|
||||
CountryGroup = "CA"
|
||||
},
|
||||
["kobo-ebook-au"] = new BuyLink
|
||||
{
|
||||
Slug = "kobo-ebook-au",
|
||||
Url = "https://www.kobo.com/au/en/ebook/the-alpha-flame",
|
||||
Retailer = "Kobo",
|
||||
Format = "eBook",
|
||||
CountryGroup = "AU"
|
||||
},
|
||||
["kobo-ebook-ie"] = new BuyLink
|
||||
{
|
||||
Slug = "kobo-ebook-ie",
|
||||
Url = "https://www.kobo.com/ie/en/ebook/the-alpha-flame",
|
||||
Retailer = "Kobo",
|
||||
Format = "eBook",
|
||||
CountryGroup = "IE"
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
public BuyController(DataAccess dataAccess)
|
||||
{
|
||||
_dataAccess = dataAccess;
|
||||
@ -193,30 +278,39 @@ namespace CatherineLynwood.Controllers
|
||||
[HttpGet("{slug}")]
|
||||
public async Task<IActionResult> Go(string slug)
|
||||
{
|
||||
if (!Links.TryGetValue(slug, out var link))
|
||||
return NotFound();
|
||||
if (!Links.TryGetValue(slug, out var link)) return NotFound();
|
||||
|
||||
var referer = Request.Headers["Referer"].ToString();
|
||||
var ua = Request.Headers["User-Agent"].ToString();
|
||||
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 page = referer; // or: $"{Request.Scheme}://{Request.Host}{Request.PathBase}{Request.Path}"
|
||||
var countryQS = Request.Query["country"].ToString(); // if you pass ?country=GB for testing
|
||||
|
||||
// session id cookie
|
||||
var sessionId = Request.Cookies["sid"] ?? Guid.NewGuid().ToString("N");
|
||||
if (!Request.Cookies.ContainsKey("sid"))
|
||||
if (HttpMethods.IsHead(Request.Method)) return Redirect(link.Url);
|
||||
if (IsLikelyBot(ua)) return Redirect(link.Url);
|
||||
|
||||
var fetchMode = Request.Headers["Sec-Fetch-Mode"].ToString();
|
||||
if (!string.Equals(fetchMode, "navigate", StringComparison.OrdinalIgnoreCase))
|
||||
return Redirect(link.Url);
|
||||
|
||||
if (!IsOurSiteReferer(referer, Request.Host.Host))
|
||||
return Redirect(link.Url);
|
||||
|
||||
// Ensure we have a first-party session id
|
||||
var sessionId = Request.Cookies["sid"];
|
||||
if (string.IsNullOrEmpty(sessionId))
|
||||
{
|
||||
sessionId = Guid.NewGuid().ToString("N");
|
||||
Response.Cookies.Append("sid", sessionId, new CookieOptions
|
||||
{
|
||||
HttpOnly = true,
|
||||
SameSite = SameSiteMode.Lax,
|
||||
Secure = true,
|
||||
MaxAge = TimeSpan.FromDays(365)
|
||||
MaxAge = TimeSpan.FromDays(365),
|
||||
Path = "/"
|
||||
});
|
||||
// carry on and log this click
|
||||
}
|
||||
|
||||
// Save click (don’t block the redirect if this fails)
|
||||
_ = await _dataAccess.SaveBuyClick(
|
||||
dateTimeUtc: DateTime.UtcNow,
|
||||
slug: link.Slug,
|
||||
@ -224,17 +318,42 @@ namespace CatherineLynwood.Controllers
|
||||
format: link.Format,
|
||||
countryGroup: link.CountryGroup,
|
||||
ip: ip,
|
||||
country: string.IsNullOrWhiteSpace(countryQS) ? null : countryQS, // optional override
|
||||
country: Request.Query["country"].ToString(),
|
||||
userAgent: ua,
|
||||
referer: referer,
|
||||
page: page,
|
||||
page: referer,
|
||||
sessionId: sessionId,
|
||||
queryString: qs,
|
||||
destinationUrl: link.Url
|
||||
);
|
||||
|
||||
return Redirect(link.Url); // 302
|
||||
return Redirect(link.Url);
|
||||
}
|
||||
|
||||
|
||||
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.ToLowerInvariant()));
|
||||
}
|
||||
|
||||
private static bool IsOurSiteReferer(string referer, string ourHost)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(referer)) return false;
|
||||
if (!Uri.TryCreate(referer, UriKind.Absolute, out var uri)) return false;
|
||||
// handle www. and subdomains as you prefer
|
||||
return uri.Host.EndsWith(ourHost, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -195,10 +195,13 @@ namespace CatherineLynwood.Controllers
|
||||
var reviewObjects = new List<Dictionary<string, object>>();
|
||||
double total = 0;
|
||||
|
||||
foreach (var review in reviews.Items.Take(take))
|
||||
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",
|
||||
|
||||
35
CatherineLynwood/Middleware/EnsureSidMiddleware.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -111,6 +111,7 @@ namespace CatherineLynwood
|
||||
app.UseMiddleware<SpamAndSecurityMiddleware>();
|
||||
app.UseMiddleware<HoneypotLoggingMiddleware>();
|
||||
app.UseMiddleware<RedirectMiddleware>();
|
||||
app.UseMiddleware<EnsureSidMiddleware>();
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseResponseCompression();
|
||||
|
||||
@ -40,32 +40,53 @@
|
||||
<div class="card-body d-flex flex-column">
|
||||
<!-- Trailer -->
|
||||
<div class="trailer-wrapper mb-3">
|
||||
<video id="trailerVideo" playsinline preload="none" class="w-100"></video>
|
||||
<!-- Mobile / tablet: PORTRAIT -->
|
||||
<video id="trailerPortrait"
|
||||
class="w-100 d-block d-lg-none"
|
||||
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>
|
||||
|
||||
<!-- 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>
|
||||
</div>
|
||||
|
||||
<!-- Compact trust panel -->
|
||||
<div class="mb-3">
|
||||
<div class="border border-2 border-dark rounded-4 p-3 bg-light">
|
||||
<div class="small text-muted text-uppercase mb-2">Formats & delivery</div>
|
||||
<ul class="list-unstyled small mb-0">
|
||||
<li class="mb-1"><i class="fad fa-gem me-1"></i> Hardback, collector’s edition</li>
|
||||
<li class="mb-1"><i class="fad fa-book me-1"></i> Paperback, bookshop edition</li>
|
||||
<li class="mb-1"><i class="fad fa-shipping-fast me-1"></i> Fast fulfilment in GB and US via our print partner</li>
|
||||
<li><i class="fad fa-store me-1"></i> Also available from Amazon, Waterstones, Barnes & Noble</li>
|
||||
</ul>
|
||||
</div>
|
||||
</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 -->
|
||||
<div id="buyBox" class="border border-2 border-dark rounded-4 p-3 bg-light mt-auto">
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between mb-2">
|
||||
<h3 class="h5 mb-2 mb-sm-0">Buy the Book</h3>
|
||||
<small class="text-muted">Best options for your country</small>
|
||||
<small id="buyCountryHint" class="text-muted d-flex align-items-center">
|
||||
<img id="buyCountryFlag" class="me-1 d-none" alt="" width="20" height="14" loading="lazy">
|
||||
<span id="buyCountryText">Best options for your country</span>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Row 1: Direct via Ingram (GB/US only) -->
|
||||
<div id="rowDirect" class="mb-3">
|
||||
<div class="d-flex align-items-center gap-2 mb-2">
|
||||
@ -78,6 +99,7 @@
|
||||
<a id="hardbackLinkSelf"
|
||||
href="/go/ingram-hardback-gb"
|
||||
target="_blank"
|
||||
rel="nofollow noindex"
|
||||
class="btn btn-dark w-100"
|
||||
data-retailer="Ingram" data-format="Hardback">
|
||||
<i class="fad fa-gem me-1"></i> Hardback, direct
|
||||
@ -87,6 +109,7 @@
|
||||
<a id="paperbackLinkSelf"
|
||||
href="/go/ingram-paperback-gb"
|
||||
target="_blank"
|
||||
rel="nofollow noindex"
|
||||
class="btn btn-dark w-100"
|
||||
data-retailer="Ingram" data-format="Paperback">
|
||||
<i class="fad fa-book me-1"></i> Paperback, direct
|
||||
@ -94,7 +117,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center gap-2 mb-2">
|
||||
<span class="badge bg-dark">Other retailers</span>
|
||||
<span class="small text-muted">From other retailers</span>
|
||||
</div>
|
||||
<!-- Row 2: Amazon -->
|
||||
<div id="rowAmazon" class="mb-3">
|
||||
<div class="row g-2">
|
||||
@ -102,6 +128,7 @@
|
||||
<a id="hardbackLink"
|
||||
href="/go/amazon-hardback-gb"
|
||||
target="_blank"
|
||||
rel="nofollow noindex"
|
||||
class="btn btn-outline-dark w-100"
|
||||
data-retailer="Amazon" data-format="Hardback">
|
||||
<i class="fad fa-gem me-1"></i> Hardback, Amazon
|
||||
@ -111,6 +138,7 @@
|
||||
<a id="paperbackLink"
|
||||
href="/go/amazon-paperback-gb"
|
||||
target="_blank"
|
||||
rel="nofollow noindex"
|
||||
class="btn btn-outline-dark w-100"
|
||||
data-retailer="Amazon" data-format="Paperback">
|
||||
<i class="fad fa-book me-1"></i> Paperback, Amazon
|
||||
@ -126,6 +154,7 @@
|
||||
<a id="hardbackNational"
|
||||
href="/go/waterstones-hardback-gb"
|
||||
target="_blank"
|
||||
rel="nofollow noindex"
|
||||
class="btn btn-outline-dark w-100"
|
||||
data-retailer="National" data-format="Hardback">
|
||||
<i class="fad fa-gem me-1"></i> Hardback, Waterstones
|
||||
@ -135,6 +164,7 @@
|
||||
<a id="paperbackNational"
|
||||
href="/go/waterstones-paperback-gb"
|
||||
target="_blank"
|
||||
rel="nofollow noindex"
|
||||
class="btn btn-outline-dark w-100"
|
||||
data-retailer="National" data-format="Paperback">
|
||||
<i class="fad fa-book-open me-1"></i> Paperback, Waterstones
|
||||
@ -143,11 +173,47 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quiet eBook -->
|
||||
<div class="mt-1 small">
|
||||
Prefer Kindle? <a id="kindleLink" href="/go/amazon-kindle-gb" target="_blank" class="link-dark" data-retailer="Amazon" data-format="Kindle">Buy the eBook</a>
|
||||
<!-- Row 4: eBook stores -->
|
||||
<div id="rowEbooks" class="mb-2">
|
||||
<div class="d-flex align-items-center gap-2 mb-2">
|
||||
<span class="badge bg-dark">Instant reading</span>
|
||||
<span class="small text-muted">Choose your preferred store</span>
|
||||
</div>
|
||||
<div class="row g-2">
|
||||
<div class="col-12 col-sm-4">
|
||||
<a id="ebookApple"
|
||||
href="/go/apple-ebook-gb"
|
||||
target="_blank"
|
||||
rel="nofollow noindex"
|
||||
class="btn btn-outline-dark w-100"
|
||||
data-retailer="Apple Books" data-format="eBook">
|
||||
<i class="fab fa-apple me-1"></i> Apple Books
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-12 col-sm-4">
|
||||
<a id="ebookKobo"
|
||||
href="/go/kobo-ebook-gb"
|
||||
target="_blank"
|
||||
rel="nofollow noindex"
|
||||
class="btn btn-outline-dark w-100"
|
||||
data-retailer="Kobo" data-format="eBook">
|
||||
<i class="fad fa-book-open me-1"></i> Kobo
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-12 col-sm-4">
|
||||
<a id="ebookKindle"
|
||||
href="/go/amazon-kindle-gb"
|
||||
target="_blank"
|
||||
rel="nofollow noindex"
|
||||
class="btn btn-outline-dark w-100"
|
||||
data-retailer="Amazon" data-format="Kindle">
|
||||
<i class="fab fa-amazon me-1"></i> Kindle
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mt-2">
|
||||
<a asp-action="HowToBuy" class="link-dark small">See all buying options</a>
|
||||
</div>
|
||||
@ -170,7 +236,7 @@
|
||||
<!-- Social proof: show one standout review near the top -->
|
||||
@if (showReviews)
|
||||
{
|
||||
var top = Model.Items.First();
|
||||
var top = Model.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);
|
||||
@ -223,57 +289,65 @@
|
||||
</section>
|
||||
}
|
||||
|
||||
<!-- Synopsis: short teaser with collapse for the full version -->
|
||||
<!-- Synopsis: open on mobile, collapsible on desktop -->
|
||||
<section id="synopsis" class="mb-4">
|
||||
<div class="card character-card">
|
||||
<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-muted small mb-0">Listen to Catherine talking about the book</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Teaser paragraph -->
|
||||
<p class="card-text">
|
||||
Set in 1983 Birmingham, 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>
|
||||
|
||||
<!-- Collapse trigger -->
|
||||
<p class="mb-2">
|
||||
<a class="btn btn-outline-dark btn-sm" data-bs-toggle="collapse" href="#fullSynopsis" role="button" aria-expanded="false" aria-controls="fullSynopsis">
|
||||
Read full synopsis
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<!-- Full synopsis in collapse -->
|
||||
<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. Beth’s 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 doesn’t just live; she races through life, fuelled by a need for speed and the thrill of freedom. Her Triumph TR6 isn’t just a car; it’s 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 Maggie’s intensity doesn’t 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 isn’t 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 she’s 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 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. Beth’s 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 doesn’t just live; she races through life, fuelled by a need for speed and the thrill of freedom. Her Triumph TR6 isn’t just a car; it’s 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 Maggie’s intensity doesn’t 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 isn’t 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 she’s 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>
|
||||
@ -297,7 +371,7 @@
|
||||
</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>
|
||||
<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>
|
||||
@ -326,149 +400,247 @@
|
||||
<!-- Trailer source selection + play button -->
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const video = document.getElementById("trailerVideo");
|
||||
const playBtn = document.getElementById("trailerPlayBtn");
|
||||
if (!video || !playBtn) return;
|
||||
const vPortrait = document.getElementById("trailerPortrait");
|
||||
const vLandscape = document.getElementById("trailerLandscape");
|
||||
const playBtn = document.getElementById("trailerPlayBtn");
|
||||
if (!playBtn) return;
|
||||
|
||||
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";
|
||||
// Which video is currently visible according to Bootstrap’s lg breakpoint (>=992px)?
|
||||
const isDesktop = () => window.matchMedia("(min-width: 992px)").matches;
|
||||
const activeVideo = () => isDesktop() ? vLandscape : vPortrait;
|
||||
|
||||
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);
|
||||
// Hide native controls when JS is active; custom button will start playback
|
||||
[vPortrait, vLandscape].forEach(v => { if (v) v.controls = false; });
|
||||
|
||||
// Start playback on whichever video is visible
|
||||
playBtn.addEventListener("click", () => {
|
||||
video.muted = false;
|
||||
video.volume = 1.0;
|
||||
video.play().then(() => {
|
||||
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);
|
||||
});
|
||||
}).catch(err => console.warn("Video play failed:", err));
|
||||
});
|
||||
|
||||
// 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"; }, { once: true });
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('click', function(e){
|
||||
const a = e.target.closest('a[href^="/go/"]');
|
||||
if (!a || typeof fbq !== 'function') return;
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const el = document.getElementById('fullSynopsis');
|
||||
if (!el || !window.bootstrap?.Collapse) return;
|
||||
|
||||
// Extract slug, retailer/format from attributes if present
|
||||
const slug = a.getAttribute('href').split('/').pop();
|
||||
const retailer = a.dataset.retailer || null;
|
||||
const format = a.dataset.format || null;
|
||||
// Initialise without auto-toggling
|
||||
const c = new bootstrap.Collapse(el, { toggle: false });
|
||||
const mq = window.matchMedia('(min-width: 768px)'); // Bootstrap md
|
||||
|
||||
// Event ID for matching with server-side (CAPI) event
|
||||
const eventId = crypto && crypto.randomUUID ? crypto.randomUUID() : (Date.now() + '-' + Math.random());
|
||||
function setInitial() {
|
||||
if (mq.matches) {
|
||||
// Desktop: keep collapsed
|
||||
c.hide();
|
||||
} else {
|
||||
// Mobile: open by default
|
||||
c.show();
|
||||
}
|
||||
}
|
||||
|
||||
// Attach eventId so the server can read it on the next request (optional)
|
||||
a.dataset.eventId = eventId;
|
||||
|
||||
fbq('trackCustom', 'BuyClick', {
|
||||
slug, retailer, format, page: location.pathname
|
||||
}, {eventID: eventId});
|
||||
setInitial();
|
||||
// Optional: if user resizes across the breakpoint, adjust state
|
||||
mq.addEventListener?.('change', setInitial);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
// map formats to IDs for richer reporting
|
||||
const CONTENT_IDS = {
|
||||
Hardback: '9781068225802',
|
||||
Paperback: '9781068225819',
|
||||
Kindle: 'B0FBS427VD'
|
||||
};
|
||||
|
||||
function utms() {
|
||||
const p = new URLSearchParams(location.search);
|
||||
return {
|
||||
utm_source: p.get('utm_source') || null,
|
||||
utm_medium: p.get('utm_medium') || null,
|
||||
utm_campaign: p.get('utm_campaign') || null,
|
||||
utm_content: p.get('utm_content') || null,
|
||||
utm_term: p.get('utm_term') || null
|
||||
};
|
||||
}
|
||||
|
||||
document.addEventListener('click', function(e){
|
||||
const a = e.target.closest('a[data-go-slug]');
|
||||
if (!a) return;
|
||||
|
||||
const slug = a.getAttribute('data-go-slug');
|
||||
const goUrl = '/go/' + encodeURIComponent(slug);
|
||||
const target = a.getAttribute('target');
|
||||
const retailer = a.dataset.retailer || null;
|
||||
const format = a.dataset.format || null;
|
||||
const cid = CONTENT_IDS[format] || null;
|
||||
|
||||
// fire FB event if available
|
||||
const params = {
|
||||
content_type: 'product',
|
||||
content_ids: cid ? [cid] : null,
|
||||
content_name: 'The Alpha Flame: Discovery',
|
||||
content_category: 'Books',
|
||||
slug, retailer, format,
|
||||
destination_hint: a.href,
|
||||
page_location: location.href,
|
||||
page_path: location.pathname,
|
||||
page_title: document.title,
|
||||
referrer: document.referrer || null,
|
||||
...utms()
|
||||
};
|
||||
try { if (typeof fbq === 'function') fbq('trackCustom', 'BuyClick', params); } catch {}
|
||||
|
||||
// Left click without modifiers → route via /go/
|
||||
const isLeft = e.button === 0 && !e.metaKey && !e.ctrlKey && !e.shiftKey && !e.altKey;
|
||||
if (isLeft) {
|
||||
e.preventDefault();
|
||||
setTimeout(() => {
|
||||
if (target === '_blank') {
|
||||
window.open(goUrl, '_blank', 'noopener');
|
||||
} else {
|
||||
window.location.href = goUrl;
|
||||
}
|
||||
}, 150);
|
||||
return;
|
||||
}
|
||||
|
||||
// Middle click or modifier keys → open /go/ in a new tab
|
||||
if (e.button === 1 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
||||
e.preventDefault();
|
||||
window.open(goUrl, '_blank', 'noopener');
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Geo-based slug swap for /go/{retailer}-{format}-{country} + dev override -->
|
||||
<script>
|
||||
(function() {
|
||||
function qsCountryOverride() {
|
||||
const qs = new URLSearchParams(location.search);
|
||||
const c = qs.get('country');
|
||||
return c ? c.toUpperCase() : null;
|
||||
}
|
||||
(function(){
|
||||
// Direct IngramSpark URLs
|
||||
const INGRAM_PB = "https://shop.ingramspark.com/b/084?params=6easpH54PaugzXFKdF4Tu4Izb0cvkMqbj3ZNlaYBKMJ";
|
||||
const INGRAM_HB = "https://shop.ingramspark.com/b/084?params=GC1p1c8b66Rhfoy6Tq97SJmmhdZSEYuxBcCY5zxNstO";
|
||||
|
||||
// Replace trailing "-gb" in slugs with target country code
|
||||
function swapCountrySlug(a, toCountry) {
|
||||
if (!a || !a.href) return;
|
||||
try {
|
||||
const url = new URL(a.href, location.origin);
|
||||
// Slugs are like: /go/amazon-hardback-gb
|
||||
url.pathname = url.pathname.replace(/-(gb|us|ca|au)$/i, '-' + toCountry.toLowerCase());
|
||||
// Also allow passing country through to /go for logging/testing
|
||||
url.searchParams.set('country', toCountry);
|
||||
a.href = url.pathname + url.search;
|
||||
} catch(e) { /* noop */ }
|
||||
}
|
||||
// Elements
|
||||
const elKindle = document.getElementById('kindleLink');
|
||||
const elPbSelf = document.getElementById('paperbackLinkSelf');
|
||||
const elHbSelf = document.getElementById('hardbackLinkSelf');
|
||||
const elPbAmazon = document.getElementById('paperbackLink'); // optional
|
||||
const elHbAmazon = document.getElementById('hardbackLink'); // optional
|
||||
const elHbNat = document.getElementById('hardbackNational');
|
||||
const elPbNat = document.getElementById('paperbackNational');
|
||||
|
||||
fetch('https://ipapi.co/json/')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const override = qsCountryOverride();
|
||||
const country = (override || data.country_code || 'US').toUpperCase();
|
||||
function show(el){ if (el) el.classList.remove('d-none'); }
|
||||
function hide(el){ if (el) el.classList.add('d-none'); }
|
||||
|
||||
// Core anchors
|
||||
const elKindle = document.getElementById("kindleLink");
|
||||
// helper to set both direct href and go slug
|
||||
function setLink(el, directHref, slug) {
|
||||
if (!el) return;
|
||||
el.href = directHref;
|
||||
el.setAttribute('data-go-slug', slug);
|
||||
}
|
||||
|
||||
const hbAmz = document.getElementById("hardbackLink");
|
||||
const pbAmz = document.getElementById("paperbackLink");
|
||||
window.applyLinks = function applyLinks(code) {
|
||||
const cc = (code || '').toUpperCase();
|
||||
|
||||
const hbNat = document.getElementById("hardbackNational");
|
||||
const pbNat = document.getElementById("paperbackNational");
|
||||
const rowNat = document.getElementById("rowNational");
|
||||
// Defaults to US Amazon
|
||||
let kindleHref = "https://www.amazon.com/dp/B0FBS427VD";
|
||||
let pbAmzHref = "https://www.amazon.com/dp/1068225815";
|
||||
let hbAmzHref = "https://www.amazon.com/dp/1068225807";
|
||||
|
||||
const rowDirect = document.getElementById("rowDirect");
|
||||
const hbSelf = document.getElementById("hardbackLinkSelf");
|
||||
const pbSelf = document.getElementById("paperbackLinkSelf");
|
||||
// National retailer defaults (GB Waterstones; US B&N)
|
||||
let hbNatHref = "https://www.waterstones.com/book/the-alpha-flame/catherine-lynwood/9781068225802";
|
||||
let pbNatHref = "https://www.waterstones.com/book/the-alpha-flame/catherine-lynwood/9781068225819";
|
||||
let hbNatSlug = "waterstones-hardback-gb";
|
||||
let pbNatSlug = "waterstones-paperback-gb";
|
||||
|
||||
// Always swap Amazon & Kindle to target country
|
||||
[hbAmz, pbAmz, elKindle].forEach(a => swapCountrySlug(a, country));
|
||||
switch (cc) {
|
||||
case 'GB':
|
||||
kindleHref = "https://www.amazon.co.uk/dp/B0FBS427VD";
|
||||
pbAmzHref = "https://www.amazon.co.uk/dp/1068225815";
|
||||
hbAmzHref = "https://www.amazon.co.uk/dp/1068225807";
|
||||
hbNatHref = "https://www.waterstones.com/book/the-alpha-flame/catherine-lynwood/9781068225802";
|
||||
pbNatHref = "https://www.waterstones.com/book/the-alpha-flame/catherine-lynwood/9781068225819";
|
||||
hbNatSlug = "waterstones-hardback-gb";
|
||||
pbNatSlug = "waterstones-paperback-gb";
|
||||
break;
|
||||
case 'US':
|
||||
// keep US Amazon defaults
|
||||
hbNatHref = "https://www.barnesandnoble.com/s/9781068225802";
|
||||
pbNatHref = "https://www.barnesandnoble.com/s/9781068225819";
|
||||
hbNatSlug = "bn-hardback-us";
|
||||
pbNatSlug = "bn-paperback-us";
|
||||
break;
|
||||
case 'CA':
|
||||
kindleHref = "https://www.amazon.ca/dp/B0FBS427VD";
|
||||
pbAmzHref = "https://www.amazon.ca/dp/1068225815";
|
||||
hbAmzHref = "https://www.amazon.ca/dp/1068225807";
|
||||
// keep GB Waterstones/B&N as-is if you prefer not to show national links in CA
|
||||
break;
|
||||
case 'AU':
|
||||
kindleHref = "https://www.amazon.com.au/dp/B0FBS427VD";
|
||||
pbAmzHref = "https://www.amazon.com.au/dp/1068225815";
|
||||
hbAmzHref = "https://www.amazon.com.au/dp/1068225807";
|
||||
break;
|
||||
case 'IE': // Ireland → use Amazon UK by default
|
||||
kindleHref = "https://www.amazon.co.uk/dp/B0FBS427VD";
|
||||
pbAmzHref = "https://www.amazon.co.uk/dp/1068225815";
|
||||
hbAmzHref = "https://www.amazon.co.uk/dp/1068225807";
|
||||
// keep Waterstones GB for national if you show it
|
||||
hbNatHref = "https://www.waterstones.com/book/the-alpha-flame/catherine-lynwood/9781068225802";
|
||||
pbNatHref = "https://www.waterstones.com/book/the-alpha-flame/catherine-lynwood/9781068225819";
|
||||
hbNatSlug = "waterstones-hardback-gb";
|
||||
pbNatSlug = "waterstones-paperback-gb";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Ingram Direct row only for GB/US
|
||||
const showDirect = country === "GB" || country === "US";
|
||||
if (rowDirect) rowDirect.classList.toggle("d-none", !showDirect);
|
||||
if (showDirect) {
|
||||
[hbSelf, pbSelf].forEach(a => swapCountrySlug(a, country)); // swap to -gb or -us
|
||||
} else {
|
||||
// If direct hidden, make Amazon buttons solid
|
||||
[hbAmz, pbAmz].forEach(el => el && el.classList.replace("btn-outline-dark", "btn-dark"));
|
||||
}
|
||||
// Kindle: direct Amazon, route via /go/ on click
|
||||
setLink(elKindle, kindleHref, `amazon-kindle-${cc || 'us'}`);
|
||||
|
||||
// National row: Waterstones (GB) or Barnes & Noble (US). Hidden elsewhere.
|
||||
if (country === "GB") {
|
||||
// Keep Waterstones slugs (already -gb), but ensure '?country=GB' param is present
|
||||
[hbNat, pbNat].forEach(a => swapCountrySlug(a, country));
|
||||
if (hbNat) hbNat.textContent = "Hardback, Waterstones";
|
||||
if (pbNat) pbNat.textContent = "Paperback, Waterstones";
|
||||
if (rowNat) rowNat.classList.remove("d-none");
|
||||
} else if (country === "US") {
|
||||
// Switch retailer in slug: waterstones-...-gb -> bn-...-us
|
||||
function switchToBN(a, format) {
|
||||
if (!a) return;
|
||||
try {
|
||||
const url = new URL(a.href, location.origin);
|
||||
url.pathname = url.pathname
|
||||
.replace(/waterstones-(hardback|paperback)-(gb|us|ca|au)/i, (m, _fmt) => {
|
||||
return `bn-${format.toLowerCase()}-us`;
|
||||
});
|
||||
url.searchParams.set('country', 'US');
|
||||
a.href = url.pathname + url.search;
|
||||
} catch(e) {}
|
||||
}
|
||||
switchToBN(hbNat, "Hardback");
|
||||
switchToBN(pbNat, "Paperback");
|
||||
if (hbNat) hbNat.textContent = "Hardback, Barnes & Noble";
|
||||
if (pbNat) pbNat.textContent = "Paperback, Barnes & Noble";
|
||||
if (rowNat) rowNat.classList.remove("d-none");
|
||||
} else {
|
||||
if (rowNat) rowNat.classList.add("d-none");
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// If geo fails, GB defaults remain; users can still click, and /go logs with country=GB
|
||||
});
|
||||
})();
|
||||
// Ingram “Self” buttons: direct Ingram in GB/US only; hide elsewhere
|
||||
if (cc === 'GB' || cc === 'US') {
|
||||
setLink(elPbSelf, INGRAM_PB, `ingram-paperback-${cc.toLowerCase()}`);
|
||||
setLink(elHbSelf, INGRAM_HB, `ingram-hardback-${cc.toLowerCase()}`);
|
||||
show(elPbSelf); show(elHbSelf);
|
||||
} else {
|
||||
hide(elPbSelf); hide(elHbSelf);
|
||||
}
|
||||
|
||||
// Amazon print buttons (if present): direct Amazon, route via /go/
|
||||
setLink(elPbAmazon, pbAmzHref, `amazon-paperback-${cc || 'us'}`);
|
||||
setLink(elHbAmazon, hbAmzHref, `amazon-hardback-${cc || 'us'}`);
|
||||
|
||||
// National retailer row: direct URLs, route via /go/
|
||||
setLink(elHbNat, hbNatHref, hbNatSlug);
|
||||
setLink(elPbNat, pbNatHref, pbNatSlug);
|
||||
|
||||
// Toggle any Ingram-only containers you’ve marked
|
||||
document.querySelectorAll('[data-ingram-only="true"]').forEach(x => {
|
||||
if (cc === 'GB' || cc === 'US') x.classList.remove('d-none'); else x.classList.add('d-none');
|
||||
});
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
}
|
||||
|
||||
@section Meta {
|
||||
@ -481,7 +653,7 @@
|
||||
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, 06)"
|
||||
article-modified-time="@new DateTime(2025, 09, 10)"
|
||||
twitter-card-type="summary_large_image"
|
||||
twitter-site-handle="@@CathLynwood"
|
||||
twitter-creator-handle="@@CathLynwood" />
|
||||
|
||||
@ -14,16 +14,21 @@
|
||||
<script>
|
||||
// Load Meta Pixel ONLY if consent was given
|
||||
(function () {
|
||||
if (localStorage.getItem('marketingConsent') !== 'yes') return;
|
||||
if (localStorage.getItem('marketingConsent') !== 'yes') return;
|
||||
|
||||
!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?
|
||||
n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;
|
||||
n.push=n; n.loaded=!0; n.version='2.0'; n.queue=[]; t=b.createElement(e); t.async=!0;
|
||||
t.src=v; s=b.getElementsByTagName(e)[0]; s.parentNode.insertBefore(t,s)}(window, document,'script',
|
||||
'https://connect.facebook.net/en_US/fbevents.js');
|
||||
!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?
|
||||
n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;
|
||||
n.push=n; n.loaded=!0; n.version='2.0'; n.queue=[]; t=b.createElement(e); t.async=!0;
|
||||
t.src=v; s=b.getElementsByTagName(e)[0]; s.parentNode.insertBefore(t,s)}(window, document,'script',
|
||||
'https://connect.facebook.net/en_US/fbevents.js');
|
||||
|
||||
fbq('init', '556474687460834');
|
||||
fbq('track', 'PageView');
|
||||
fbq('init', '556474687460834');
|
||||
fbq('track', 'PageView', {
|
||||
page_location: location.href,
|
||||
page_path: location.pathname,
|
||||
page_title: document.title,
|
||||
referrer: document.referrer || null
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<noscript>
|
||||
@ -195,33 +200,41 @@
|
||||
<p>
|
||||
<a class="text-light" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy Policy</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a class="text-light" href="#" onclick="manageCookies();return false;">Manage cookies</a>
|
||||
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<div id="cookieBanner"
|
||||
class="d-none position-fixed bottom-0 start-0 end-0 bg-light border-top border-3 border-dark shadow-lg"
|
||||
style="z-index: 1080;">
|
||||
<div class="container py-3">
|
||||
<div class="d-flex flex-column flex-md-row align-items-md-center justify-content-between gap-2">
|
||||
<div class="me-md-3">
|
||||
<strong class=text-dark>Cookies & privacy.</strong>
|
||||
<span class="text-muted small">
|
||||
We use necessary cookies and, with your permission, marketing cookies (e.g. Meta Pixel) to measure and improve.
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button id="cookieReject" type="button" class="btn btn-outline-dark btn-sm">Reject</button>
|
||||
<button id="cookieAccept" type="button" class="btn btn-dark btn-sm">Accept</button>
|
||||
</div>
|
||||
<div class="modal fade"
|
||||
id="cookieModal"
|
||||
tabindex="-1"
|
||||
aria-labelledby="cookieTitle"
|
||||
aria-hidden="true"
|
||||
data-bs-backdrop="static"
|
||||
data-bs-keyboard="false"
|
||||
data-nosnippet>
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content border border-3 border-dark rounded-4 shadow-lg" data-nosnippet>
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title text-dark" id="cookieTitle">Cookies & privacy</h5>
|
||||
</div>
|
||||
<div class="modal-body" data-nosnippet>
|
||||
<p class="text-dark mb-2">We use necessary cookies and, with your permission, marketing cookies…</p>
|
||||
<p class="small text-muted mb-0">You can change your choice anytime…</p>
|
||||
</div>
|
||||
<div class="modal-footer d-flex gap-2" data-nosnippet>
|
||||
<button type="button" class="btn btn-outline-dark" id="cookieReject">Reject</button>
|
||||
<button type="button" class="btn btn-dark" id="cookieAccept">Accept</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script src="~/lib/jquery/dist/jquery.min.js"></script>
|
||||
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="~/js/site.js" asp-append-version="true"></script>
|
||||
@ -236,33 +249,45 @@
|
||||
@RenderSection("Scripts", required: false)
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const banner = document.getElementById('cookieBanner');
|
||||
const consent = localStorage.getItem('marketingConsent');
|
||||
(function () {
|
||||
const key = 'marketingConsent';
|
||||
const choice = localStorage.getItem(key);
|
||||
const modalEl = document.getElementById('cookieModal');
|
||||
|
||||
// Show banner if no decision yet
|
||||
if (!consent) banner.classList.remove('d-none');
|
||||
// Bootstrap modal API
|
||||
const modal = new bootstrap.Modal(modalEl, { backdrop: 'static', keyboard: false });
|
||||
|
||||
document.getElementById('cookieAccept')?.addEventListener('click', function () {
|
||||
localStorage.setItem('marketingConsent', 'yes');
|
||||
banner.classList.add('d-none');
|
||||
// reload so the pixel loader in <head> runs
|
||||
location.reload();
|
||||
});
|
||||
// Show only if not decided
|
||||
if (!choice) {
|
||||
modal.show();
|
||||
|
||||
document.getElementById('cookieReject')?.addEventListener('click', function () {
|
||||
localStorage.setItem('marketingConsent', 'no');
|
||||
banner.classList.add('d-none');
|
||||
});
|
||||
// (Optional) nudge after 3s: shake Accept button a bit
|
||||
setTimeout(() => {
|
||||
document.getElementById('cookieAccept')?.classList.add('btn-pulse');
|
||||
setTimeout(()=>document.getElementById('cookieAccept')?.classList.remove('btn-pulse'), 1200);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Optional: expose a simple “Manage cookies” helper you can call from a footer link
|
||||
window.manageCookies = function () {
|
||||
banner.classList.remove('d-none');
|
||||
};
|
||||
})();
|
||||
document.getElementById('cookieAccept')?.addEventListener('click', () => {
|
||||
localStorage.setItem(key, 'yes');
|
||||
modal.hide();
|
||||
location.reload(); // loads pixel
|
||||
});
|
||||
|
||||
document.getElementById('cookieReject')?.addEventListener('click', () => {
|
||||
localStorage.setItem(key, 'no');
|
||||
modal.hide();
|
||||
});
|
||||
|
||||
// Expose a footer hook to reopen
|
||||
window.manageCookies = function () {
|
||||
modal.show();
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
window.addEventListener("load", () => {
|
||||
setTimeout(() => {
|
||||
@ -313,8 +338,8 @@
|
||||
function docHeight() {
|
||||
return Math.max(
|
||||
document.body.scrollHeight, document.documentElement.scrollHeight,
|
||||
document.body.offsetHeight, document.documentElement.offsetHeight,
|
||||
document.body.clientHeight, document.documentElement.clientHeight
|
||||
document.body.offsetHeight, document.documentElement.offsetHeight,
|
||||
document.body.clientHeight, document.documentElement.clientHeight
|
||||
);
|
||||
}
|
||||
function canScroll() {
|
||||
|
||||
BIN
CatherineLynwood/wwwroot/images/jpg/synopsis-background-400.jpg
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
CatherineLynwood/wwwroot/images/synopsis-background.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 209 KiB |
|
After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 340 KiB |
|
After Width: | Height: | Size: 488 KiB |
|
After Width: | Height: | Size: 664 KiB |
|
After Width: | Height: | Size: 1.1 MiB |