// Renders the share images (Open Graph and Twitter) from the real hero, one per // 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 // // It drives the locally installed Chrome through Playwright and writes // src/assets/share/share-de.png and share-en.png (used by app/[locale]/layout.tsx). import { chromium } from "playwright"; const origin = (process.argv[2] ?? "https://mintel.me").replace(/\/$/, ""); // The browser language matches the page, so the language detection lets it through. const SHOTS = [ { locale: "de", path: "/", browserLocale: "de-DE" }, { locale: "en", path: "/en", browserLocale: "en-US" }, ]; const browser = await chromium.launch({ channel: "chrome", headless: true }); for (const shot of SHOTS) { const out = `src/assets/share/share-${shot.locale}.png`; const page = await browser.newPage({ viewport: { width: 1200, height: 630 }, deviceScaleFactor: 1, locale: shot.browserLocale, }); await page.goto(`${origin}${shot.path}`, { waitUntil: "load", timeout: 90000, }); await page.addStyleTag({ content: ` header, footer, #hero-cta, [class*="fixed"], nextjs-portal { display: none !important; } main > div > section:first-of-type > div.relative { min-height: 630px !important; height: 630px !important; padding: 0 72px !important; justify-content: center !important; max-width: none !important; } main > div > section:first-of-type ul { display: none !important; } main > div > section:first-of-type h1 { font-size: 150px !important; } main > div > section:first-of-type p { font-size: 34px !important; max-width: 900px !important; margin-top: 36px !important; } 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; 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. await page.waitForTimeout(2000); await page.evaluate(() => { const section = document.querySelector("main section"); const tag = document.createElement("div"); tag.textContent = "MARC MINTEL"; tag.style.cssText = "position:absolute;left:72px;top:54px;z-index:20;font:700 15px ui-monospace,Menlo,monospace;letter-spacing:.3em;color:#0f172a"; const site = document.createElement("div"); site.textContent = "mintel.me"; site.style.cssText = "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); await page.screenshot({ path: out, clip: { x: 0, y: 0, width: 1200, height: 630 }, }); await page.close(); console.log("written:", out); } await browser.close();