feat: share image in the look of the site's dark sections
Build & Deploy / 🔔 Notify (push) Successful in 2s
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🏗️ Build (push) Successful in 6m48s
Build & Deploy / 🚀 Deploy (push) Successful in 19s
Build & Deploy / 🩺 Smoke Test (push) Successful in 3s

The link preview was a plain white screenshot of the hero. It is still
rendered from the real hero, so it cannot drift from the site, but now as
light type on the dark surface with a lit frame and binary digits coming
off its edge. The digits are seeded, so every run gives the same picture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 15:53:01 +02:00
co-authored by Claude Opus 5.5
parent ca0eddfde8
commit c67c13d82f
3 changed files with 67 additions and 3 deletions
+67 -3
View File
@@ -1,6 +1,8 @@
// Renders the share images (Open Graph and Twitter) from the real hero, one per
// language, so they always match the site. Run it after the hero headline or its
// design changes:
// language, so they always match the site. The hero is shown the way the dark
// sections of the site look: light type on a dark surface, a lit edge, and
// binary digits coming off it. Run it after the hero headline or its design
// changes:
//
// pnpm --filter @mintel/web share-image (uses the live site)
// pnpm --filter @mintel/web share-image http://localhost:3000
@@ -37,7 +39,9 @@ for (const shot of SHOTS) {
main > div > section:first-of-type .grid { margin-top: 0 !important; display: block !important; }
main > div > section:first-of-type h1 span:nth-of-type(2) { color: rgba(15, 23, 42, 0.2) !important; }
main > div > section:first-of-type > div.relative { padding-top: 34px !important; }
html, body { overflow: hidden !important; }
html, body { overflow: hidden !important; background: #000 !important; }
/* Dark version of the hero: inverted, with the hues turned back. */
main > div > section:first-of-type { filter: invert(1) hue-rotate(180deg) !important; }
`,
});
// Let the page finish starting up first; it would discard the added labels.
@@ -54,6 +58,66 @@ for (const shot of SHOTS) {
"position:absolute;right:72px;top:54px;z-index:20;font:500 15px ui-monospace,Menlo,monospace;letter-spacing:.2em;color:#64748b";
section.style.position = "relative";
section.append(tag, site);
// Everything below lies over the inverted hero and is not inverted itself.
const W = 1200;
const H = 630;
const layer = (css) => {
const el = document.createElement("div");
el.style.cssText = `position:absolute;left:0;top:0;width:${W}px;height:${H}px;z-index:60;pointer-events:none;${css}`;
document.body.append(el);
return el;
};
document.body.style.position = "relative";
// Black becomes the slate of the site's dark sections; light type stays light.
layer("background:#0f172a;mix-blend-mode:screen");
// Light coming from the lower right corner.
layer(
"mix-blend-mode:screen;background:radial-gradient(58% 62% at 92% 108%, rgba(99,102,241,.85), transparent 70%),radial-gradient(34% 40% at 104% -6%, rgba(129,140,248,.4), transparent 70%)",
);
// A frame whose lower right edge is lit, with a soft glow behind it.
const INSET = 22;
const edge = (blur, width, opacity) => {
const el = layer(
`left:${INSET}px;top:${INSET}px;width:${W - 2 * INSET}px;height:${H - 2 * INSET}px;box-sizing:border-box;border-radius:30px;padding:${width}px;opacity:${opacity};filter:blur(${blur}px);` +
"background:conic-gradient(from 60deg at 50% 50%, transparent 0 8%, rgba(129,140,248,.95) 22%, #fff 30%, rgba(129,140,248,.95) 40%, transparent 56% 100%);" +
"-webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask:linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0)",
);
return el;
};
layer(
`left:${INSET}px;top:${INSET}px;width:${W - 2 * INSET}px;height:${H - 2 * INSET}px;box-sizing:border-box;border-radius:30px;border:1px solid rgba(165,180,252,.16)`,
);
edge(26, 14, 0.9);
edge(8, 5, 0.9);
edge(0, 2, 1);
// Digits coming off the lit edge. Seeded, so the picture is the same on every run.
let seed = 20261004;
const random = () => {
seed = (seed * 1664525 + 1013904223) % 4294967296;
return seed / 4294967296;
};
const digits = layer(
"font:500 15px var(--font-jetbrains-mono), ui-monospace, Menlo, monospace;color:#c7d2fe",
);
for (let i = 0; i < 150; i++) {
// Along the bottom edge (right half) and up the right edge (lower half).
const alongBottom = random() < 0.62;
const spread = Math.pow(random(), 1.8);
const x = alongBottom ? W * 0.42 + random() * (W * 0.58 - INSET - 30) : 0;
// Under the lead text the digits stay close to the edge.
const reach = spread * (alongBottom && x < 820 ? 54 : 120);
const left = alongBottom ? x : W - INSET - 14 - reach;
const y = alongBottom
? H - INSET - 16 - reach
: H * 0.3 + random() * (H * 0.7 - INSET - 30);
const digit = document.createElement("span");
digit.textContent = random() < 0.5 ? "0" : "1";
const big = random() < 0.2;
digit.style.cssText = `position:absolute;left:${left.toFixed(1)}px;top:${y.toFixed(1)}px;opacity:${((1 - spread) * (0.35 + random() * 0.65)).toFixed(2)};font-size:${big ? 19 : 14}px;font-weight:${big ? 700 : 500}`;
digits.append(digit);
}
window.scrollTo(0, 0);
});
await page.waitForTimeout(4500);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 278 KiB