// Generates branding/social-preview.png (1280x640) -- the SnapOtter social/OG card. // Echoes the landing hero: trust badges, the "Every file tool you need / Your files // never leave your network" headline, the supporting subhead, and the five modality // cards. Rendered from HTML with the real brand fonts via headless Chromium. // Run (tsx, because it imports the shared TS catalog for live tool counts): // apps/api/node_modules/.bin/tsx scripts/branding/generate-social-preview.mjs // // The same output is the canonical OG image; sync it to apps/landing/public/og-image.png // and apps/web/public/og-image.png (see scripts/branding/sync-og.sh). import { rmSync, writeFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { chromium } from "@playwright/test"; // Relative import (not the "@snapotter/shared" specifier) so the script runs // from the repo root under tsx without the workspace symlink on the resolve path. import { TOOLS, toolSection } from "../../packages/shared/src/index.ts"; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", ".."); const out = resolve(repoRoot, "branding/social-preview.png"); // Temp HTML lives in branding/ so the page origin is file:// and can load the // sibling logo and the ../apps fonts as file:// subresources. const htmlPath = resolve(repoRoot, "branding/.og.html"); // Enterprise trust badges (subset of Hero.astro's list). const BADGES = ["On-premise", "GDPR & HIPAA", "Air-gap", "SAML SSO", "Open source"]; // lucide icon inner-SVG (stroke), matching the icons used in CategoryCards.astro. const ICON = { image: '', video: '', music: '', fileText: '', database: '', }; // Mirrors CategoryCards.astro (label/blurb/color/icon). Counts are derived live // from toolSection() over the shared TOOLS catalog -- same source the landing // cards use -- so the card never drifts as tools are added. const sectionCount = (section) => TOOLS.filter((t) => toolSection(t) === section).length; const CARDS = [ { label: "Image Tools", count: sectionCount("image"), blurb: "Resize, convert, and enhance", color: "#E07832", icon: ICON.image, }, { label: "Video Tools", count: sectionCount("video"), blurb: "Trim, convert, and caption", color: "#BE4A3C", icon: ICON.video, }, { label: "Audio Tools", count: sectionCount("audio"), blurb: "Convert, trim, and transcribe", color: "#2C7A75", icon: ICON.music, }, { label: "PDF & Documents", count: sectionCount("pdf"), blurb: "Merge, split, and convert", color: "#44568C", icon: ICON.fileText, }, { label: "File Tools", count: sectionCount("files"), blurb: "Convert and transform", color: "#5C8642", icon: ICON.database, }, ]; const total = CARDS.reduce((s, c) => s + c.count, 0); const badgesHtml = BADGES.map( (b) => `${b}`, ).join(""); const cardsHtml = CARDS.map( (c) => `
${c.icon} ${c.count} tools
${c.label}
${c.blurb}
`, ).join(""); const html = `
SnapOtter${total} tools
${badgesHtml}
Every file tool you need.Your files never leave your network.
The file-processing suite for teams that keep sensitive data in-house.
${cardsHtml}
`; writeFileSync(htmlPath, html); try { const browser = await chromium.launch(); const ctx = await browser.newContext({ viewport: { width: 1280, height: 640 } }); const page = await ctx.newPage(); await page.goto("file://" + htmlPath, { waitUntil: "load" }); await page.evaluate(async () => { await document.fonts.ready; }); await page.waitForTimeout(150); await page.screenshot({ path: out }); await browser.close(); console.log("wrote " + out); } finally { rmSync(htmlPath, { force: true }); }