|
| 1 | +import * as path from "node:path"; |
| 2 | + |
| 3 | +import { Resvg } from "@resvg/resvg-js"; |
| 4 | + |
| 5 | +// Generates the 1200x630 social card images (og:image / twitter:card) served at |
| 6 | +// /social-card.png by the www and esm workers (app pages reference the www card). |
| 7 | +// Run with `pnpm run build:social-cards` from the repo root after changing any of |
| 8 | +// the variants below. |
| 9 | + |
| 10 | +const SANS = "Helvetica Neue, Helvetica, Arial, sans-serif"; |
| 11 | +const MONO = "Menlo, Monaco, Consolas, monospace"; |
| 12 | + |
| 13 | +const BLUE = "#5da9ff"; |
| 14 | +const GRAY = "#9ca3af"; |
| 15 | +const DIM = "#6b7280"; |
| 16 | + |
| 17 | +interface Variant { |
| 18 | + file: string; |
| 19 | + title: string; |
| 20 | + tagline: string; |
| 21 | + url: Array<[text: string, color: string]>; |
| 22 | +} |
| 23 | + |
| 24 | +const variants: Variant[] = [ |
| 25 | + { |
| 26 | + file: "packages/unpkg-www/public/social-card.png", |
| 27 | + title: "UNPKG", |
| 28 | + tagline: "The CDN for everything on npm", |
| 29 | + url: [ |
| 30 | + ["unpkg.com/", GRAY], |
| 31 | + [":package", BLUE], |
| 32 | + ["@", DIM], |
| 33 | + [":version", BLUE], |
| 34 | + ["/", DIM], |
| 35 | + [":file", BLUE], |
| 36 | + ], |
| 37 | + }, |
| 38 | + { |
| 39 | + file: "packages/unpkg-esm/public/social-card.png", |
| 40 | + title: "UNPKG ESM", |
| 41 | + tagline: "Browser-ready ES modules for every npm package", |
| 42 | + url: [ |
| 43 | + ["esm.unpkg.com/", GRAY], |
| 44 | + [":package", BLUE], |
| 45 | + ["@", DIM], |
| 46 | + [":version", BLUE], |
| 47 | + ["/", DIM], |
| 48 | + [":subpath", BLUE], |
| 49 | + ], |
| 50 | + }, |
| 51 | +]; |
| 52 | + |
| 53 | +function createSvg(variant: Variant): string { |
| 54 | + let urlSpans = variant.url |
| 55 | + .map(([text, color]) => `<tspan fill="${color}">${text.replace(/&/g, "&").replace(/</g, "<")}</tspan>`) |
| 56 | + .join(""); |
| 57 | + |
| 58 | + // Menlo advances ~0.603em per character, so size the chip to fit the URL text |
| 59 | + let urlChars = variant.url.reduce((n, [text]) => n + text.length, 0); |
| 60 | + let chipWidth = 64 + Math.round(urlChars * 30 * 0.603); |
| 61 | + |
| 62 | + return `<svg width="1200" height="630" viewBox="0 0 1200 630" xmlns="http://www.w3.org/2000/svg"> |
| 63 | + <rect width="1200" height="630" fill="#000000"/> |
| 64 | +
|
| 65 | + <!-- giant cropped U watermark bleeding off the right edge --> |
| 66 | + <text x="1265" y="660" text-anchor="end" font-family="${SANS}" font-weight="bold" font-size="860" |
| 67 | + fill="#101010">U</text> |
| 68 | +
|
| 69 | + <!-- wordmark --> |
| 70 | + <text x="80" y="330" font-family="${SANS}" font-weight="bold" font-size="148" letter-spacing="-4" |
| 71 | + fill="#ffffff">${variant.title}</text> |
| 72 | +
|
| 73 | + <!-- tagline --> |
| 74 | + <text x="84" y="410" font-family="${SANS}" font-weight="normal" font-size="42" |
| 75 | + fill="#a3a3a3">${variant.tagline}</text> |
| 76 | +
|
| 77 | + <!-- URL pattern chip --> |
| 78 | + <rect x="80" y="472" rx="14" width="${chipWidth}" height="76" fill="#0d0d0d" stroke="#262626" stroke-width="2"/> |
| 79 | + <text x="112" y="521" font-family="${MONO}" font-size="30">${urlSpans}</text> |
| 80 | +</svg>`; |
| 81 | +} |
| 82 | + |
| 83 | +let rootDir = path.resolve(import.meta.dirname, ".."); |
| 84 | + |
| 85 | +for (let variant of variants) { |
| 86 | + let resvg = new Resvg(createSvg(variant), { |
| 87 | + fitTo: { mode: "width", value: 1200 }, |
| 88 | + font: { loadSystemFonts: true }, |
| 89 | + }); |
| 90 | + |
| 91 | + let png = resvg.render().asPng(); |
| 92 | + let file = path.join(rootDir, variant.file); |
| 93 | + await Bun.write(file, png); |
| 94 | + console.log(`Wrote ${variant.file} (${png.length} bytes)`); |
| 95 | +} |
0 commit comments