Skip to content

Commit 97bc13b

Browse files
committed
promo: GitHub social-preview card generator + cards
Add scripts/gen-social-preview.js (npm run gen-social): renders 1280x640 GitHub social-preview cards in the Terminal brand style for @imqueue, rpc, core, cli and job. GitHub has no API for the social-preview image, so these are produced as files under promotion/social-preview/ for manual upload via each repo Settings -> Social preview.
1 parent 01f65c9 commit 97bc13b

7 files changed

Lines changed: 128 additions & 1 deletion

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@
4141
"sync-cli-guide:check": "node scripts/sync-cli-wiki.js --check",
4242
"indexnow:org": "node scripts/indexnow-ping.js org --exclude=/api/",
4343
"indexnow:com": "node scripts/indexnow-ping.js com",
44-
"gen-syndication": "node scripts/gen-syndication.js"
44+
"gen-syndication": "node scripts/gen-syndication.js",
45+
"gen-social": "node scripts/gen-social-preview.js"
4546
},
4647
"repository": {
4748
"type": "git",

promotion/social-preview/cli.png

71 KB
Loading

promotion/social-preview/core.png

72.6 KB
Loading

promotion/social-preview/job.png

72.2 KB
Loading

promotion/social-preview/org.png

71.9 KB
Loading

promotion/social-preview/rpc.png

70.9 KB
Loading

scripts/gen-social-preview.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/**
2+
* Generate GitHub social-preview cards (1280x640, GitHub's recommended size)
3+
* for the main @imqueue repos, in the Terminal (.org) brand style.
4+
*
5+
* node scripts/gen-social-preview.js (or: npm run gen-social)
6+
*
7+
* GitHub social-preview images can only be UPLOADED per-repo via
8+
* Settings -> Social preview (there is no API), so these are produced as files
9+
* for manual upload. Output: promotion/social-preview/<repo>.png
10+
*
11+
* Font handling mirrors gen-og-images.js (woff2 -> sfnt behind a throwaway
12+
* fontconfig) so librsvg/Pango can resolve the brand fonts.
13+
*/
14+
const fs = require("fs");
15+
const os = require("os");
16+
const path = require("path");
17+
const woff2 = require("wawoff2");
18+
19+
const ROOT = path.resolve(__dirname, "..");
20+
const FONTS_DIR = path.join(ROOT, "src", "_shared", "fonts");
21+
const OUT_DIR = path.join(ROOT, "promotion", "social-preview");
22+
const MONO = "JetBrains Mono";
23+
24+
const FONT_FILES = [
25+
"jetbrains-mono-latin-400-normal.woff2",
26+
"jetbrains-mono-latin-700-normal.woff2",
27+
];
28+
29+
async function setupFonts() {
30+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "imq-social-"));
31+
const ttfDir = path.join(dir, "ttf");
32+
const cacheDir = path.join(dir, "cache");
33+
fs.mkdirSync(ttfDir, { recursive: true });
34+
fs.mkdirSync(cacheDir, { recursive: true });
35+
for (const f of FONT_FILES) {
36+
const sfnt = await woff2.decompress(fs.readFileSync(path.join(FONTS_DIR, f)));
37+
fs.writeFileSync(path.join(ttfDir, f.replace(/\.woff2$/, ".ttf")), Buffer.from(sfnt));
38+
}
39+
const conf = path.join(dir, "fonts.conf");
40+
fs.writeFileSync(
41+
conf,
42+
`<?xml version="1.0"?>
43+
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
44+
<fontconfig>
45+
<dir>${ttfDir}</dir>
46+
<cachedir>${cacheDir}</cachedir>
47+
<config></config>
48+
</fontconfig>`
49+
);
50+
process.env.FONTCONFIG_FILE = conf;
51+
process.env.FONTCONFIG_PATH = dir;
52+
}
53+
54+
// Brand glyph (from src/_shared/_includes/brand-logo.html), viewBox "3 10 42 28".
55+
const glyph = (x, y, scale, color) => `
56+
<g transform="translate(${x},${y}) scale(${scale})" fill="none" stroke="${color}" stroke-width="4" stroke-linecap="round" stroke-linejoin="round">
57+
<path d="M33 12H15a8 8 0 0 0-8 8v8a8 8 0 0 0 8 8h18"/>
58+
<circle cx="16" cy="24" r="2.6" fill="${color}" stroke="none"/>
59+
<circle cx="24" cy="24" r="2.6" fill="${color}" stroke="none"/>
60+
<circle cx="41" cy="24" r="2.8" fill="${color}" stroke="none"/>
61+
</g>`;
62+
63+
const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
64+
65+
// name, tagline, install command
66+
const CARDS = [
67+
{ repo: "org", name: "@imqueue", tagline: "RPC over a message queue for Node & TypeScript", cmd: "npm i @imqueue/rpc" },
68+
{ repo: "rpc", name: "@imqueue/rpc", tagline: "Type-safe RPC over a message queue", cmd: "npm i @imqueue/rpc" },
69+
{ repo: "core", name: "@imqueue/core", tagline: "The Redis-backed messaging-queue engine", cmd: "npm i @imqueue/core" },
70+
{ repo: "cli", name: "@imqueue/cli", tagline: "Scaffolding & typed-client generation", cmd: "npm i -g @imqueue/cli" },
71+
{ repo: "job", name: "@imqueue/job", tagline: "Simple, safe-by-default Redis job queue", cmd: "npm i @imqueue/job" },
72+
];
73+
74+
// Fit the package name: shrink font so it never collides with the right edge.
75+
function nameSize(name) {
76+
if (name.length <= 8) return 96; // "@imqueue"
77+
if (name.length <= 13) return 78; // "@imqueue/core"
78+
return 70;
79+
}
80+
81+
function card({ name, tagline, cmd }) {
82+
const ns = nameSize(name);
83+
return `<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="640" viewBox="0 0 1280 640">
84+
<defs>
85+
<linearGradient id="gt" x1="0" y1="0" x2="1" y2="1">
86+
<stop offset="0" stop-color="#3ddc84"/>
87+
<stop offset="1" stop-color="#35d0e0"/>
88+
</linearGradient>
89+
<radialGradient id="tglow" cx="0.15" cy="0.15" r="0.6">
90+
<stop offset="0" stop-color="#3ddc84" stop-opacity="0.22"/>
91+
<stop offset="1" stop-color="#3ddc84" stop-opacity="0"/>
92+
</radialGradient>
93+
</defs>
94+
<rect width="1280" height="640" fill="#0a0e0d"/>
95+
<rect width="1280" height="640" fill="url(#tglow)"/>
96+
<rect x="0" y="0" width="1280" height="8" fill="url(#gt)"/>
97+
98+
<g transform="translate(96,150)">
99+
${glyph(0, 0, 2.4, "url(#gt)")}
100+
<text x="124" y="82" font-family="${MONO}" font-weight="700" font-size="${ns}" fill="#e8f0ec">${esc(name)}</text>
101+
</g>
102+
103+
<text x="100" y="372" font-family="${MONO}" font-weight="400" font-size="40" fill="#d5e2db">${esc(tagline)}</text>
104+
105+
<text x="100" y="486" font-family="${MONO}" font-weight="700" font-size="32" fill="#63e6a0"><tspan fill="#35d0e0">$</tspan> ${esc(cmd)}</text>
106+
107+
<text x="100" y="590" font-family="${MONO}" font-weight="700" font-size="28" fill="#7f8f89">open source · GPL-3.0</text>
108+
<text x="1180" y="590" text-anchor="end" font-family="${MONO}" font-weight="700" font-size="28" fill="#7f8f89">imqueue.org</text>
109+
</svg>`;
110+
}
111+
112+
async function main() {
113+
await setupFonts();
114+
const sharp = require("sharp"); // require after FONTCONFIG_* are set
115+
fs.mkdirSync(OUT_DIR, { recursive: true });
116+
for (const c of CARDS) {
117+
await sharp(Buffer.from(card(c))).png().toFile(path.join(OUT_DIR, `${c.repo}.png`));
118+
console.log(` wrote promotion/social-preview/${c.repo}.png`);
119+
}
120+
console.log(`\nUpload each per repo: Settings -> Options -> Social preview.`);
121+
}
122+
123+
main().catch((e) => {
124+
console.error(e);
125+
process.exit(1);
126+
});

0 commit comments

Comments
 (0)