diff --git a/CatherineLynwood/Controllers/BuyController.cs b/CatherineLynwood/Controllers/BuyController.cs index f82dbc0..19749ed 100644 --- a/CatherineLynwood/Controllers/BuyController.cs +++ b/CatherineLynwood/Controllers/BuyController.cs @@ -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 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); + } + + } } diff --git a/CatherineLynwood/Controllers/DiscoveryController.cs b/CatherineLynwood/Controllers/DiscoveryController.cs index 14a0a66..a5f2e33 100644 --- a/CatherineLynwood/Controllers/DiscoveryController.cs +++ b/CatherineLynwood/Controllers/DiscoveryController.cs @@ -195,10 +195,13 @@ namespace CatherineLynwood.Controllers var reviewObjects = new List>(); 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 { ["@type"] = "Review", diff --git a/CatherineLynwood/Middleware/EnsureSidMiddleware.cs b/CatherineLynwood/Middleware/EnsureSidMiddleware.cs new file mode 100644 index 0000000..4e5c836 --- /dev/null +++ b/CatherineLynwood/Middleware/EnsureSidMiddleware.cs @@ -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); + } + } + +} diff --git a/CatherineLynwood/Program.cs b/CatherineLynwood/Program.cs index d9a7ec4..f741afe 100644 --- a/CatherineLynwood/Program.cs +++ b/CatherineLynwood/Program.cs @@ -111,6 +111,7 @@ namespace CatherineLynwood app.UseMiddleware(); app.UseMiddleware(); app.UseMiddleware(); + app.UseMiddleware(); app.UseHttpsRedirection(); app.UseResponseCompression(); diff --git a/CatherineLynwood/Views/Discovery/Index1.cshtml b/CatherineLynwood/Views/Discovery/Index1.cshtml index 87d0828..a5e5b91 100644 --- a/CatherineLynwood/Views/Discovery/Index1.cshtml +++ b/CatherineLynwood/Views/Discovery/Index1.cshtml @@ -40,32 +40,53 @@
- + + + + + +
- -
-
-
Formats & delivery
-
    -
  • Hardback, collector’s edition
  • -
  • Paperback, bookshop edition
  • -
  • Fast fulfilment in GB and US via our print partner
  • -
  • Also available from Amazon, Waterstones, Barnes & Noble
  • -
-
-
+ +

Buy the Book

- Best options for your country + + + Best options for your country +
+ +
- +
+ Other retailers + From other retailers +
- -
- Prefer Kindle? Buy the eBook + +
+
+ Instant reading + Choose your preferred store +
+
+ @@ -170,7 +236,7 @@ @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 @@ } - +
-
-
-

The Alpha Flame: Discovery

-

Survival, secrets, and sisters in 1983 Birmingham.

-
-
- -
-
- -
-
-
- -
-

Listen to Catherine talking about the book

-
-
- - -

- 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. -

- - -

- -

- - -
-
-

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.

-

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.

-

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.

-

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.

-

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.

-

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.

-
-
-
+
+
+

The Alpha Flame: Discovery

+

Survival, secrets, and sisters in 1983 Birmingham.

+ +
+ +
+
+ +
+
+
+ +
+

Listen to Catherine talking about the book

+
+
+ + +

+ 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. +

+ + +

+ +

+ + +
+
+

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.

+

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.

+

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.

+

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.

+

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.

+

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.

+
+
+
+
+ +

Chapter Previews

@@ -297,7 +371,7 @@

Chapter 2: The Last Lesson — Maggie

-

On Christmas Eve, Maggie nervously heads out for her driving test...

+

On Christmas Eve, Maggie nervously heads out for her driving test not knowing the story that will pan out before her...

@@ -326,149 +400,247 @@ + + + + + + + } @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" /> diff --git a/CatherineLynwood/Views/Shared/_Layout.cshtml b/CatherineLynwood/Views/Shared/_Layout.cshtml index cfa1ce3..37fb698 100644 --- a/CatherineLynwood/Views/Shared/_Layout.cshtml +++ b/CatherineLynwood/Views/Shared/_Layout.cshtml @@ -14,16 +14,21 @@
-
-
-
-
- Cookies & privacy. - - We use necessary cookies and, with your permission, marketing cookies (e.g. Meta Pixel) to measure and improve. - -
-
- - -
+
- @@ -236,33 +249,45 @@ @RenderSection("Scripts", required: false) +