bingo: SEO pass and resized photos
All checks were successful
Build and Deploy to Production / test (push) Successful in 22s
Build and Deploy to Production / build (push) Successful in 1m1s
Build and Deploy to Production / deploy (push) Successful in 39s

The bingo host fell through to asxp.io's public/, so crawlers got
asxp.io's robots.txt pointing at asxp.io's sitemap (and the keys and
invoice JS were served too). Turn static files off for BingoWeb and give
it its own robots.txt and a sitemap with hreflang alternates.

The card loaded 15.6 MB of full-size photos as cell backgrounds. Derive
480/1200 px WebP variants with ImageMagick on store (memory/area limits,
input coder pinned to the sniffed type), render cells as <img> with lazy
loading past the first two rows, and add an admin action that rebuilds
variants from the stored originals.

Head gets per-locale titles and descriptions carrying the live score,
og:image from the latest hit, Twitter card, og:locale alternates,
JSON-LD WebSite + ItemList, and Vary: Accept-Language on the redirect.

Assisted-by: Claude:opus-5
This commit is contained in:
Sergei Poljanski 2026-10-10 06:26:03 +04:00
commit 201a70b07b
Signed by: asxpi
GPG key ID: 4F8851660FA4121B
11 changed files with 236 additions and 23 deletions

View file

@ -1,20 +1,39 @@
<% base = "https://#{ENV.fetch('BINGO_HOST', request.host)}" %>
<% base = base_url %>
<% desc = format(t(:seo_desc), hit: @board.hit.count(true), total: @board.cells.size) %>
<% og_img = og_photo(@board) %>
<!doctype html>
<html lang="<%= @locale %>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><%= t(:title) %></title>
<meta name="description" content="<%= t(:desc) %>">
<title><%= t(:seo_title) %></title>
<meta name="description" content="<%= desc %>">
<meta name="robots" content="index, follow, max-image-preview:large">
<meta name="theme-color" content="#121212">
<link rel="canonical" href="<%= base %>/<%= @locale %>">
<% Bingo::LOCALES.each do |l| %>
<link rel="alternate" hreflang="<%= l %>" href="<%= base %>/<%= l %>">
<% end %>
<link rel="alternate" hreflang="x-default" href="<%= base %>/">
<link rel="sitemap" type="application/xml" href="/sitemap.xml">
<meta property="og:type" content="website">
<meta property="og:title" content="<%= t(:title) %>">
<meta property="og:description" content="<%= t(:desc) %>">
<meta property="og:site_name" content="<%= t(:title) %>">
<meta property="og:title" content="<%= t(:seo_title) %>">
<meta property="og:description" content="<%= desc %>">
<meta property="og:url" content="<%= base %>/<%= @locale %>">
<meta property="og:locale" content="<%= t(:og_locale) %>">
<% (Bingo::LOCALES - [@locale]).each do |l| %>
<meta property="og:locale:alternate" content="<%= Bingo::T[l][:og_locale] %>">
<% end %>
<% if og_img %>
<meta property="og:image" content="<%= og_img %>">
<meta property="og:image:alt" content="<%= t(:title) %>">
<meta name="twitter:image" content="<%= og_img %>">
<% end %>
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="<%= t(:seo_title) %>">
<meta name="twitter:description" content="<%= desc %>">
<script type="application/ld+json"><%== json_ld(@board, desc) %></script>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🎱</text></svg>">
<style>
:root {
@ -52,13 +71,14 @@
.grid { margin-top: 6px; }
.cell {
position: relative; aspect-ratio: 1; padding: 8px; overflow: hidden; width: 100%;
background: var(--card) center / cover no-repeat; border: 1px solid var(--line); border-radius: 8px;
background: var(--card); border: 1px solid var(--line); border-radius: 8px;
display: flex; flex-direction: column; justify-content: space-between;
text-align: left; color: inherit; font: inherit; cursor: pointer;
}
.cell.photo { color: var(--on-photo); border-color: transparent; }
.cell.photo::before { content: ""; position: absolute; inset: 0; background: linear-gradient(to top, var(--shade), rgb(0 0 0 / .15) 70%); }
.cell > * { position: relative; z-index: 1; }
.cell .bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: 0; }
.cell.photo::before { content: ""; position: absolute; inset: 0; z-index: 1; background: linear-gradient(to top, var(--shade), rgb(0 0 0 / .15) 70%); }
.cell > :not(.bg) { position: relative; z-index: 2; }
.cell .n { font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }
.cell.photo .n, .cell.photo .sub { color: var(--on-photo-muted); }
.cell .name { font-weight: 700; font-size: clamp(11px, 1.9vw, 15px); line-height: 1.15; }
@ -67,12 +87,12 @@
.cell.hit { border-color: var(--hit); }
.cell.hit:not(.photo) { background-color: var(--hit-bg); }
.cell.hit::after {
content: ""; position: absolute; inset: 0; z-index: 2; pointer-events: none; opacity: .8;
content: ""; position: absolute; inset: 0; z-index: 3; pointer-events: none; opacity: .8;
background:
linear-gradient(to top right, transparent calc(50% - 2px), var(--hit) calc(50% - 2px), var(--hit) calc(50% + 2px), transparent calc(50% + 2px)),
linear-gradient(to top left, transparent calc(50% - 2px), var(--hit) calc(50% - 2px), var(--hit) calc(50% + 2px), transparent calc(50% + 2px));
}
.cell .stamp { position: absolute; right: 6px; top: 4px; font-size: clamp(16px, 3vw, 24px); z-index: 3; }
.cell .stamp { position: absolute; right: 6px; top: 4px; font-size: clamp(16px, 3vw, 24px); z-index: 4; }
.cell.win { box-shadow: 0 0 0 3px var(--win); }
.cell:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }