feat: Hugo engine for iGaming affiliate template (layouts, assets, i18n, verified data, CI build script)
This commit is contained in:
@@ -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>
|
||||
@@ -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 -}}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
© {{ 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>
|
||||
@@ -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…"
|
||||
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>
|
||||
@@ -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" . }}
|
||||
@@ -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>
|
||||
@@ -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 -}}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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">✓</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">✗</span><span>{{ . }}</span></li>{{ end -}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{{- end -}}
|
||||
@@ -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>
|
||||
@@ -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 →</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> — cap how much you can add to your account per day, week or month.</li>
|
||||
<li><strong>Loss & wager limits</strong> — set a ceiling on stakes or net losses over a period.</li>
|
||||
<li><strong>Reality checks</strong> — on-screen reminders of how long you have been playing.</li>
|
||||
<li><strong>Time-outs</strong> — lock yourself out for 24 hours up to several weeks.</li>
|
||||
<li><strong>Self-exclusion</strong> — 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 — 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 -}}
|
||||
@@ -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 -}}
|
||||
@@ -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 -}}
|
||||
@@ -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 — not the operator
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user