Files
Pete/internal/web/templates/games_layout.html
prosolis 7ca1f7a030 games: the door you can see from outside, and the picture on it
We never had Open Graph on the casino, and adding meta tags would not have
fixed it. Every route was behind requirePlayer, so a link pasted into a chat
window got a 302 to sign-in and unfurled as whatever the auth screen said:
"parodia.dev", no image, no description. Tags on a page a stranger cannot
fetch are tags nobody reads. So the casino now has a front door — a real page,
served to anybody, that says what the place is and offers a way in. You still
can't play from it, and every table still bounces you to sign-in.

The share card is drawn in Go rather than checked in as a picture, because the
casino has two names on a clock and the card keeps the joke: paste the link in
daylight and you get Casinopolis on green felt, paste it after six and the neon
is on and it says Casino Night Zone. Same roomAt() rule as everywhere else,
except the clock that decides is the server's — an unfurl bot has no evening of
its own. Both cards are drawn once, at first ask, and kept.

Two things worth keeping from building it. color.RGBA is alpha-premultiplied,
and the lamp over the table wrote raw channels next to a low alpha, which is
not a dim glow but an invalid colour: image/draw ran it past 255 and wrapped
the hue, and the first card came out with a blue dome over a green stripe. If
a colour here ever comes out impossible, look for a missing premultiply. And
og:image has to be an absolute URL that actually resolves, which is two
different addresses depending on how you arrived: /og.png on the games host
(hostRouter puts the /games back on) and /games/og.png anywhere else. The dev
rig advertised the first while serving only the second. The test now reads the
URL off the page and goes and fetches it, on both hosts, because an og:image
that 404s is worth exactly as much as no og:image.

Fredoka is vendored (OFL) — the page can reach for a font over the network and
a server drawing a PNG cannot.

Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ
2026-07-14 13:30:52 -07:00

120 lines
6.5 KiB
HTML

