Add Open Graph image + social link preview metadata
When someone shares a link to the dashboard in WhatsApp, Facebook,
LinkedIn, Discord, etc., the platform now shows a custom preview
instead of a plain blank card.
- public/og-image.png 1200x630 PNG with the brand mark, the
MEGA INFRASTRUCTURE wordmark, and a
tagline over a dark gradient with the
brand accent bars.
- public/fonts/ Inter ExtraBold (woff) used by the OG image
generator so the rendered text is identical
on every machine.
- scripts/build-og-image.mjs
Generates the image from an SVG template
using sharp. The font is loaded from the
local file (or Google Fonts CDN as fallback)
and embedded as base64 inside the SVG so
the renderer doesn't depend on system
fonts.
- src/app/layout.tsx Adds the openGraph + twitter metadata
pointing at /og-image.png so the preview
appears in link-unfurl contexts.
- package.json 'npm run og-image' runs the generator;
'npm run build' chains it via prebuild so
the image is regenerated on every CI build.
This commit is contained in:
parent
3506640e4a
commit
4b03125002
5 changed files with 159 additions and 1 deletions
|
|
@ -4,7 +4,8 @@
|
|||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -H 0.0.0.0 -p 3000 --webpack",
|
||||
"build": "next build --webpack",
|
||||
"build": "npm run og-image && next build --webpack",
|
||||
"og-image": "node scripts/build-og-image.mjs",
|
||||
"start": "next start -H 0.0.0.0 -p 3000",
|
||||
"lint": "next lint",
|
||||
"typecheck": "tsc --noEmit",
|
||||
|
|
|
|||
BIN
public/fonts/inter-800.woff
Normal file
BIN
public/fonts/inter-800.woff
Normal file
Binary file not shown.
BIN
public/og-image.png
Normal file
BIN
public/og-image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 111 KiB |
122
scripts/build-og-image.mjs
Normal file
122
scripts/build-og-image.mjs
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
"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})`);
|
||||
|
|
@ -9,6 +9,41 @@ export const metadata: Metadata = {
|
|||
description:
|
||||
"Construction site operations dashboard — fuel tracking, order management, and workforce hours for MEGA INFRASTRUCTURE.",
|
||||
applicationName: "MEGA INFRASTRUCTURE",
|
||||
authors: [{ name: "MEGA INFRASTRUCTURE" }],
|
||||
generator: "Next.js",
|
||||
keywords: [
|
||||
"construction",
|
||||
"operations",
|
||||
"fuel tracking",
|
||||
"workforce",
|
||||
"orders",
|
||||
"site management",
|
||||
],
|
||||
// Open Graph — used by WhatsApp, Facebook, LinkedIn, Discord, etc.
|
||||
openGraph: {
|
||||
type: "website",
|
||||
siteName: "MEGA INFRASTRUCTURE",
|
||||
title: "MEGA INFRASTRUCTURE · Operations",
|
||||
description:
|
||||
"Site operations dashboard — fuel tracking, order management, and workforce hours.",
|
||||
images: [
|
||||
{
|
||||
url: "/og-image.png",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "MEGA INFRASTRUCTURE — Site operations dashboard",
|
||||
},
|
||||
],
|
||||
locale: "en_ZA",
|
||||
},
|
||||
// Twitter / X card
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "MEGA INFRASTRUCTURE · Operations",
|
||||
description:
|
||||
"Site operations dashboard — fuel tracking, order management, and workforce hours.",
|
||||
images: ["/og-image.png"],
|
||||
},
|
||||
appleWebApp: {
|
||||
capable: true,
|
||||
title: "MEGA OPS",
|
||||
|
|
|
|||
Loading…
Reference in a new issue