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>
132 lines
6.6 KiB
JavaScript
132 lines
6.6 KiB
JavaScript
// 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();
|