{{define "layout"}}<!doctype html>
<html lang="en" data-room="{{.Room.Slug}}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{block "title" .}}{{.Room.Name}}{{end}}</title>
<meta name="robots" content="noindex">
{{/* What the casino looks like when somebody pastes it into a chat window.
noindex keeps it out of a search engine and does not keep it out of a link
preview: those are two different bots and only one of them is reading this.
The image is drawn by games_og.go and follows the clock, so a link shared
after six unfurls with the neon on. */}}
<meta property="og:type" content="website">
<meta property="og:site_name" content="{{.Room.Name}}">
<meta property="og:title" content="{{block "og_title" .}}{{.Room.Name}}{{end}}">
<meta property="og:description" content="{{block "og_desc" .}}Blackjack, hold'em, UNO, trivia, hangman and solitaire, played for real gogobee euros. Sign in and pull up a chair.{{end}}">
<meta property="og:url" content="{{.URL}}">
<meta property="og:image" content="{{.OGImage}}">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="{{.Room.Name}}: two cards and a stack of chips on the felt.">
<meta name="twitter:card" content="summary_large_image">
<meta name="description" content="Blackjack, hold'em, UNO, trivia, hangman and solitaire, played for real gogobee euros.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/css/output.css">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cpolygon points='16,3 27,9.5 27,22.5 16,29 5,22.5 5,9.5' fill='%23f2b53d' stroke='%232b2118' stroke-width='2.5' stroke-linejoin='round'/%3E%3C/svg%3E" type="image/svg+xml">
<meta name="theme-color" content="#17231d">
<script>
// Which room you're standing in, from your clock rather than the server's:
// Casinopolis in daylight, Casino Night Zone once the lights come on at six.
// Same rule as roomAt() in games_pages.go — keep the two in step.
//
// The server already painted its best guess into the markup, so this only
// corrects a player whose evening isn't the server's.
(function () {
var ROOMS = {
"casinopolis": "Casinopolis",
"casino-night": "Casino Night Zone",
};
function roomAt(h) { return (h >= 6 && h < 18) ? "casinopolis" : "casino-night"; }
function apply() {
var slug = roomAt(new Date().getHours());
// The palette can be set from <head> — the sign can't: this script runs
// before the header exists. So set the attribute now (no flash of the
// wrong room) and always re-stamp the name, which is a no-op until the
// header is there to stamp. Bailing out early when the slug already
// matches would leave the server's name under the browser's palette.
document.documentElement.dataset.room = slug;
document.querySelectorAll("[data-room-name]").forEach(function (el) {
el.textContent = ROOMS[slug];
});
}
apply();
document.addEventListener("DOMContentLoaded", apply);
setInterval(apply, 60000);
})();
</script>
</head>
<body class="min-h-screen bg-[color:var(--bg)] text-[color:var(--ink)]">
<div class="pointer-events-none fixed inset-0 -z-10 cs-room" aria-hidden="true"></div>
<header class="mx-auto max-w-5xl px-4 pt-6 pb-4 sm:pt-10">
<div class="flex items-center justify-between gap-3">
<a href="/games" class="group flex min-w-0 items-center gap-3">
{{template "_comb" .}}
<span class="min-w-0">
<span data-room-name class="block font-display text-2xl sm:text-3xl font-bold leading-none tracking-tight">{{.Room.Name}}</span>
<span class="mt-1 block text-xs font-semibold uppercase tracking-[0.18em] text-[color:var(--ink)]/45">Chips are euros</span>
</span>
</a>
{{if .User}}
<a href="/auth/logout" title="Signed in as {{.User.Display}} — sign out"
class="inline-flex shrink-0 items-center gap-2 rounded-full bg-[color:var(--card)] px-2.5 py-1.5 text-sm font-semibold shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition">
<span class="grid h-6 w-6 place-items-center rounded-full bg-[color:var(--accent)] text-xs font-bold text-[#20180c]">{{.User.Initial}}</span>
<span class="hidden sm:inline max-w-[7rem] truncate">{{.User.Display}}</span>
</a>
{{else}}
{{/* Nobody is signed in, which on any page but the front door can't happen. */}}
<a href="/auth/login?next=/games"
class="inline-flex shrink-0 items-center rounded-full bg-[color:var(--accent)] px-4 py-1.5 text-sm font-bold text-[#20180c] shadow-pete hover:-translate-y-0.5 transition">
Sign in
</a>
{{end}}
</div>
</header>
<main class="mx-auto max-w-5xl px-4 pb-20">
{{block "main" .}}{{end}}
</main>
<footer class="mx-auto max-w-5xl px-4 pb-10 text-center text-xs text-[color:var(--ink)]/40">
Play for what you can lose. Cash out whenever you like.
</footer>
<!-- The room's noise. Loaded here rather than per-table because every table
wants it and none of them owns it, and because it has to be defined before
casino-fx.js — which reaches for it on every chip it throws. `defer` runs
these in document order, so that's guaranteed. -->
<script src="/static/js/casino-sfx.js" defer></script>
{{block "scripts" .}}{{end}}
</body>
</html>{{end}}
{{/* The house mark: a honeycomb cell struck like a chip. Stands in for a logo,
and deliberately isn't a face — nobody is watching you play. */}}
{{define "_comb"}}
<span class="cs-comb grid h-11 w-11 shrink-0 place-items-center transition-transform group-hover:-rotate-6" aria-hidden="true">
<svg viewBox="0 0 32 32" class="h-11 w-11">
<polygon points="16,2.5 27.5,9.25 27.5,22.75 16,29.5 4.5,22.75 4.5,9.25"
fill="var(--accent)" stroke="var(--ink)" stroke-width="2.5" stroke-linejoin="round"/>
<polygon points="16,9 21.5,12.25 21.5,18.75 16,22 10.5,18.75 10.5,12.25"
fill="none" stroke="var(--ink)" stroke-width="1.75" stroke-linejoin="round" opacity="0.5"/>
</svg>
</span>
{{end}}