123 lines
4.5 KiB
JavaScript
123 lines
4.5 KiB
JavaScript
|
|
"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})`);
|