Initial import of the public datacenter bingo site
The bingo card ran inside the asxpio container, dispatched by Host. Give the public site its own repo, image and deploy so it can be released and mirrored independently of asxp.io. Editing stays in the asxp.io admin, which keeps owning the schema and the photo pipeline; this app only reads the shared bingo tables and the bingo/ photos, through a read-only DB role and MinIO user. Tests load a snapshot of the asxpio-built schema (test/schema.sql). Assisted-by: Claude:opus-5
This commit is contained in:
commit
062611fa21
35 changed files with 2050 additions and 0 deletions
2
views/_credits.erb
Normal file
2
views/_credits.erb
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<p class="credits"><%= t(:credits_ai) %>
|
||||
<%== format(Rack::Utils.escape_html(t(:credits_hym)), link: '<a href="https://hackyourmom.com/en/karta-data-czentriv-na-rasii/" target="_blank" rel="noopener">hackyourmom</a>') %></p>
|
||||
10
views/_dc_incidents.erb
Normal file
10
views/_dc_incidents.erb
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<% if dc.incidents.empty? %>
|
||||
<p class="empty"><%= t(:no_incidents) %></p>
|
||||
<% else %>
|
||||
<ul>
|
||||
<% dc.incidents.each do |i| %>
|
||||
<li><%= kind_icon(i.kind) %> <time datetime="<%= i.occurred_on %>"><%= fmt_date(i.occurred_on) %></time> —
|
||||
<% if i.url %><a href="<%= i.url %>" target="_blank" rel="noopener nofollow"><%= i.l(:title, @locale) %></a><% else %><%= i.l(:title, @locale) %><% end %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
9
views/_dc_meta.erb
Normal file
9
views/_dc_meta.erb
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<div class="meta">
|
||||
<%= [dc.operator, mw(dc), dc.l(:city, @locale), status_label(dc) || (dc.year && format(t(:since), dc.year))].compact.join(' · ') %>
|
||||
</div>
|
||||
<% if dc.photo_credit || dc.info_url %>
|
||||
<div class="credit">
|
||||
<% if dc.photo_credit %><%= t(:photo) %>: <%= dc.photo_credit %><% end %>
|
||||
<% if dc.info_url %> · <a href="<%= dc.info_url %>" target="_blank" rel="noopener nofollow"><%= t(:source) %></a><% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
35
views/dc.erb
Normal file
35
views/dc.erb
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<% dc = @dc %>
|
||||
<main>
|
||||
<header>
|
||||
<nav class="crumbs"><a href="/<%= @locale %>"><%= t(:title) %></a></nav>
|
||||
<nav class="langs">
|
||||
<% Bingo::LOCALES.each do |l| %>
|
||||
<a href="<%= @path_for.(l) %>" hreflang="<%= l %>" lang="<%= l %>" <%== 'aria-current="page"' if l == @locale %>><%= l %></a>
|
||||
<% end %>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<article class="dc-page<%= ' hit' if dc.incidents.any? %>">
|
||||
<% if (photo = photo_path(dc, 1200)) %><img src="<%= photo %>" alt="<%= dc.l(:name, @locale) %>, <%= dc.l(:city, @locale) %>" decoding="async"><% end %>
|
||||
<h1><%= dc.l(:name, @locale) %></h1>
|
||||
<%== erb :_dc_meta, layout: false, locals: { dc: dc } %>
|
||||
<p class="facts"><%= dc_facts(dc).join(' ') %></p>
|
||||
<% dc.about(@locale).each do |para| %><p class="facts"><%= para %></p><% end %>
|
||||
<section>
|
||||
<h2><%= t(:incidents) %></h2>
|
||||
<%== erb :_dc_incidents, layout: false, locals: { dc: dc } %>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
<nav class="pager">
|
||||
<% if @prev %><a href="<%= dc_path(@prev) %>" rel="prev">← #<%= @prev.position %> <%= @prev.l(:name, @locale) %></a><% else %><span></span><% end %>
|
||||
<% if @next %><a href="<%= dc_path(@next) %>" rel="next">#<%= @next.position %> <%= @next.l(:name, @locale) %> →</a><% end %>
|
||||
</nav>
|
||||
<p><a href="/<%= @locale %>"><%= t(:back) %></a></p>
|
||||
|
||||
<footer>
|
||||
<p><%= t(:footer) %></p>
|
||||
<%== erb :_credits, layout: false %>
|
||||
<p>© <%= Date.today.year %> <a href="https://asxp.io">asxp.io</a></p>
|
||||
</footer>
|
||||
</main>
|
||||
117
views/index.erb
Normal file
117
views/index.erb
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
<% b = @board %>
|
||||
<main>
|
||||
<header>
|
||||
<h1><%= t(:lead) %> <span><%= t(:accent) %></span> <small><%= t(:tagline) %></small></h1>
|
||||
<div class="switches">
|
||||
<nav class="toggle" aria-label="<%= t(:view_label) %>">
|
||||
<% Bingo::VIEWS.each do |v| %>
|
||||
<a href="<%= card_path(@locale, v) %>" <%== 'aria-current="page"' if v == @view %>><%= t(:"view_#{v}") %></a>
|
||||
<% end %>
|
||||
</nav>
|
||||
<nav class="langs">
|
||||
<% Bingo::LOCALES.each do |l| %>
|
||||
<a href="<%= card_path(l) %>" hreflang="<%= l %>" lang="<%= l %>" <%== 'aria-current="page"' if l == @locale %>><%= l %></a>
|
||||
<% end %>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<p class="lead"><%= t(:desc) %></p>
|
||||
<% provs = Bingo::Board.providers(@view) %>
|
||||
<form class="providers" method="get" action="/<%= @locale %>">
|
||||
<% if @view == 'all' %><input type="hidden" name="view" value="all"><% end %>
|
||||
<fieldset>
|
||||
<legend><%= t(:providers_label) %></legend>
|
||||
<% provs.each do |slug, total, crossed| %>
|
||||
<label><input type="checkbox" name="p[]" value="<%= slug %>" <%= 'checked' if @providers.include?(slug) %>> <%= Bingo.provider_name(slug, @locale) %> <span><%= crossed %>/<%= total %></span></label>
|
||||
<% end %>
|
||||
</fieldset>
|
||||
<div class="provider-actions">
|
||||
<button type="submit"><%= t(:providers_apply) %></button>
|
||||
<% if @board.filtered? %><a href="<%= card_path(@locale, @view, []) %>"><%= t(:providers_reset) %></a><% end %>
|
||||
</div>
|
||||
</form>
|
||||
<p class="meta">
|
||||
<%= t(:crossed) %> <b><%= b.hit.count(true) %></b>/<%= b.cells.size %>
|
||||
· <%= t(:lines) %> <b><%= b.wins.size %></b>
|
||||
<% if b.updated_on %>· <%= t(:updated) %> <b><%= fmt_date(b.updated_on) %></b><% end %>
|
||||
</p>
|
||||
|
||||
<% if b.wins.any? %>
|
||||
<div class="banner">
|
||||
<%= b.wins.size == Bingo::LINES.size ? t(:full) : b.wins.size > 1 ? format(t(:bingo_n), b.wins.size) : t(:bingo) %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="letters" aria-hidden="true"><% t(:letters).each do |ch| %><div><%= ch %></div><% end %></div>
|
||||
<div class="grid">
|
||||
<% b.cells.each_with_index do |dc, idx| %>
|
||||
<% photo = photo_path(dc, 480) %>
|
||||
<a href="<%= dc_path(dc) %>" data-dlg="dlg-<%= dc.slug %>"
|
||||
class="cell<%= ' photo' if photo %><%= ' hit' if b.hit[idx] %><%= ' win' if b.win_cells.include?(idx) %>"
|
||||
aria-label="<%= dc.l(:name, @locale) %><%= " — #{kind_label(dc.incidents.first.kind)}" if b.hit[idx] %>">
|
||||
<% if photo %><img class="bg" src="<%= photo %>" alt="" width="480" height="480" decoding="async"<%== ' loading="lazy"' if idx >= 10 %>><% end %>
|
||||
<div>
|
||||
<div class="n">#<%= idx + 1 %><%= " · #{mw(dc, tag: false)}" if mw(dc) %></div>
|
||||
<% if status_label(dc) %><div class="tag"><%= status_label(dc) %></div><% end %>
|
||||
</div>
|
||||
<div>
|
||||
<div class="name"><%= dc.l(:name, @locale) %></div>
|
||||
<div class="sub"><%= dc.l(:city, @locale) %></div>
|
||||
</div>
|
||||
<% if b.hit[idx] %><div class="stamp"><%= kind_icon(dc.incidents.first.kind) %></div><% end %>
|
||||
</a>
|
||||
<% end %>
|
||||
<% (b.cells.size...Bingo::SIZE**2).each do %><div class="cell empty" aria-hidden="true"></div><% end %>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2><%= t(:log) %></h2>
|
||||
<ul class="log">
|
||||
<% if b.incidents.empty? %>
|
||||
<li class="empty"><%= t(:empty) %></li>
|
||||
<% end %>
|
||||
<% b.incidents.each do |i| %>
|
||||
<li>
|
||||
<time datetime="<%= i.occurred_on %>"><%= fmt_date(i.occurred_on) %></time>
|
||||
<span><%= kind_icon(i.kind) %></span>
|
||||
<div>
|
||||
<% if i.url %><a href="<%= i.url %>" target="_blank" rel="noopener nofollow"><%= i.l(:title, @locale) %></a><% else %><%= i.l(:title, @locale) %><% end %>
|
||||
<div class="dc"><a href="<%= dc_path(i.datacenter) %>"><%= i.datacenter.l(:name, @locale) %></a> · <%= kind_label(i.kind) %></div>
|
||||
</div>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<p><%= t(:footer) %></p>
|
||||
<%== erb :_credits, layout: false %>
|
||||
<p>© <%= Date.today.year %> <a href="https://asxp.io">asxp.io</a></p>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<% b.cells.each do |dc| %>
|
||||
<dialog id="dlg-<%= dc.slug %>">
|
||||
<% if (photo = photo_path(dc, 1200)) %><img src="<%= photo %>" alt="<%= dc.l(:name, @locale) %>, <%= dc.l(:city, @locale) %>" loading="lazy" decoding="async"><% end %>
|
||||
<div class="body">
|
||||
<h3><%= dc.l(:name, @locale) %></h3>
|
||||
<%== erb :_dc_meta, layout: false, locals: { dc: dc } %>
|
||||
<%== erb :_dc_incidents, layout: false, locals: { dc: dc } %>
|
||||
<div class="actions">
|
||||
<a href="<%= dc_path(dc) %>"><%= t(:dc_page) %></a>
|
||||
<form method="dialog"><button><%= t(:close) %></button></form>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
<% end %>
|
||||
|
||||
<script>
|
||||
// Cells are links to the datacenter pages; a plain click opens the dialog
|
||||
// instead, modified clicks (new tab etc.) keep the link.
|
||||
document.querySelectorAll('[data-dlg]').forEach(a => a.addEventListener('click', e => {
|
||||
if (e.button || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
||||
e.preventDefault();
|
||||
document.getElementById(a.dataset.dlg).showModal();
|
||||
}));
|
||||
</script>
|
||||
163
views/layout.erb
Normal file
163
views/layout.erb
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
<% 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; }
|
||||
.cell.empty { background: transparent; border-style: dashed; cursor: default; }
|
||||
.providers { margin: 0 0 16px; }
|
||||
.providers fieldset { border: 0; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.providers legend { font-size: 13px; color: var(--muted); margin-bottom: 6px; padding: 0; }
|
||||
.providers label { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border: 1px solid var(--line); border-radius: 999px; font-size: 13px; cursor: pointer; white-space: nowrap; }
|
||||
.providers label:has(input:checked) { border-color: var(--ink); }
|
||||
.providers input { accent-color: var(--hit); margin: 0; }
|
||||
.providers label span { color: var(--muted); font-variant-numeric: tabular-nums; }
|
||||
.provider-actions { display: flex; gap: 12px; align-items: center; margin-top: 8px; font-size: 13px; }
|
||||
.provider-actions button { font: inherit; padding: 3px 12px; border-radius: 6px; border: 1px solid var(--line); background: var(--card); color: var(--ink); cursor: pointer; }
|
||||
.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>
|
||||
16
views/sitemap.erb
Normal file
16
views/sitemap.erb
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
|
||||
<% @entries.each do |path_for, lastmod, x_default| %>
|
||||
<% Bingo::LOCALES.each do |l| %>
|
||||
<url>
|
||||
<loc><%= base_url %><%= path_for.(l) %></loc>
|
||||
<% if lastmod %> <lastmod><%= lastmod.utc.iso8601 %></lastmod>
|
||||
<% end %>
|
||||
<% Bingo::LOCALES.each do |alt| %> <xhtml:link rel="alternate" hreflang="<%= alt %>" href="<%= base_url %><%= path_for.(alt) %>"/>
|
||||
<% end %>
|
||||
<% if x_default %> <xhtml:link rel="alternate" hreflang="x-default" href="<%= base_url %><%= x_default %>"/>
|
||||
<% end %>
|
||||
</url>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</urlset>
|
||||
Loading…
Add table
Add a link
Reference in a new issue