feat: Hugo engine for iGaming affiliate template (layouts, assets, i18n, verified data, CI build script)

This commit is contained in:
pipeline
2026-07-16 06:07:22 +03:00
parent b9385a1f72
commit 1816ff2093
58 changed files with 5631 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
{{ define "main" }}
<section class="site-container section text-center">
<p class="eyebrow">Error 404</p>
<h1 class="mt-2 text-4xl font-extrabold tracking-tight sm:text-5xl">Page not found</h1>
<p class="mx-auto mt-4 max-w-md text-secondary/70">
The page you&rsquo;re looking for doesn&rsquo;t exist or may have moved. Let&rsquo;s get you back on track.
</p>
<div class="mt-8 flex flex-wrap justify-center gap-3">
<a href="{{ "/" | relURL }}" class="btn-primary">Back to home</a>
{{ with site.GetPage "faq" }}<a href="{{ .RelPermalink }}" class="btn-secondary">Visit the FAQ</a>{{ end }}
</div>
<div class="mx-auto mt-12 grid max-w-2xl gap-3 sm:grid-cols-3">
{{- range slice "slots" "bonuses" "responsible-gambling" -}}
{{ with site.GetPage . }}
<a href="{{ .RelPermalink }}" class="card card-hover block no-underline text-left">
<span class="font-semibold text-primary">{{ .LinkTitle }}</span>
</a>
{{ end }}
{{- end -}}
</div>
</section>
{{ end }}
+65
View File
@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="{{ .Site.LanguageCode | default "en" }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="{{ .Site.Data.brand.primaryColor }}">
{{ partial "head-seo.html" . }}
{{/* Brand + locale data exposed to client-side scripts (age gate, locale switcher).
safeJS prevents html/template from escaping the JSON into a quoted string. */}}
<script>
window.__BRAND__ = {{ dict "name" .Site.Data.brand.name "defaultLocale" .Site.Data.brand.defaultLocale "ageRestrictionDefault" .Site.Data.brand.ageRestrictionDefault | jsonify | safeJS }};
window.__LOCALES__ = {{ .Site.Data.locales | jsonify | safeJS }};
</script>
{{/* Add the age-gate class as early as possible to avoid a flash for confirmed visitors. */}}
<script>
(function () {
try {
if (localStorage.getItem("ageConfirmed") !== "1") {
document.documentElement.classList.add("age-gate-active");
}
} catch (e) {
document.documentElement.classList.add("age-gate-active");
}
})();
</script>
{{/* CSS pipeline: inject brand vars (ExecuteAsTemplate) -> Tailwind/PostCSS ->
minify (production only) -> fingerprint. Hugo's [minify] does not minify
Pipes resources, so we minify explicitly for production builds. */}}
{{ $css := resources.Get "css/main.css" | resources.ExecuteAsTemplate "css/main.css" . | css.PostCSS }}
{{ if hugo.IsProduction }}{{ $css = $css | minify }}{{ end }}
{{ $css = $css | fingerprint "sha256" }}
<link rel="stylesheet" href="{{ $css.RelPermalink }}" integrity="{{ $css.Data.Integrity }}" crossorigin="anonymous">
<link rel="icon" href="{{ "favicon.svg" | relURL }}" type="image/svg+xml">
</head>
<body class="flex min-h-screen flex-col bg-white">
<a href="#main" class="sr-only rounded bg-primary px-4 py-2 text-primary-contrast focus:not-sr-only focus:absolute focus:left-4 focus:top-4 focus:z-[110]">{{ i18n "skipToContent" }}</a>
{{ partial "age-gate-banner.html" . }}
{{ partial "header.html" . }}
<main id="main" class="flex-1">
{{ block "main" . }}{{ end }}
</main>
{{ partial "footer.html" . }}
{{ partial "cookie-consent.html" . }}
{{/* App bundle (age gate, cookie consent, games filter). Loads BEFORE Alpine so the
gamesFilter component is registered on the alpine:init event. */}}
{{ $appJs := slice (resources.Get "js/age-gate.js") (resources.Get "js/cookie-consent.js") (resources.Get "js/games-filter.js") | resources.Concat "js/app.js" | minify | fingerprint "sha256" }}
<script defer src="{{ $appJs.RelPermalink }}" integrity="{{ $appJs.Data.Integrity }}" crossorigin="anonymous"></script>
{{/* Alpine.js — vendored first-party (assets/js/vendor/alpine.js, pinned 3.14.8,
refreshed from node_modules by `npm run build:js`) and fingerprinted so it
ships with Subresource Integrity like the other assets. Deferred and placed
AFTER the app bundle so components register on alpine:init before core runs. */}}
{{ $alpine := resources.Get "js/vendor/alpine.js" | fingerprint "sha256" }}
<script defer src="{{ $alpine.RelPermalink }}" integrity="{{ $alpine.Data.Integrity }}" crossorigin="anonymous"></script>
</body>
</html>
+25
View File
@@ -0,0 +1,25 @@
{{ define "main" }}
{{ partial "breadcrumbs.html" . }}
<div class="site-container section">
<header class="mb-8 max-w-3xl">
<h1 class="text-3xl font-extrabold tracking-tight sm:text-4xl">{{ .Params.h1 | default .Title }}</h1>
{{ with .Params.description }}<p class="mt-3 text-lg text-secondary/70">{{ . }}</p>{{ end }}
</header>
{{ with .Content }}<div class="prose-content max-w-3xl">{{ . }}</div>{{ end }}
{{ with .Pages }}
<div class="mt-10 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{{- range . -}}
<a href="{{ .RelPermalink }}" class="card card-hover block no-underline">
<h2 class="text-lg font-bold">{{ .LinkTitle }}</h2>
<p class="mt-2 text-sm text-secondary/70">{{ .Params.description | default (.Summary | plainify) | truncate 120 }}</p>
</a>
{{- end -}}
</div>
{{ end }}
</div>
{{ partial "related-pages.html" . }}
{{ end }}
+70
View File
@@ -0,0 +1,70 @@
{{ define "main" }}
{{ partial "breadcrumbs.html" . }}
<article class="site-container section">
<header class="mb-8 max-w-3xl">
<h1 class="text-3xl font-extrabold tracking-tight sm:text-4xl">{{ .Params.h1 | default .Title }}</h1>
{{ with .Params.description }}<p class="mt-3 text-lg text-secondary/70">{{ . }}</p>{{ end }}
{{ if not .Lastmod.IsZero }}<p class="mt-2 text-xs text-secondary/70">Last updated {{ .Lastmod.Format "2 January 2006" }}</p>{{ end }}
</header>
{{ with .Content }}<div class="prose-content max-w-3xl">{{ . }}</div>{{ end }}
{{ if or .Params.pros .Params.cons }}
<section class="mt-12 max-w-3xl">
<h2 class="mb-4 text-2xl font-bold">Pros &amp; cons at a glance</h2>
{{ partial "pros-cons-table.html" . }}
</section>
{{ end }}
{{ if .Params.showRatingWidget }}
<section class="mt-12 max-w-3xl">
<h2 class="mb-4 text-2xl font-bold">Our verdict</h2>
{{ partial "rating-widget.html" . }}
</section>
{{ end }}
{{ if .Params.showKycByLocale }}
<section class="mt-12 max-w-4xl">
<h2 class="mb-3 text-2xl font-bold">Verification documents by region</h2>
<p class="mb-4 text-secondary/70">KYC requirements vary by market. Choose your region in the header to highlight your row; the full list is below.</p>
<div class="overflow-x-auto">
<table class="data-table">
<thead><tr><th scope="col">Region</th><th scope="col">Required documents (KYC)</th></tr></thead>
<tbody>
{{- range .Site.Data.locales -}}
<tr data-kyc-row data-locale="{{ .locale }}">
<th scope="row" class="font-medium">{{ .label | default .locale }}</th>
<td><ul class="list-disc space-y-1 pl-4">{{ range .requiredKycDocs }}<li>{{ . }}</li>{{ end }}</ul></td>
</tr>
{{- end -}}
</tbody>
</table>
</div>
</section>
{{ end }}
{{ if .Params.showFaqAccordion }}
<section class="mt-12">
<h2 class="mb-6 text-2xl font-bold">Frequently asked questions</h2>
{{ partial "faq-accordion.html" . }}
</section>
{{ end }}
{{ if .Params.responsibleGamblingSection }}
<section class="mt-12">
{{ partial "responsible-gambling-block.html" (dict "page" . "variant" "full") }}
</section>
{{ end }}
{{ if not .Params.hideCta }}
<div class="mt-12 max-w-3xl rounded-2xl border border-primary/20 bg-primary/5 p-6">
<h2 class="text-xl font-bold">Ready to get started?</h2>
<p class="mt-1 text-secondary/70"><span data-age-value>{{ .Site.Data.brand.ageRestrictionDefault }}</span>+ only. T&amp;Cs apply. Please gamble responsibly.</p>
<div class="mt-4">{{ partial "cta-button.html" (dict "page" . "text" "Visit Casino" "size" "lg") }}</div>
</div>
{{ end }}
</article>
{{ partial "related-pages.html" . }}
{{ end }}
+102
View File
@@ -0,0 +1,102 @@
{{ define "main" }}
{{ $brand := .Site.Data.brand }}
{{/* Hero */}}
<section class="hero-gradient text-white">
<div class="site-container grid items-center gap-10 py-16 lg:grid-cols-2 lg:py-24">
<div>
<p class="eyebrow text-white/80">{{ $brand.name }}</p>
<h1 class="mt-2 text-4xl font-extrabold leading-tight tracking-tight sm:text-5xl">{{ .Params.h1 | default .Title }}</h1>
<p class="mt-4 max-w-xl text-lg text-white/90">{{ .Params.description | default $brand.tagline }}</p>
<div class="mt-8 flex flex-wrap gap-3">
{{ partial "cta-button.html" (dict "page" . "text" "Play Now" "size" "lg") }}
{{ with site.GetPage "bonuses" }}<a href="{{ .RelPermalink }}" class="btn border-2 border-white/80 text-white hover:bg-white hover:text-secondary">See bonuses</a>{{ end }}
</div>
<p class="mt-5 text-sm text-white/70"><span data-age-value>{{ $brand.ageRestrictionDefault }}</span>+ only &bull; T&amp;Cs apply &bull; Please gamble responsibly</p>
</div>
<div class="relative">
{{ $hero := resources.Get "images/hero.png" }}
<div class="overflow-hidden rounded-2xl shadow-2xl ring-1 ring-white/20">
{{ partial "responsive-image.html" (dict
"resource" $hero
"alt" (printf "%s highlights" $brand.name)
"loading" "eager"
"class" "w-full"
"widths" (slice 600 900 1200)
"sizes" "(min-width:1024px) 50vw, 100vw") }}
</div>
</div>
</div>
</section>
{{/* USP blocks */}}
{{ with .Params.usps }}
<section class="site-container section">
<div class="grid gap-6 md:grid-cols-3">
{{- range . -}}
<div class="card card-hover">
<div class="mb-3 grid h-11 w-11 place-items-center rounded-lg bg-primary/10 text-primary">
{{- if .icon -}}
<span class="text-xl" aria-hidden="true">{{ .icon }}</span>
{{- else -}}
<svg class="h-6 w-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
{{- end -}}
</div>
<h2 class="text-lg font-bold">{{ .title }}</h2>
<p class="mt-2 text-secondary/70">{{ .text }}</p>
</div>
{{- end -}}
</div>
</section>
{{ end }}
{{/* Editorial intro (from _index.md body) */}}
{{ with .Content }}
<section class="site-container section pt-0">
<div class="prose-content max-w-3xl">{{ . }}</div>
</section>
{{ end }}
{{/* Quick shortcuts */}}
<section class="site-container section pt-0">
<h2 class="mb-6 text-2xl font-bold">Start here</h2>
<div class="grid gap-4 sm:grid-cols-3">
{{- range slice "registration" "slots" "bonuses" -}}
{{ with site.GetPage . }}
<a href="{{ .RelPermalink }}" class="card card-hover block no-underline">
<span class="badge-primary">{{ .LinkTitle }}</span>
<p class="mt-2 text-secondary/70">{{ .Params.description | truncate 90 }}</p>
<span class="mt-3 inline-block font-semibold text-primary">Explore &rarr;</span>
</a>
{{ end }}
{{- end -}}
</div>
</section>
{{/* Featured games */}}
{{ $featured := where (where .Site.RegularPages "Section" "slots") "Params.featured" true }}
{{ with $featured }}
<section class="site-container section pt-0">
<div class="mb-6 flex items-end justify-between">
<h2 class="text-2xl font-bold">Featured games</h2>
{{ with site.GetPage "slots" }}<a href="{{ .RelPermalink }}" class="font-semibold text-primary">All games &rarr;</a>{{ end }}
</div>
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-4">
{{- range first 4 . -}}
{{ $img := .Resources.GetMatch "thumb.*" }}
<article class="card card-hover overflow-hidden p-0">
<div class="aspect-[3/2] overflow-hidden bg-secondary/10">
{{ partial "responsive-image.html" (dict "resource" $img "alt" .Title "class" "h-full w-full object-cover" "widths" (slice 300 600)) }}
</div>
<div class="p-4">
<span class="badge-primary">{{ .Params.provider }}</span>
<h3 class="mt-2 font-bold"><a href="{{ .RelPermalink }}" class="no-underline hover:underline">{{ .Title }}</a></h3>
</div>
</article>
{{- end -}}
</div>
</section>
{{ end }}
{{ partial "related-pages.html" . }}
{{ end }}
+24
View File
@@ -0,0 +1,24 @@
{{- /* age-gate-banner.html — full-screen, non-dismissible age interstitial shown
on first visit. Visibility is driven by the .age-gate-active class on
<html> (set early in baseof.html and cleared by age-gate.js on confirm).
The age value is data-driven and updated per locale by the switcher. */ -}}
{{- $brand := .Site.Data.brand -}}
<div id="age-gate" class="age-gate" role="dialog" aria-modal="true" aria-labelledby="age-gate-title" aria-describedby="age-gate-desc">
<div class="age-gate-card">
<div class="mx-auto mb-4 grid h-16 w-16 place-items-center rounded-full bg-primary/10 text-xl font-extrabold text-primary">
<span><span data-age-value>{{ $brand.ageRestrictionDefault }}</span>+</span>
</div>
<h2 id="age-gate-title" class="text-2xl font-bold text-secondary">{{ i18n "ageGateTitle" }}</h2>
<p id="age-gate-desc" class="mt-3 text-secondary/80">
This website is intended for users aged <span data-age-value>{{ $brand.ageRestrictionDefault }}</span>+ only.
By entering, you confirm you meet the legal gambling age in your jurisdiction. Gamble responsibly.
</p>
<div class="mt-6 flex flex-col gap-3">
<button type="button" data-age-confirm class="btn-primary btn-lg">{{ i18n "ageGateConfirm" }}</button>
<button type="button" data-age-exit class="btn-ghost">{{ i18n "ageGateExit" }}</button>
</div>
<p class="mt-4 text-xs text-secondary/60">
Underage gambling is an offence. This is an independent affiliate resource, not the operator.
</p>
</div>
</div>
+32
View File
@@ -0,0 +1,32 @@
{{- /* breadcrumbs.html — visible breadcrumb trail + BreadcrumbList JSON-LD on all
non-home pages. Built from .Ancestors (reversed) + the current page. */ -}}
{{- if not .IsHome -}}
{{- $crumbs := slice -}}
{{- range .Ancestors.Reverse -}}
{{- $crumbs = $crumbs | append . -}}
{{- end -}}
{{- $crumbs = $crumbs | append . -}}
{{- $last := sub (len $crumbs) 1 -}}
<nav aria-label="Breadcrumb" class="site-container pt-6">
<ol class="flex flex-wrap items-center gap-1 text-sm text-secondary/60">
{{- range $i, $c := $crumbs -}}
{{- if $i }}<li aria-hidden="true" class="px-1 text-secondary/30">/</li>{{ end -}}
<li>
{{- if eq $i $last -}}
<span aria-current="page" class="font-medium text-secondary/80">{{ $c.LinkTitle }}</span>
{{- else -}}
<a href="{{ $c.RelPermalink }}" class="no-underline hover:text-primary hover:underline">{{ $c.LinkTitle }}</a>
{{- end -}}
</li>
{{- end -}}
</ol>
</nav>
{{- $items := slice -}}
{{- range $i, $c := $crumbs -}}
{{- $items = $items | append (dict "@type" "ListItem" "position" (add $i 1) "name" $c.LinkTitle "item" $c.Permalink) -}}
{{- end -}}
{{- $bc := dict "@context" "https://schema.org" "@type" "BreadcrumbList" "itemListElement" $items -}}
<script type="application/ld+json">{{ $bc | jsonify | safeJS }}</script>
{{- end -}}
+48
View File
@@ -0,0 +1,48 @@
{{- /* cookie-consent.html — GDPR consent banner (accept / reject / manage).
Non-essential scripts stay inert (type="text/plain" data-cookiecategory)
until the visitor consents; cookie-consent.js then activates them. */ -}}
<div id="cookie-consent" class="cookie-banner" role="region" aria-label="Cookie consent" hidden>
<div class="site-container">
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div class="max-w-2xl text-sm text-secondary/80">
<p class="font-semibold text-secondary">{{ i18n "cookieTitle" }}</p>
<p class="mt-1">
We use strictly necessary cookies to run this site and, with your consent, analytics and
marketing cookies to improve it. You can change your choice at any time.
{{ with site.GetPage "cookie-policy" }}<a href="{{ .RelPermalink }}" class="font-medium text-primary underline">Read our Cookie Policy</a>.{{ end }}
</p>
</div>
<div class="flex flex-wrap gap-2">
<button type="button" data-cc-accept-all class="btn-primary">{{ i18n "cookieAcceptAll" }}</button>
<button type="button" data-cc-reject class="btn-secondary">{{ i18n "cookieReject" }}</button>
<button type="button" data-cc-manage class="btn-ghost">{{ i18n "cookieManage" }}</button>
</div>
</div>
<div data-cc-panel hidden class="mt-4">
<div class="grid gap-3 sm:grid-cols-3">
<div class="cookie-toggle">
<input type="checkbox" checked disabled class="mt-1" aria-label="Necessary cookies (always on)">
<span><span class="block font-semibold text-secondary">{{ i18n "cookieNecessary" }}</span><span class="text-xs text-secondary/70">Always active. Required for core functionality.</span></span>
</div>
<label class="cookie-toggle">
<input type="checkbox" data-cc-category="analytics" class="mt-1">
<span><span class="block font-semibold text-secondary">{{ i18n "cookieAnalytics" }}</span><span class="text-xs text-secondary/70">Anonymous usage statistics.</span></span>
</label>
<label class="cookie-toggle">
<input type="checkbox" data-cc-category="marketing" class="mt-1">
<span><span class="block font-semibold text-secondary">{{ i18n "cookieMarketing" }}</span><span class="text-xs text-secondary/70">Personalised offers and campaign measurement.</span></span>
</label>
</div>
<div class="mt-3">
<button type="button" data-cc-save class="btn-primary">{{ i18n "cookieSave" }}</button>
</div>
</div>
</div>
</div>
{{/* Example gated script — stays inert until the visitor accepts "analytics".
Replace the body with your real analytics snippet (e.g. GA4/Plausible). */}}
<script type="text/plain" data-cookiecategory="analytics">
// console.log("Analytics enabled after consent");
</script>
+39
View File
@@ -0,0 +1,39 @@
{{- /* cta-button.html — the ONLY place an affiliate link is constructed.
Link = brand.affiliateBaseUrl ? brand.affiliateSubIdParam = <per-page sub-id>.
Never hardcode a raw affiliate URL anywhere else.
Call with a dict:
{{ partial "cta-button.html" (dict "page" . "text" "Play Now") }}
Optional keys: subid, style ("primary"|"secondary"), size ("lg"), class, block(bool).
Sub-id resolution (robust when .File is nil for generated pages):
explicit subid -> .File.ContentBaseName -> .Type -> "home"
ContentBaseName equals the classic BaseFileName for standard pages and,
unlike BaseFileName, yields the real slug for page bundles (e.g. games). */ -}}
{{- $page := .page -}}
{{- $brand := $page.Site.Data.brand -}}
{{- $text := .text | default "Visit Casino" -}}
{{- $style := .style | default "primary" -}}
{{- $size := .size | default "" -}}
{{- $subid := .subid | default "" -}}
{{- if not $subid -}}
{{- if $page.IsHome -}}
{{- $subid = "home" -}}
{{- else -}}
{{- with $page.File -}}{{- $subid = .ContentBaseName -}}{{- end -}}
{{- end -}}
{{- end -}}
{{- if not $subid -}}{{- $subid = $page.Type -}}{{- end -}}
{{- if or (eq $subid "_index") (eq $subid "") -}}{{- $subid = "home" -}}{{- end -}}
{{- $href := printf "%s?%s=%s" $brand.affiliateBaseUrl $brand.affiliateSubIdParam $subid -}}
{{- $classes := "btn-primary" -}}
{{- if eq $style "secondary" -}}{{- $classes = "btn-secondary" -}}{{- end -}}
{{- if eq $style "ghost" -}}{{- $classes = "btn-ghost" -}}{{- end -}}
{{- if $size -}}{{- $classes = printf "%s btn-%s" $classes $size -}}{{- end -}}
{{- if .block -}}{{- $classes = printf "%s w-full" $classes -}}{{- end -}}
{{- with .class -}}{{- $classes = printf "%s %s" $classes . -}}{{- end -}}
<a href="{{ $href }}" class="{{ $classes }}" rel="sponsored nofollow noopener" target="_blank" data-affiliate data-subid="{{ $subid }}">{{ $text }}</a>
+27
View File
@@ -0,0 +1,27 @@
{{- /* faq-accordion.html — accordion over data/faq.yaml (sorted by weight).
Progressive enhancement: each item is a native <details>/<summary>, so
every answer is fully readable and keyboard-operable with NO JavaScript.
Alpine (when present) layers single-open accordion behaviour on top via
x-bind:open + the native `toggle` event — never hiding content with
x-cloak. The FAQPage JSON-LD is emitted separately by schema-jsonld.html. */ -}}
{{- $faqs := sort .Site.Data.faq "weight" -}}
<div class="mx-auto max-w-3xl divide-y divide-secondary/10 rounded-xl border border-secondary/10" x-data="{ open: null }">
{{- range $i, $f := $faqs -}}
<details class="group"
x-bind:open="open === {{ $i }}"
@toggle="open = $event.target.open ? {{ $i }} : (open === {{ $i }} ? null : open)">
<summary id="faq-btn-{{ $i }}" aria-controls="faq-panel-{{ $i }}"
class="flex w-full cursor-pointer list-none items-center justify-between gap-4 px-5 py-4 text-left text-base font-semibold text-secondary [&::-webkit-details-marker]:hidden">
<span>{{ $f.question }}</span>
<svg class="h-5 w-5 shrink-0 text-primary transition-transform duration-200 group-open:rotate-180" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 9l6 6 6-6" />
</svg>
</summary>
<div id="faq-panel-{{ $i }}" role="region" aria-labelledby="faq-btn-{{ $i }}"
class="px-5 pb-5 text-secondary/80">
<p>{{ $f.answer }}</p>
{{- with $f.category }}<p class="mt-2 text-xs uppercase tracking-wide text-secondary/70">{{ . }}</p>{{ end -}}
</div>
</details>
{{- end -}}
</div>
+86
View File
@@ -0,0 +1,86 @@
{{- /* footer.html — appears on every page. Includes (per spec): affiliate
disclosure link, licence/authority text (from data/brand.yaml),
responsible-gambling links, cookie policy link, an "18+ Gamble
Responsibly" badge and a dynamic copyright year. */ -}}
{{- $brand := .Site.Data.brand -}}
{{- $footerPages := where .Site.Pages "Params.showInFooter" true -}}
{{- $primary := sort (where $footerPages "Params.footerGroup" "primary") "Weight" -}}
{{- $legal := sort (where $footerPages "Params.footerGroup" "legal") "Weight" -}}
<footer class="mt-16 bg-secondary text-white">
<div class="site-container grid gap-10 py-12 md:grid-cols-2 lg:grid-cols-4">
{{/* Brand + social */}}
<div>
<div class="flex items-center gap-2">
{{ partial "responsive-image.html" (dict "src" $brand.logoPath "alt" (printf "%s logo" $brand.name) "class" "h-9 w-9") }}
<span class="text-lg font-extrabold">{{ $brand.name }}</span>
</div>
<p class="mt-3 text-sm text-white/70">{{ $brand.tagline }}</p>
<div class="mt-4 flex items-center gap-3">
{{- range $brand.socialLinks -}}
<a href="{{ .url }}" target="_blank" rel="noopener nofollow" class="text-white/70 hover:text-white" aria-label="{{ .platform | title }}">
{{- if eq .platform "twitter" -}}
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M18.9 2H22l-7.5 8.6L23 22h-6.8l-5.3-7-6.1 7H1.7l8-9.2L1 2h7l4.8 6.4L18.9 2Zm-2.4 18h1.9L7.6 4H5.6l10.9 16Z"/></svg>
{{- else if eq .platform "facebook" -}}
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M22 12a10 10 0 1 0-11.6 9.9v-7H7.9V12h2.5V9.8c0-2.5 1.5-3.9 3.8-3.9 1.1 0 2.2.2 2.2.2v2.5h-1.2c-1.2 0-1.6.8-1.6 1.6V12h2.7l-.4 2.9h-2.3v7A10 10 0 0 0 22 12Z"/></svg>
{{- else if eq .platform "instagram" -}}
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2.2c3.2 0 3.6 0 4.9.1 1.2.1 1.8.3 2.2.4.6.2 1 .5 1.4.9.4.4.7.8.9 1.4.1.4.3 1 .4 2.2.1 1.3.1 1.7.1 4.9s0 3.6-.1 4.9c-.1 1.2-.3 1.8-.4 2.2-.2.6-.5 1-.9 1.4-.4.4-.8.7-1.4.9-.4.1-1 .3-2.2.4-1.3.1-1.7.1-4.9.1s-3.6 0-4.9-.1c-1.2-.1-1.8-.3-2.2-.4-.6-.2-1-.5-1.4-.9-.4-.4-.7-.8-.9-1.4-.1-.4-.3-1-.4-2.2C2.2 15.6 2.2 15.2 2.2 12s0-3.6.1-4.9c.1-1.2.3-1.8.4-2.2.2-.6.5-1 .9-1.4.4-.4.8-.7 1.4-.9.4-.1 1-.3 2.2-.4C8.4 2.2 8.8 2.2 12 2.2Zm0 3.2A6.6 6.6 0 1 0 18.6 12 6.6 6.6 0 0 0 12 5.4Zm0 10.9A4.3 4.3 0 1 1 16.3 12 4.3 4.3 0 0 1 12 16.3Zm6.9-11.1a1.5 1.5 0 1 1-1.5-1.5 1.5 1.5 0 0 1 1.5 1.5Z"/></svg>
{{- else if eq .platform "youtube" -}}
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.4-4.7a2.5 2.5 0 0 0-1.8-1.8C19.3 5 12 5 12 5s-7.3 0-8.8.5A2.5 2.5 0 0 0 1.4 7.3C1 8.8 1 12 1 12s0 3.2.4 4.7a2.5 2.5 0 0 0 1.8 1.8C4.7 19 12 19 12 19s7.3 0 8.8-.5a2.5 2.5 0 0 0 1.8-1.8C23 15.2 23 12 23 12Zm-13 3V9l5.2 3-5.2 3Z"/></svg>
{{- else -}}
<span class="text-sm">{{ .platform | title }}</span>
{{- end -}}
</a>
{{- end -}}
</div>
</div>
{{/* Explore */}}
<nav aria-label="Footer">
<h2 class="text-sm font-semibold uppercase tracking-wider text-white/60">{{ i18n "footerExplore" }}</h2>
<ul class="mt-3 space-y-2 text-sm">
{{- range $primary -}}
<li><a href="{{ .RelPermalink }}" class="text-white/80 no-underline hover:text-white hover:underline">{{ .LinkTitle }}</a></li>
{{- end -}}
</ul>
</nav>
{{/* Legal & safety */}}
<nav aria-label="Legal and safety">
<h2 class="text-sm font-semibold uppercase tracking-wider text-white/60">{{ i18n "footerLegal" }}</h2>
<ul class="mt-3 space-y-2 text-sm">
{{- range $legal -}}
<li><a href="{{ .RelPermalink }}" class="text-white/80 no-underline hover:text-white hover:underline">{{ .LinkTitle }}</a></li>
{{- end -}}
<li>
<button type="button" onclick="window.openCookieSettings && window.openCookieSettings()" class="text-white/80 underline-offset-2 hover:text-white hover:underline">{{ i18n "cookieSettings" }}</button>
</li>
</ul>
</nav>
{{/* Responsible gambling summary */}}
<div>
<h2 class="text-sm font-semibold uppercase tracking-wider text-white/60">{{ i18n "footerResponsible" }}</h2>
<div class="mt-3 text-sm text-white/80">
{{ partial "responsible-gambling-block.html" (dict "page" . "variant" "compact") }}
</div>
</div>
</div>
{{ partial "trust-badges.html" . }}
{{/* Licence + copyright bar */}}
<div class="border-t border-white/10">
<div class="site-container flex flex-col gap-3 py-6 text-xs text-white/60 md:flex-row md:items-center md:justify-between">
<p>
<span class="mr-2 inline-flex items-center rounded bg-white/10 px-2 py-1 font-bold text-white"><span data-age-value>{{ $brand.ageRestrictionDefault }}</span>+ Gamble Responsibly</span>
{{ $brand.legalOperatorName }} is licensed and regulated by the {{ $brand.licenseAuthority }} (Licence No. {{ $brand.licenseNumber }}).
</p>
<p>
&copy; {{ now.Year }} {{ $brand.name }}. {{ i18n "allRightsReserved" }}
{{ with site.GetPage "affiliate-disclosure" }}<a href="{{ .RelPermalink }}" class="ml-2 text-white/80 underline hover:text-white">{{ i18n "affiliateDisclosure" }}</a>{{ end }}
</p>
</div>
</div>
</footer>
+77
View File
@@ -0,0 +1,77 @@
{{- /* games-filter.html — Alpine.js-powered filter for the slots library.
Call with (dict "page" . "games" <slice of game pages>).
Cards are rendered server-side (so thumbnails use the image pipeline) and
shown/hidden by the gamesFilter Alpine component (assets/js/games-filter.js). */ -}}
{{- $games := .games -}}
{{- $providers := slice -}}
{{- range $games -}}{{ with .Params.provider }}{{ $providers = $providers | append . }}{{ end }}{{- end -}}
{{- $providers = $providers | uniq | sort -}}
{{- $categories := slice -}}
{{- range $games -}}{{ with .Params.category }}{{ $categories = $categories | append . }}{{ end }}{{- end -}}
{{- $categories = $categories | uniq | sort -}}
<div x-data="gamesFilter()" x-init="recount()" x-effect="recount()">
{{/* Controls */}}
<div class="grid gap-3 rounded-xl border border-secondary/10 bg-secondary/5 p-4 sm:grid-cols-3">
<div>
<label for="game-search" class="mb-1 block text-xs font-semibold uppercase tracking-wide text-secondary/60">{{ i18n "filterSearch" }}</label>
<input id="game-search" type="search" x-model="search" placeholder="Game name&hellip;"
class="w-full rounded-md border border-secondary/20 px-3 py-2 text-sm focus-visible:outline-primary">
</div>
<div>
<label for="game-provider" class="mb-1 block text-xs font-semibold uppercase tracking-wide text-secondary/60">{{ i18n "filterProvider" }}</label>
<select id="game-provider" x-model="provider" class="w-full rounded-md border border-secondary/20 px-3 py-2 text-sm focus-visible:outline-primary">
<option value="all">{{ i18n "filterAllProviders" }}</option>
{{- range $providers }}<option value="{{ . }}">{{ . }}</option>{{ end -}}
</select>
</div>
<div>
<label for="game-category" class="mb-1 block text-xs font-semibold uppercase tracking-wide text-secondary/60">{{ i18n "filterCategory" }}</label>
<select id="game-category" x-model="category" class="w-full rounded-md border border-secondary/20 px-3 py-2 text-sm focus-visible:outline-primary">
<option value="all">{{ i18n "filterAllCategories" }}</option>
{{- range $categories }}<option value="{{ . }}">{{ . | title }}</option>{{ end -}}
</select>
</div>
</div>
<div class="mt-3 flex items-center justify-between text-sm text-secondary/60">
<p><span x-text="visibleCount"></span> game(s) shown</p>
<button type="button" @click="reset()" class="font-semibold text-primary hover:underline">{{ i18n "filterReset" }}</button>
</div>
{{/* Cards */}}
<div class="mt-5 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
{{- range $games -}}
{{- $img := .Resources.GetMatch "thumb.*" -}}
<article data-game
data-provider="{{ .Params.provider }}"
data-category="{{ .Params.category }}"
data-title="{{ .Title | lower }}"
x-show="cardVisible($el)" x-transition.opacity
class="card card-hover overflow-hidden p-0">
<div class="aspect-[3/2] w-full overflow-hidden bg-secondary/10">
{{ partial "responsive-image.html" (dict
"resource" $img
"alt" (printf "%s by %s" .Title (.Params.provider | default "provider"))
"class" "h-full w-full object-cover"
"widths" (slice 300 600)
"sizes" "(min-width:1024px) 300px, (min-width:640px) 50vw, 100vw") }}
</div>
<div class="p-4">
<div class="flex items-center justify-between gap-2">
<span class="badge-primary">{{ .Params.provider }}</span>
<span class="badge-muted capitalize">{{ .Params.category }}</span>
</div>
<h3 class="mt-2 text-lg font-bold"><a href="{{ .RelPermalink }}" class="no-underline hover:underline">{{ .Title }}</a></h3>
{{ with .Params.rtp }}<p class="mt-1 text-sm text-secondary/60">RTP {{ . }}%</p>{{ end }}
</div>
</article>
{{- end -}}
</div>
<p x-show="visibleCount === 0" x-cloak class="mt-6 rounded-lg bg-secondary/5 p-6 text-center text-secondary/70">
No games match your filters. <button type="button" @click="reset()" class="font-semibold text-primary underline">Reset</button>
</p>
</div>
+72
View File
@@ -0,0 +1,72 @@
{{- /* head-seo.html — <title>, meta description, canonical, Open Graph, Twitter,
RSS alternate, and JSON-LD (delegated to schema-jsonld.html). All values
fall back sensibly to brand data. */ -}}
{{- $brand := .Site.Data.brand -}}
{{- $title := "" -}}
{{- if .IsHome -}}
{{- $title = printf "%s | %s" $brand.name $brand.tagline -}}
{{- else -}}
{{- $title = printf "%s | %s" (.Params.title | default .Title) $brand.name -}}
{{- end -}}
{{- $desc := "" -}}
{{- with .Params.description -}}
{{- $desc = . -}}
{{- else -}}
{{- $desc = .Summary | default .Site.Params.defaultDescription -}}
{{- end -}}
{{- $desc = $desc | plainify | chomp | truncate 160 -}}
{{- /* Per-page OG image override (.Params.image) with fallback to the site default. */ -}}
{{- $ogImage := (.Params.image | default .Site.Params.ogImage) | absURL -}}
<title>{{ $title }}</title>
<meta name="description" content="{{ $desc }}">
<link rel="canonical" href="{{ .Permalink }}">
<meta name="robots" content="{{ if .Params.noindex }}noindex,nofollow{{ else }}index,follow{{ end }}">
<meta name="author" content="{{ $brand.name }}">
{{- /* Open Graph */ -}}
<meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}">
<meta property="og:title" content="{{ $title }}">
<meta property="og:description" content="{{ $desc }}">
<meta property="og:url" content="{{ .Permalink }}">
<meta property="og:site_name" content="{{ $brand.name }}">
<meta property="og:locale" content="{{ replace $brand.defaultLocale "-" "_" }}">
<meta property="og:image" content="{{ $ogImage }}">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
{{- /* Twitter */ -}}
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ $title }}">
<meta name="twitter:description" content="{{ $desc }}">
<meta name="twitter:image" content="{{ $ogImage }}">
{{- /* hreflang alternates for the multilingual build. No-op on single-language
sites (.IsTranslated is false), so it is safe to keep permanently. URL
scheme: defaultContentLanguageInSubdir = false (English at root). */ -}}
{{- if .IsTranslated -}}
{{- range .AllTranslations -}}
<link rel="alternate" hreflang="{{ .Language.LanguageCode | default .Language.Lang }}" href="{{ .Permalink }}">
{{- end -}}
{{- /* x-default must point at the site's DEFAULT content language (English at
root), independent of the page currently being rendered. site.Languages
is ordered by weight, so index 0 is the default language. */ -}}
{{- $defaultLang := (index site.Languages 0).Lang -}}
{{- range .AllTranslations -}}
{{- if eq .Language.Lang $defaultLang -}}
<link rel="alternate" hreflang="x-default" href="{{ .Permalink }}">
{{- end -}}
{{- end -}}
{{- range .Translations -}}
<meta property="og:locale:alternate" content="{{ replace (.Language.LanguageCode | default .Language.Lang) "-" "_" }}">
{{- end -}}
{{- end }}
{{ with .OutputFormats.Get "RSS" -}}
<link rel="alternate" type="application/rss+xml" href="{{ .RelPermalink }}" title="{{ $brand.name }} RSS">
{{- end }}
{{ partial "schema-jsonld.html" . }}
+28
View File
@@ -0,0 +1,28 @@
{{- /* header.html — sticky site header: logo (brand name from data), primary nav,
language switcher and a desktop affiliate CTA.
The region/compliance data layer (age/RG/currency/payment filtering) is
applied headlessly by locale-switcher.html — it no longer renders a visible
selector, but its script must still run on every page. */ -}}
<header class="sticky top-0 z-40 border-b border-secondary/10 bg-white/95 backdrop-blur">
<div class="site-container flex h-16 items-center gap-3 md:gap-4">
<a href="{{ "/" | relURL }}" class="flex shrink-0 items-center gap-2 no-underline" aria-label="{{ .Site.Data.brand.name }} home">
{{ partial "responsive-image.html" (dict
"src" .Site.Data.brand.logoPath
"alt" (printf "%s logo" .Site.Data.brand.name)
"class" "h-9 w-9"
"loading" "eager") }}
<span class="text-lg font-extrabold tracking-tight text-secondary">{{ .Site.Data.brand.name }}</span>
</a>
{{ partial "nav.html" . }}
<div class="ml-auto flex shrink-0 items-center gap-2">
{{ if gt (len site.Languages) 1 }}{{ partial "language-switcher.html" . }}{{ end }}
<span class="hidden sm:inline-flex">
{{ partial "cta-button.html" (dict "page" . "text" (i18n "playNow")) }}
</span>
</div>
</div>
{{ partial "locale-switcher.html" . }}
</header>
+32
View File
@@ -0,0 +1,32 @@
{{- /* language-switcher.html — content LANGUAGE switcher (distinct from the region
switcher). The region switcher (locale-switcher.html) changes compliance
chrome (age/RG/currency) client-side; THIS switcher navigates to the
translated page of the SAME content (linked via front-matter translationKey).
Renders nothing on single-language builds. */ -}}
{{- if gt (len site.Languages) 1 -}}
<div class="relative">
<label for="language-switcher" class="sr-only">{{ i18n "selectLanguage" | default "Language" }}</label>
<select id="language-switcher" data-language-switcher
onchange="if(this.value){window.location.href=this.value;}"
class="rounded-md border border-secondary/20 bg-white py-1.5 pl-2 pr-7 text-sm text-secondary focus-visible:outline-primary">
{{- $cur := . -}}
{{- range site.Languages -}}
{{- $lang := . -}}
{{- $isCurrent := eq $lang.Lang $cur.Language.Lang -}}
{{- $href := "" -}}
{{- /* Prefer the direct translation of the current page… */ -}}
{{- range $cur.AllTranslations -}}
{{- if eq .Language.Lang $lang.Lang -}}{{- $href = .RelPermalink -}}{{- end -}}
{{- end -}}
{{- /* …otherwise fall back to that language's home page. */ -}}
{{- if not $href -}}
{{- with site.Home.AllTranslations -}}
{{- range . -}}{{- if eq .Language.Lang $lang.Lang -}}{{- $href = .RelPermalink -}}{{- end -}}{{- end -}}
{{- end -}}
{{- end -}}
{{- if not $href -}}{{- $href = "/" -}}{{- end -}}
<option value="{{ $href }}"{{ if $isCurrent }} selected{{ end }}>{{ $lang.LanguageName | default (upper $lang.Lang) }}</option>
{{- end -}}
</select>
</div>
{{- end -}}
+80
View File
@@ -0,0 +1,80 @@
{{- /* locale-switcher.html — HEADLESS region/compliance data layer.
This partial no longer renders a visible region selector (the header now
exposes only the content language switcher). It still applies the stored
or default locale on load so all locale-dependent COMPLIANCE bindings
render correctly across the site:
[data-age-value] age restriction
[data-rg-name] responsible-gambling org name
[data-rg-link] responsible-gambling URL
[data-locale-currency]currency symbol (payment table)
[data-payment-row] show only methods available in the locale
[data-kyc-row] highlight the matching KYC row
Data comes from data/locales.yaml (exposed as window.__LOCALES__).
window.applyLocale(loc) and the "localechange" event remain the public
API, so a region selector can be reintroduced later without touching the
compliance logic. */ -}}
<script>
(function () {
function apply(loc) {
var locales = window.__LOCALES__ || [];
var entry = null;
for (var i = 0; i < locales.length; i++) {
if (locales[i].locale === loc) { entry = locales[i]; break; }
}
if (!entry) {
if (!locales.length) return;
entry = locales[0];
loc = entry.locale;
}
try { localStorage.setItem("siteLocale", loc); } catch (e) {}
document.querySelectorAll("[data-age-value]").forEach(function (el) {
el.textContent = entry.ageRestriction;
});
document.querySelectorAll("[data-rg-name]").forEach(function (el) {
el.textContent = entry.responsibleGamblingOrgName;
});
document.querySelectorAll("[data-rg-link]").forEach(function (el) {
el.setAttribute("href", entry.responsibleGamblingUrl);
});
document.querySelectorAll("[data-locale-currency]").forEach(function (el) {
el.textContent = entry.currencySymbol;
});
// Show only payment methods available in the selected locale.
document.querySelectorAll("[data-payment-row]").forEach(function (row) {
var locs = (row.getAttribute("data-locales") || "").split(",");
row.hidden = locs.indexOf(loc) === -1;
});
// Highlight the KYC row matching the selected locale.
document.querySelectorAll("[data-kyc-row]").forEach(function (row) {
row.classList.toggle("is-active-locale", row.getAttribute("data-locale") === loc);
});
// Kept for backwards compatibility if a region selector is reintroduced.
document.querySelectorAll("[data-locale-switcher]").forEach(function (sel) {
sel.value = loc;
});
document.dispatchEvent(new CustomEvent("localechange", { detail: { locale: loc, entry: entry } }));
}
window.applyLocale = apply;
function init() {
var brand = window.__BRAND__ || {};
var stored = null;
try { stored = localStorage.getItem("siteLocale"); } catch (e) {}
apply(stored || brand.defaultLocale);
// If a region selector is present (reintroduced later), wire it up.
document.querySelectorAll("[data-locale-switcher]").forEach(function (sel) {
sel.addEventListener("change", function () { apply(this.value); });
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();
</script>
+47
View File
@@ -0,0 +1,47 @@
{{- /* nav.html — primary navigation built from pages with showInNav: true,
ordered by weight. Desktop = inline list; mobile = Alpine.js toggle. */ -}}
{{- $current := . -}}
{{- $navPages := sort (where .Site.Pages "Params.showInNav" true) "Weight" -}}
{{/* Desktop — min-w-0 lets the nav shrink inside the flex row; overflow-x-auto
keeps a long section list on one line and scrollable instead of pushing the
header past the viewport. */}}
<nav aria-label="Primary" class="hidden min-w-0 flex-1 lg:block">
<ul class="nav-scroll flex items-center gap-1 overflow-x-auto whitespace-nowrap">
{{- range $navPages -}}
{{- $active := eq $current.RelPermalink .RelPermalink -}}
<li class="shrink-0">
<a href="{{ .RelPermalink }}" class="nav-link{{ if $active }} nav-link-active{{ end }}"{{ if $active }} aria-current="page"{{ end }}>{{ .LinkTitle }}</a>
</li>
{{- end -}}
</ul>
</nav>
{{/* Mobile — native <details> so the menu works with NO JavaScript; Alpine adds
escape-to-close and click-outside-to-close when available. */}}
<details class="group relative lg:hidden"
x-data="{ open: false }"
x-bind:open="open"
@toggle="open = $event.target.open"
@keydown.escape.window="open = false"
@click.outside="open = false">
<summary class="inline-flex cursor-pointer list-none items-center justify-center rounded-md p-2 text-secondary hover:bg-secondary/5 [&::-webkit-details-marker]:hidden"
aria-controls="mobile-menu" aria-label="{{ i18n "toggleMenu" }}">
<svg class="h-6 w-6 group-open:hidden" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" />
</svg>
<svg class="hidden h-6 w-6 group-open:block" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</summary>
<div id="mobile-menu"
class="absolute inset-x-0 top-16 z-40 max-h-[calc(100vh-4rem)] overflow-y-auto border-b border-secondary/10 bg-white p-4 shadow-lg">
<ul class="flex flex-col gap-1">
{{- range $navPages -}}
<li><a href="{{ .RelPermalink }}" class="nav-link block">{{ .LinkTitle }}</a></li>
{{- end -}}
<li class="pt-3">{{ partial "cta-button.html" (dict "page" $current "text" (i18n "playNow") "block" true) }}</li>
</ul>
</div>
</details>
+50
View File
@@ -0,0 +1,50 @@
{{- /* payment-table.html — renders data/payment_methods.yaml.
Call with (dict "page" . "locale" "<optional locale>").
- If "locale" is given, methods are filtered server-side to that market.
- Otherwise ALL methods render, and each row carries data-locales so the
locale switcher can show/hide rows for the visitor's selected region. */ -}}
{{- $page := .page -}}
{{- $brand := $page.Site.Data.brand -}}
{{- $methods := $page.Site.Data.payment_methods -}}
{{- $locale := .locale -}}
{{- if $locale -}}
{{- $f := slice -}}
{{- range $methods -}}{{ if in .availableLocales $locale }}{{ $f = $f | append . }}{{ end }}{{- end -}}
{{- $methods = $f -}}
{{- end -}}
{{- $curr := "£" -}}
{{- with where $page.Site.Data.locales "locale" $brand.defaultLocale -}}{{- $curr = (index . 0).currencySymbol -}}{{- end -}}
<div class="overflow-x-auto">
<table class="data-table">
<thead>
<tr>
<th scope="col">Method</th>
<th scope="col">Type</th>
<th scope="col">Min. deposit</th>
<th scope="col">Max. deposit</th>
<th scope="col">Processing time</th>
<th scope="col">Fees</th>
</tr>
</thead>
<tbody>
{{- range $methods -}}
<tr data-payment-row data-locales="{{ delimit .availableLocales "," }}">
<th scope="row" class="font-medium">
<span class="flex items-center gap-2">
<img src="{{ .logoPath | relURL }}" alt="{{ .name }} logo" width="40" height="24" loading="lazy" class="h-6 w-auto rounded-sm border border-secondary/10">
{{ .name }}
</span>
</th>
<td><span class="badge-muted capitalize">{{ .type }}</span></td>
<td><span data-locale-currency>{{ $curr }}</span>{{ .minDeposit }}</td>
<td><span data-locale-currency>{{ $curr }}</span>{{ .maxDeposit }}</td>
<td>{{ .processingTime }}</td>
<td>{{ .fees }}</td>
</tr>
{{- end -}}
</tbody>
</table>
</div>
<p class="mt-2 text-xs text-secondary/70">Availability and limits vary by region and are shown for your selected market. Select your region in the header to update this table.</p>
+26
View File
@@ -0,0 +1,26 @@
{{- /* pros-cons-table.html — reads .Params.pros and .Params.cons (arrays of
strings) from the page front matter and renders a two-column comparison. */ -}}
{{- $pros := .Params.pros -}}
{{- $cons := .Params.cons -}}
{{- if or $pros $cons -}}
<div class="grid gap-4 sm:grid-cols-2">
<div class="card border-emerald-200 bg-emerald-50">
<h3 class="flex items-center gap-2 text-lg font-bold text-emerald-800">
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
Pros
</h3>
<ul class="mt-3 space-y-2 text-sm text-emerald-900">
{{- range $pros }}<li class="flex gap-2"><span aria-hidden="true" class="text-emerald-500">&#10003;</span><span>{{ . }}</span></li>{{ end -}}
</ul>
</div>
<div class="card border-rose-200 bg-rose-50">
<h3 class="flex items-center gap-2 text-lg font-bold text-rose-800">
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
Cons
</h3>
<ul class="mt-3 space-y-2 text-sm text-rose-900">
{{- range $cons }}<li class="flex gap-2"><span aria-hidden="true" class="text-rose-500">&#10007;</span><span>{{ . }}</span></li>{{ end -}}
</ul>
</div>
</div>
{{- end -}}
+21
View File
@@ -0,0 +1,21 @@
{{- /* rating-widget.html — editorial star rating. Uses page front matter
(rating / ratingCount) with a fallback to brand.yaml. */ -}}
{{- $brand := .Site.Data.brand -}}
{{- $rating := .Params.rating | default $brand.ratingValue -}}
{{- $count := .Params.ratingCount | default $brand.ratingCount -}}
{{- $rounded := int (math.Round $rating) -}}
<div class="card flex flex-wrap items-center gap-5">
<div class="text-5xl font-extrabold leading-none text-primary">
{{ $rating }}<span class="text-xl font-semibold text-secondary/40">/5</span>
</div>
<div>
<div class="flex gap-0.5" role="img" aria-label="Rated {{ $rating }} out of 5">
{{- range seq 5 -}}
<svg class="h-6 w-6 {{ if le . $rounded }}text-amber-400{{ else }}text-secondary/20{{ end }}" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 2l2.9 6.3 6.9.7-5.1 4.7 1.4 6.8L12 17.8 5.9 20.5l1.4-6.8L2.2 9l6.9-.7L12 2z"/>
</svg>
{{- end -}}
</div>
<p class="mt-1 text-sm text-secondary/60">Independent editorial score based on {{ $count }} data points.</p>
</div>
</div>
+30
View File
@@ -0,0 +1,30 @@
{{- /* related-pages.html — renders the current page's relatedPages front-matter
(a list of slugs) as cards. Matches by .Slug or file base name. */ -}}
{{- $current := . -}}
{{- $related := .Params.relatedPages -}}
{{- if $related -}}
{{- $found := slice -}}
{{- range $slug := $related -}}
{{- range $p := $current.Site.Pages -}}
{{- if and (ne $p $current) (not $p.IsHome) -}}
{{- if or (eq $p.Slug $slug) (and $p.File (eq $p.File.ContentBaseName $slug)) -}}
{{- $found = $found | append $p -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- with $found -}}
<section class="site-container section" aria-labelledby="related-heading">
<h2 id="related-heading" class="text-xl font-bold">Related pages</h2>
<div class="mt-4 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{{- range . -}}
<a href="{{ .RelPermalink }}" class="card card-hover block no-underline">
<span class="badge-primary">{{ .LinkTitle }}</span>
<p class="mt-2 text-sm text-secondary/70">{{ .Params.description | default (.Summary | plainify) | truncate 110 }}</p>
<span class="mt-3 inline-block text-sm font-semibold text-primary">Read more &rarr;</span>
</a>
{{- end -}}
</div>
</section>
{{- end -}}
{{- end -}}
@@ -0,0 +1,64 @@
{{- /* responsible-gambling-block.html — locale-aware RG messaging.
Call with (dict "page" . "variant" "compact"|"full").
Server renders the DEFAULT locale; the locale switcher updates the
[data-age-value] / [data-rg-name] / [data-rg-link] elements client-side. */ -}}
{{- $page := .page -}}
{{- $variant := .variant | default "full" -}}
{{- $brand := $page.Site.Data.brand -}}
{{- $locales := $page.Site.Data.locales -}}
{{- $default := "" -}}
{{- with where $locales "locale" $brand.defaultLocale -}}{{- $default = index . 0 -}}{{- end -}}
{{- $age := $brand.ageRestrictionDefault -}}
{{- $orgName := "your national support service" -}}
{{- $orgUrl := "#" -}}
{{- with $default -}}
{{- $age = .ageRestriction -}}
{{- $orgName = .responsibleGamblingOrgName -}}
{{- $orgUrl = .responsibleGamblingUrl -}}
{{- end -}}
{{- if eq $variant "compact" -}}
<p>This site is intended for adults aged <span data-age-value>{{ $age }}</span>+ only. Gambling should be entertainment, never a way to make money.</p>
<p class="mt-2">Need help? Contact <a data-rg-link href="{{ $orgUrl }}" target="_blank" rel="noopener nofollow" class="font-semibold text-white underline"><span data-rg-name>{{ $orgName }}</span></a>.</p>
{{- else -}}
<div class="prose-content">
<p>
Gambling must stay fun. This site is intended for adults aged
<strong><span data-age-value>{{ $age }}</span>+</strong> only. Never bet more than you can afford to
lose, never chase losses, and never gamble to escape stress or debt. If it stops being enjoyable,
it is time to stop.
</p>
<h3>Tools to stay in control</h3>
<ul>
<li><strong>Deposit limits</strong> &mdash; cap how much you can add to your account per day, week or month.</li>
<li><strong>Loss &amp; wager limits</strong> &mdash; set a ceiling on stakes or net losses over a period.</li>
<li><strong>Reality checks</strong> &mdash; on-screen reminders of how long you have been playing.</li>
<li><strong>Time-outs</strong> &mdash; lock yourself out for 24 hours up to several weeks.</li>
<li><strong>Self-exclusion</strong> &mdash; block access for months or years, including national schemes.</li>
</ul>
<h3>Find local support</h3>
<p>
Your currently selected region recommends
<a data-rg-link href="{{ $orgUrl }}" target="_blank" rel="noopener nofollow"><span data-rg-name>{{ $orgName }}</span></a>.
Support services for every market we cover are listed below &mdash; all offer free, confidential help.
</p>
<div class="overflow-x-auto">
<table class="data-table">
<thead>
<tr><th scope="col">Region</th><th scope="col">Min. age</th><th scope="col">Support organisation</th></tr>
</thead>
<tbody>
{{- range $locales -}}
<tr>
<td>{{ .label | default .locale }}</td>
<td>{{ .ageRestriction }}+</td>
<td><a href="{{ .responsibleGamblingUrl }}" target="_blank" rel="noopener nofollow">{{ .responsibleGamblingOrgName }}</a></td>
</tr>
{{- end -}}
</tbody>
</table>
</div>
</div>
{{- end -}}
+46
View File
@@ -0,0 +1,46 @@
{{- /* responsive-image.html — safe image renderer.
Resize/Fit only work on raster resources (png/jpg/webp/gif). This partial:
- resizes + builds srcset for raster Hugo resources,
- falls back to a plain <img> for SVG resources or static paths,
- falls back to a CSS gradient placeholder when nothing resolves.
A missing image can therefore NEVER break the build.
Call with a dict. Provide ONE of:
resource : a Hugo resource (assets/ or page bundle), OR
src : a string path (e.g. "/images/logo.svg")
Optional: alt, class, loading ("lazy"|"eager"), sizes, widths (slice of ints). */ -}}
{{- $r := .resource -}}
{{- $src := .src -}}
{{- $alt := .alt | default "" -}}
{{- $class := .class | default "" -}}
{{- $loading := .loading | default "lazy" -}}
{{- $sizes := .sizes | default "100vw" -}}
{{- $widths := .widths | default (slice 400 800 1200) -}}
{{- if $r -}}
{{- $sub := $r.MediaType.SubType -}}
{{- if in (slice "png" "jpg" "jpeg" "webp" "gif") $sub -}}
{{- $ow := $r.Width -}}
{{- $srcset := slice -}}
{{- range $widths -}}
{{- if le . $ow -}}
{{- $img := $r.Resize (printf "%dx" .) -}}
{{- $srcset = $srcset | append (printf "%s %dw" $img.RelPermalink $img.Width) -}}
{{- end -}}
{{- end -}}
{{- $fallback := $r -}}
{{- if gt $ow 1200 -}}{{- $fallback = $r.Resize "1200x" -}}{{- end -}}
<img
src="{{ $fallback.RelPermalink }}"
{{ with $srcset }}srcset="{{ delimit . ", " }}" sizes="{{ $sizes }}"{{ end }}
width="{{ $fallback.Width }}" height="{{ $fallback.Height }}"
alt="{{ $alt }}" loading="{{ $loading }}" decoding="async"
{{ with $class }}class="{{ . }}"{{ end }}>
{{- else -}}
<img src="{{ $r.RelPermalink }}" alt="{{ $alt }}" loading="{{ $loading }}" decoding="async"{{ with $class }} class="{{ . }}"{{ end }}>
{{- end -}}
{{- else if $src -}}
<img src="{{ $src | relURL }}" alt="{{ $alt }}" loading="{{ $loading }}" decoding="async"{{ with $class }} class="{{ . }}"{{ end }}>
{{- else -}}
<div class="img-placeholder {{ $class }}" role="img" aria-label="{{ $alt }}"></div>
{{- end -}}
+57
View File
@@ -0,0 +1,57 @@
{{- /* schema-jsonld.html — emits JSON-LD based on the page's front-matter
schemaType. BreadcrumbList is emitted separately by breadcrumbs.html.
Uses dict + jsonify so all values are safely escaped. */ -}}
{{- $brand := .Site.Data.brand -}}
{{- $type := .Params.schemaType | default "none" -}}
{{- if eq $type "Organization" -}}
{{- $sameAs := slice -}}
{{- range $brand.socialLinks -}}
{{- $sameAs = $sameAs | append .url -}}
{{- end -}}
{{- $data := dict
"@context" "https://schema.org"
"@type" "Organization"
"name" $brand.name
"url" .Site.BaseURL
"logo" ($brand.logoPath | absURL)
"description" $brand.tagline
"email" $brand.contactEmail
"foundingDate" (printf "%d" (int $brand.foundedYear))
"sameAs" $sameAs
-}}
<script type="application/ld+json">{{ $data | jsonify | safeJS }}</script>
{{- else if eq $type "FAQPage" -}}
{{- $items := slice -}}
{{- range .Site.Data.faq -}}
{{- $items = $items | append (dict
"@type" "Question"
"name" .question
"acceptedAnswer" (dict "@type" "Answer" "text" .answer)
) -}}
{{- end -}}
{{- $data := dict
"@context" "https://schema.org"
"@type" "FAQPage"
"mainEntity" $items
-}}
<script type="application/ld+json">{{ $data | jsonify | safeJS }}</script>
{{- else if eq $type "Article" -}}
{{- /* The About page is an editorial review, so we emit a Review with the
promoted operator as the reviewed item. */ -}}
{{- $data := dict
"@context" "https://schema.org"
"@type" "Review"
"headline" (.Params.h1 | default .Title)
"url" .Permalink
"datePublished" (.Date.Format "2006-01-02")
"dateModified" (.Lastmod.Format "2006-01-02")
"author" (dict "@type" "Organization" "name" $brand.name)
"publisher" (dict "@type" "Organization" "name" $brand.name "logo" (dict "@type" "ImageObject" "url" ($brand.logoPath | absURL)))
"itemReviewed" (dict "@type" "Organization" "name" $brand.legalOperatorName)
"reviewRating" (dict "@type" "Rating" "ratingValue" $brand.ratingValue "bestRating" 5 "worstRating" 1)
-}}
<script type="application/ld+json">{{ $data | jsonify | safeJS }}</script>
{{- end -}}
+22
View File
@@ -0,0 +1,22 @@
{{- /* trust-badges.html — compliance/trust strip shown in the footer and reusable
elsewhere. Uses the licence authority + age from brand data. */ -}}
{{- $brand := .Site.Data.brand -}}
<div class="border-t border-white/10 bg-black/20">
<div class="site-container flex flex-wrap items-center justify-center gap-4 py-5 text-xs text-white/70">
<span class="inline-flex items-center gap-2 rounded-full border border-white/20 px-3 py-1 font-semibold">
<span class="grid h-5 w-5 place-items-center rounded-full bg-white/20"><span data-age-value>{{ $brand.ageRestrictionDefault }}</span></span>
Over-age play only
</span>
<span class="inline-flex items-center gap-2 rounded-full border border-white/20 px-3 py-1">
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3l7 4v5c0 4.4-3 8.3-7 9-4-0.7-7-4.6-7-9V7l7-4z"/></svg>
{{ $brand.licenseAuthority }}
</span>
<span class="inline-flex items-center gap-2 rounded-full border border-white/20 px-3 py-1">
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
SSL secured
</span>
<span class="inline-flex items-center gap-2 rounded-full border border-white/20 px-3 py-1">
Independent affiliate &mdash; not the operator
</span>
</div>
</div>
+4
View File
@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: {{ "sitemap.xml" | absURL }}
+10
View File
@@ -0,0 +1,10 @@
{{- /* {{< cta text="Play Now" style="primary" size="lg" subid="custom" >}}
Reuses partials/cta-button.html so the affiliate link is built in ONE place. */ -}}
<div class="my-6">
{{ partial "cta-button.html" (dict
"page" .Page
"text" (.Get "text" | default "Play Now")
"style" (.Get "style")
"size" (.Get "size")
"subid" (.Get "subid")) }}
</div>
+9
View File
@@ -0,0 +1,9 @@
{{- /* Paired shortcode for an inline, no-JS FAQ item:
{{< faq-item question="How do I ...?" >}}
Markdown answer body.
{{< /faq-item >}} */ -}}
{{- $q := .Get "question" | default (.Get 0) -}}
<details class="my-3 rounded-lg border border-secondary/10 p-4">
<summary class="cursor-pointer font-semibold text-secondary">{{ $q }}</summary>
<div class="prose-content mt-3">{{ .Inner | markdownify }}</div>
</details>
+5
View File
@@ -0,0 +1,5 @@
{{- /* {{< payment-table >}} or {{< payment-table locale="de-DE" >}}
Renders data/payment_methods.yaml via partials/payment-table.html. */ -}}
<div class="my-6">
{{ partial "payment-table.html" (dict "page" .Page "locale" (.Get "locale")) }}
</div>
+31
View File
@@ -0,0 +1,31 @@
{{ define "main" }}
{{ partial "breadcrumbs.html" . }}
<div class="site-container section">
<header class="mb-8 max-w-3xl">
<h1 class="text-3xl font-extrabold tracking-tight sm:text-4xl">{{ .Params.h1 | default .Title }}</h1>
{{ with .Params.description }}<p class="mt-3 text-lg text-secondary/70">{{ . }}</p>{{ end }}
</header>
{{ with .Content }}<div class="prose-content max-w-3xl">{{ . }}</div>{{ end }}
{{ $games := sort (where .Site.RegularPages "Section" "slots") "Title" }}
<section class="mt-10" aria-label="Game library">
{{ if $games }}
{{ partial "games-filter.html" (dict "page" . "games" $games) }}
{{ else }}
<p class="rounded-lg bg-secondary/5 p-6 text-secondary/70">No games have been added yet. Add page bundles under <code>content/slots/games/&lt;slug&gt;/index.md</code>.</p>
{{ end }}
</section>
{{ if not .Params.hideCta }}
<div class="mt-12 max-w-3xl rounded-2xl border border-primary/20 bg-primary/5 p-6">
<h2 class="text-xl font-bold">Ready to spin?</h2>
<p class="mt-1 text-secondary/70"><span data-age-value>{{ .Site.Data.brand.ageRestrictionDefault }}</span>+ only. T&amp;Cs apply. Please gamble responsibly.</p>
<div class="mt-4">{{ partial "cta-button.html" (dict "page" . "text" "Play the Slots" "size" "lg") }}</div>
</div>
{{ end }}
</div>
{{ partial "related-pages.html" . }}
{{ end }}