Everything lived on one URL per language, with each datacenter's details and incidents inside a <dialog>. That page can rank for the brand and a couple of head terms, but not for searches like "Yandex Sasovo drone strike". Add /:locale/dc/:slug for every datacenter, reserve included. Each page has its own title (naming the latest incident when there is one), a description built from the facts, canonical and hreflang links, the photo as og:image, and JSON-LD Place + BreadcrumbList. It shows the facts in a sentence or two, the incident list with sources, and prev/next links by position. The slug never changes, so the URLs are stable. The card cells are now links to these pages. A plain click still opens the dialog, while modified clicks open the page. The dialog and the incident log link to the page too, and the sitemap lists every page in all three languages. The layout takes title, description, alternates and JSON-LD from the route, and the dialog and the page share the meta and incident partials. Assisted-by: Claude:opus-5
153 lines
8.6 KiB
Text
153 lines
8.6 KiB
Text
<% base = base_url %>
|
|
<% og_img = @og && "#{base}/p/#{@og.og_file}" %>
|
|
<!doctype html>
|
|
<html lang="<%= @locale %>">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title><%= @title %></title>
|
|
<meta name="description" content="<%= @desc %>">
|
|
<meta name="robots" content="<%= @noindex ? 'noindex, follow' : 'index, follow, max-image-preview:large' %>">
|
|
<meta name="theme-color" content="#121212">
|
|
<link rel="canonical" href="<%= base %><%= @path_for.(@locale) %>">
|
|
<% Bingo::LOCALES.each do |l| %>
|
|
<link rel="alternate" hreflang="<%= l %>" href="<%= base %><%= @path_for.(l) %>">
|
|
<% end %>
|
|
<% if @x_default %>
|
|
<link rel="alternate" hreflang="x-default" href="<%= base %><%= @x_default %>">
|
|
<% end %>
|
|
<link rel="sitemap" type="application/xml" href="/sitemap.xml">
|
|
<meta property="og:type" content="website">
|
|
<meta property="og:site_name" content="<%= t(:title) %>">
|
|
<meta property="og:title" content="<%= @title %>">
|
|
<meta property="og:description" content="<%= @desc %>">
|
|
<meta property="og:url" content="<%= base %><%= @path_for.(@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 %>">
|
|
<% if @og.photo_og %>
|
|
<meta property="og:image:type" content="image/jpeg">
|
|
<meta property="og:image:width" content="<%= Bingo::Photo::OG_SIZE[0] %>">
|
|
<meta property="og:image:height" content="<%= Bingo::Photo::OG_SIZE[1] %>">
|
|
<% end %>
|
|
<meta property="og:image:alt" content="<%= @og.l(:name, @locale) %>, <%= @og.l(:city, @locale) %>">
|
|
<meta name="twitter:image" content="<%= og_img %>">
|
|
<% end %>
|
|
<meta name="twitter:card" content="summary_large_image">
|
|
<meta name="twitter:title" content="<%= @title %>">
|
|
<meta name="twitter:description" content="<%= @desc %>">
|
|
<script type="application/ld+json"><%== @ld %></script>
|
|
<link rel="icon" href="/favicon.ico" sizes="48x48">
|
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
|
<style>
|
|
:root {
|
|
--bg: #f4f1ea; --card: #fffdf8; --ink: #1d1b18; --muted: #6f6a60; --line: #d9d3c6;
|
|
--hit: #c8361d; --hit-bg: #fbe3dc; --win: #e7b416; --on-win: #1d1b18;
|
|
--shade: rgb(0 0 0 / .62); --on-photo: #fff; --on-photo-muted: rgb(255 255 255 / .75);
|
|
}
|
|
@media (prefers-color-scheme: dark) {
|
|
:root:not([data-theme="light"]) {
|
|
--bg: #121212; --card: #1c1b19; --ink: #ece7dc; --muted: #9a9384; --line: #34312b;
|
|
--hit: #ff6a4d; --hit-bg: #3a1d16; --win: #f0c43a;
|
|
}
|
|
}
|
|
:root[data-theme="dark"] {
|
|
--bg: #121212; --card: #1c1b19; --ink: #ece7dc; --muted: #9a9384; --line: #34312b;
|
|
--hit: #ff6a4d; --hit-bg: #3a1d16; --win: #f0c43a;
|
|
}
|
|
* { box-sizing: border-box; }
|
|
body { margin: 0; background: var(--bg); color: var(--ink); font: 15px/1.4 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
|
|
main { max-width: 980px; margin: 0 auto; padding: 24px 16px 48px; }
|
|
a { color: inherit; }
|
|
header { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 8px 16px; margin-bottom: 16px; }
|
|
h1 { margin: 0; font-size: clamp(28px, 6vw, 44px); letter-spacing: -.02em; }
|
|
h1 span { color: var(--hit); }
|
|
h1 small { display: block; margin-top: 4px; font-size: 16px; font-weight: 600; letter-spacing: 0; color: var(--muted); }
|
|
.switches { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
|
.toggle { display: flex; border: 1px solid var(--line); border-radius: 6px; overflow: hidden; }
|
|
.toggle a { padding: 2px 10px; font-size: 13px; text-decoration: none; }
|
|
.toggle a + a { border-left: 1px solid var(--line); }
|
|
.toggle a[aria-current] { background: var(--hit); color: var(--bg); }
|
|
.langs { display: flex; gap: 4px; }
|
|
.langs a { padding: 2px 8px; border: 1px solid var(--line); border-radius: 6px; text-decoration: none; font-size: 13px; text-transform: uppercase; }
|
|
.langs a[aria-current] { background: var(--ink); color: var(--bg); border-color: var(--ink); }
|
|
.meta { color: var(--muted); font-size: 14px; }
|
|
.meta b { color: var(--ink); font-variant-numeric: tabular-nums; }
|
|
.lead { color: var(--muted); margin: 0 0 16px; max-width: 60ch; }
|
|
.banner { margin: 0 0 16px; padding: 12px 16px; border-radius: 10px; background: var(--win); color: var(--on-win); font-weight: 700; font-size: 18px; }
|
|
|
|
.letters, .grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 6px; }
|
|
.letters div { text-align: center; font-weight: 900; font-size: clamp(22px, 5vw, 36px); color: var(--hit); }
|
|
.grid { margin-top: 6px; }
|
|
.cell {
|
|
position: relative; aspect-ratio: 1; padding: 8px; overflow: hidden; width: 100%;
|
|
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; text-decoration: none; cursor: pointer;
|
|
}
|
|
.cell.photo { color: var(--on-photo); border-color: transparent; }
|
|
.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; }
|
|
.cell .sub { font-size: clamp(10px, 1.4vw, 12px); color: var(--muted); }
|
|
.cell .tag { font-size: 10px; text-transform: uppercase; letter-spacing: .05em; }
|
|
.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: 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: 4; }
|
|
.cell.win { box-shadow: 0 0 0 3px var(--win); }
|
|
.cell:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
|
|
|
|
section { margin-top: 32px; }
|
|
h2 { font-size: 18px; margin: 0 0 12px; }
|
|
.log { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--line); }
|
|
.log li { display: grid; grid-template-columns: 96px 28px 1fr; gap: 8px; padding: 10px 0; border-bottom: 1px solid var(--line); }
|
|
.log time, .log .dc { color: var(--muted); }
|
|
.log .dc { font-size: 13px; }
|
|
.empty { color: var(--muted); }
|
|
|
|
dialog { width: min(520px, calc(100vw - 32px)); border: 1px solid var(--line); border-radius: 12px; background: var(--card); color: var(--ink); padding: 0; }
|
|
dialog::backdrop { background: rgb(0 0 0 / .55); }
|
|
dialog img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; }
|
|
dialog .body { padding: 16px 20px 20px; }
|
|
dialog h3 { margin: 0 0 4px; }
|
|
dialog .credit { font-size: 12px; color: var(--muted); margin-top: 4px; }
|
|
dialog ul { padding-left: 18px; }
|
|
dialog .actions { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-top: 12px; }
|
|
dialog form { margin: 0; }
|
|
.crumbs a { font-weight: 700; text-decoration: none; }
|
|
.crumbs a:hover { text-decoration: underline; }
|
|
.dc-page img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; border-radius: 10px; margin-bottom: 16px; }
|
|
.dc-page h1 { font-size: clamp(26px, 5vw, 38px); }
|
|
.dc-page.hit h1 { color: var(--hit); }
|
|
.dc-page .credit { font-size: 12px; color: var(--muted); margin-top: 4px; }
|
|
.dc-page .facts { max-width: 65ch; }
|
|
.dc-page ul { padding-left: 18px; }
|
|
.pager { display: flex; justify-content: space-between; gap: 12px; margin-top: 32px; padding-top: 12px; border-top: 1px solid var(--line); }
|
|
dialog button { font: inherit; padding: 6px 14px; border-radius: 6px; border: 1px solid var(--line); background: var(--bg); color: var(--ink); cursor: pointer; }
|
|
footer { margin-top: 32px; color: var(--muted); font-size: 13px; }
|
|
footer p { margin: 0 0 4px; }
|
|
@media (max-width: 560px) {
|
|
.cell { padding: 5px; border-radius: 6px; }
|
|
.cell .sub, .cell .n, .cell .tag { display: none; }
|
|
.letters, .grid { gap: 4px; }
|
|
.log li { grid-template-columns: 84px 24px 1fr; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<%== yield %>
|
|
</body>
|
|
</html>
|