MEGAINFRA-WEBAPP/scripts/build-og-image.mjs

123 lines
4.5 KiB
JavaScript
Raw Normal View History

"use strict";
/**
* Generates /public/og-image.png from the brand logo with the project
* colours and name baked in. Run with `node scripts/build-og-image.mjs`.
* Requires `sharp` (already a transitive Next.js dependency).
*
* The SVG embeds the Inter font directly via base64 so the image is
* identical on every machine regardless of which fonts are installed.
* If the font fetch fails we fall back to the system sans-serif at a
* conservatively smaller size so the wordmark still fits.
*/
import { promises as fs } from "node:fs";
import path from "node:path";
import sharp from "sharp";
const ROOT = path.resolve(process.cwd());
const W = 1200;
const H = 630;
const palette = {
bg: "#0f172a",
bgAccent: "#1c2433",
orange: "#fdb13c",
teal: "#006672",
gridLine: "rgba(255,255,255,0.04)",
};
async function loadFontBase64() {
// Check for a local font file first (committed to the repo so Docker
// builds work offline).
const localPath = path.join(ROOT, "public/fonts/inter-800.woff");
let buffer;
try {
buffer = await fs.readFile(localPath);
} catch {
// Fall back to Google Fonts.
const url =
"https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuDyYMZtrib2Ai-4.woff";
const res = await fetch(url);
if (!res.ok) throw new Error(`Failed to fetch Inter: ${res.status}`);
buffer = Buffer.from(await res.arrayBuffer());
}
return buffer.toString("base64");
}
const fontB64 = await loadFontBase64();
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
<defs>
<style type="text/css">
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 800;
src: url(data:font/woff;base64,${fontB64}) format('woff');
}
</style>
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="${palette.bg}"/>
<stop offset="100%" stop-color="${palette.bgAccent}"/>
</linearGradient>
<radialGradient id="glow" cx="0.85" cy="0.15" r="0.6">
<stop offset="0%" stop-color="${palette.orange}" stop-opacity="0.35"/>
<stop offset="60%" stop-color="${palette.orange}" stop-opacity="0.05"/>
<stop offset="100%" stop-color="${palette.orange}" stop-opacity="0"/>
</radialGradient>
<pattern id="grid" width="48" height="48" patternUnits="userSpaceOnUse">
<path d="M 48 0 L 0 0 0 48" fill="none" stroke="${palette.gridLine}" stroke-width="1"/>
</pattern>
</defs>
<!-- Background -->
<rect width="${W}" height="${H}" fill="url(#bg)"/>
<rect width="${W}" height="${H}" fill="url(#grid)"/>
<rect width="${W}" height="${H}" fill="url(#glow)"/>
<!-- Brand mark (rounded square on the left) -->
<g transform="translate(80, 215)">
<rect x="0" y="0" width="200" height="200" rx="40" fill="${palette.orange}"/>
<g transform="translate(50, 50)">
<rect x="0" y="0" width="100" height="100" rx="16" fill="#0f172a"/>
<rect x="12" y="12" width="76" height="76" rx="10" fill="${palette.orange}"/>
<path d="M 50 22 L 78 50 L 50 78 L 22 50 Z" fill="#0f172a"/>
<circle cx="50" cy="50" r="10" fill="${palette.orange}"/>
</g>
</g>
<!-- Wordmark + tagline -->
<g transform="translate(320, 0)">
<text x="0" y="220" font-family="Inter, system-ui, sans-serif" font-size="22" font-weight="700" letter-spacing="6" fill="rgba(255,255,255,0.55)">
CONSTRUCTION OPERATIONS
</text>
<text x="0" y="334" font-family="Inter, system-ui, sans-serif" font-size="64" font-weight="800" letter-spacing="-1">
<tspan fill="${palette.orange}">MEGA</tspan><tspan fill="#ffffff">INFRASTRUCTURE</tspan>
</text>
<text x="0" y="400" font-family="Inter, system-ui, sans-serif" font-size="26" font-weight="500" fill="rgba(255,255,255,0.78)">
Site operations dashboard · fuel · orders · workforce · sites
</text>
</g>
<!-- Bottom accent bars -->
<g transform="translate(120, 575)">
<rect width="180" height="6" rx="3" fill="${palette.orange}"/>
<rect x="200" width="180" height="6" rx="3" fill="#a1ae3b"/>
<rect x="400" width="180" height="6" rx="3" fill="#4f9c5b"/>
<rect x="600" width="180" height="6" rx="3" fill="${palette.teal}"/>
</g>
</svg>
`;
const out = path.join(ROOT, "public", "og-image.png");
const buf = Buffer.from(svg);
await sharp(buf, { density: 144 })
.resize({ width: W, height: H })
.png({ compressionLevel: 9 })
.toFile(out);
const stat = await fs.stat(out);
console.log(`Wrote ${out} (${(stat.size / 1024).toFixed(1)} KB, ${W}x${H})`);