Every served page now carries its og: and twitter: block from site/og/pages.mjs through site/build.mjs (injected between share markers on the hand-written pages, through meta() on the generated ones); nothing is pasted per page. New cards under site/og/: eighteen 1200 x 630 PNGs at about 100 KB and a 1200 x 1200 square each (the second og:image for the readers that crop square), rendered from site/og/card.html by site/og/render.mjs on build box 2 with its Playwright, never on the Mac. The old og.png family stays on disk for cached links; nothing points at it. Titles under 60, descriptions under 110, checked by the build. One command refreshes every card: node site/og/render.mjs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
85 lines
6.1 KiB
HTML
85 lines
6.1 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>Igneum share card</title>
|
||
<!-- The share card template (the link-preview lane, 8 October 2026). Not a served page: site/og/render.mjs opens it with
|
||
?card=<key>&size=wide|square, reads the lines from site/og/pages.mjs and screenshots the stage. Palette and type are the
|
||
site's own tokens (site/site.css): obsidian, ember, molten, bone, ash; Unbounded for the line, IBM Plex for the rest.
|
||
Design: one centred stack so the centre square crop (WhatsApp's small thumbnail) still holds the mark and the line;
|
||
a soft ember glow behind the mark; a thin ember rule; the route in mono at the foot. Nothing on it dates. -->
|
||
<style>
|
||
@font-face{font-family:'Unbounded';font-weight:700;src:url(../fonts/unbounded-700.woff2) format('woff2')}
|
||
@font-face{font-family:'Unbounded';font-weight:900;src:url(../fonts/unbounded-900.woff2) format('woff2')}
|
||
@font-face{font-family:'Plex Sans';font-weight:400;src:url(../fonts/plex-sans-400.woff2) format('woff2')}
|
||
@font-face{font-family:'Plex Sans';font-weight:500;src:url(../fonts/plex-sans-500.woff2) format('woff2')}
|
||
@font-face{font-family:'Plex Mono';font-weight:500;src:url(../fonts/plex-mono-500.woff2) format('woff2')}
|
||
:root{--obsidian:#0C0C0E;--graphite:#16161A;--line:#2A2A30;--ember:#F2541B;--ember-hi:#FF6A2B;--molten:#FFB35C;--bone:#F4F1EC;--ink-2:#C9C7C2;--ash:#9A9A9E}
|
||
html,body{margin:0;background:var(--obsidian)}
|
||
body{font-family:'Plex Sans',Arial,sans-serif;color:var(--bone);-webkit-font-smoothing:antialiased;text-rendering:geometricPrecision}
|
||
#stage{position:relative;width:1200px;height:630px;overflow:hidden;background:var(--obsidian)}
|
||
#stage.square{height:1200px}
|
||
/* the glow: one soft ember light behind the mark, a blurred disc rather than a radial gradient (Chromium dithers gradients, which doubles the PNG) */
|
||
.glow{position:absolute;left:50%;top:150px;width:560px;height:560px;transform:translate(-50%,-50%);border-radius:50%;background:var(--ember);opacity:.26;filter:blur(150px)}
|
||
.square .glow{top:360px;width:720px;height:720px;filter:blur(190px)}
|
||
.stack{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:0 110px;gap:0}
|
||
.square .stack{padding:0 120px}
|
||
.mark{width:132px;height:132px;flex:none;filter:drop-shadow(0 0 28px rgba(242,84,27,.45))}
|
||
.square .mark{width:230px;height:230px;margin-bottom:12px}
|
||
.kicker{margin:26px 0 14px;font:500 17px/1 'Plex Mono',monospace;letter-spacing:.22em;text-transform:uppercase;color:var(--molten)}
|
||
.square .kicker{margin:40px 0 22px;font-size:22px}
|
||
.line{margin:0;font:700 54px/1.14 'Unbounded',sans-serif;letter-spacing:-.012em;color:var(--bone);max-width:980px;text-wrap:balance}
|
||
.square .line{font-size:70px;max-width:960px}
|
||
.sub{margin:22px 0 0;font:400 26px/1.4 'Plex Sans',sans-serif;color:var(--ink-2);max-width:860px;text-wrap:balance}
|
||
.square .sub{margin-top:30px;font-size:32px;max-width:900px}
|
||
.foot{position:absolute;left:0;right:0;bottom:0;height:78px;display:flex;align-items:center;justify-content:space-between;padding:0 60px;border-top:1px solid var(--line)}
|
||
.square .foot{height:110px;padding:0 80px}
|
||
.foot .brand{display:flex;align-items:center;gap:12px;font:900 19px/1 'Unbounded',sans-serif;letter-spacing:.14em;color:var(--bone)}
|
||
.square .foot .brand{font-size:24px}
|
||
.foot .brand svg{width:22px;height:22px}
|
||
.square .foot .brand svg{width:28px;height:28px}
|
||
.foot .route{font:500 18px/1 'Plex Mono',monospace;color:var(--ember-hi);letter-spacing:.02em}
|
||
.square .foot .route{font-size:23px}
|
||
.rule{position:absolute;left:50%;bottom:78px;width:160px;height:3px;transform:translateX(-50%);background:var(--ember);border-radius:2px;opacity:.95}
|
||
.square .rule{bottom:110px;width:200px}
|
||
/* fit: a card whose stack would run into the foot shrinks its type a step (checked by the renderer, which fails on an overflow) */
|
||
.tight .line{font-size:48px}.tight .sub{font-size:24px}
|
||
.square.tight .line{font-size:62px}.square.tight .sub{font-size:30px}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="stage">
|
||
<div class="glow"></div>
|
||
<div class="stack">
|
||
<svg class="mark" viewBox="0 0 100 100" aria-hidden="true"><polygon points="50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34" fill="#F2541B"/><polygon points="50,42 59,58 50,82 41,58" fill="#0C0C0E"/></svg>
|
||
<div class="kicker" id="kicker"></div>
|
||
<h1 class="line" id="line"></h1>
|
||
<p class="sub" id="sub"></p>
|
||
</div>
|
||
<div class="rule"></div>
|
||
<div class="foot">
|
||
<div class="brand"><svg viewBox="0 0 100 100" aria-hidden="true"><polygon points="50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34" fill="#F2541B"/><polygon points="50,42 59,58 50,82 41,58" fill="#0C0C0E"/></svg>IGNEUM</div>
|
||
<div class="route" id="route"></div>
|
||
</div>
|
||
</div>
|
||
<script type="module">
|
||
import { CARDS } from './pages.mjs';
|
||
const q = new URLSearchParams(location.search);
|
||
const key = q.get('card') || 'home', size = q.get('size') || 'wide';
|
||
const c = CARDS[key]; if (!c) throw new Error('no card ' + key);
|
||
const stage = document.getElementById('stage'); if (size === 'square') stage.classList.add('square');
|
||
// the widow guard: the last two words of each line are glued (copy law rule 18)
|
||
const glue = s => s.replace(/ (\S+)$/, ' $1');
|
||
document.getElementById('kicker').textContent = c.kicker;
|
||
document.getElementById('line').textContent = glue(c.title);
|
||
document.getElementById('sub').textContent = glue(c.sub);
|
||
document.getElementById('route').textContent = c.route;
|
||
document.title = 'Igneum share card: ' + key + ' ' + size;
|
||
await document.fonts.ready;
|
||
// a stack that reaches the foot takes the tight step; still too tall is a render failure the renderer reads
|
||
const fits = () => { const r = document.querySelector('.stack').getBoundingClientRect(); const h = [...document.querySelectorAll('.stack > *')].reduce((a, e) => a + e.getBoundingClientRect().height, 0) + 40; return h < r.height - (size === 'square' ? 110 : 78) - 40; };
|
||
if (!fits()) stage.classList.add('tight');
|
||
window.__card = { key, size, fits: fits(), lines: Math.round(document.getElementById('line').getBoundingClientRect().height / (parseFloat(getComputedStyle(document.getElementById('line')).fontSize) * 1.14)) };
|
||
</script>
|
||
</body>
|
||
</html>
|