diff --git a/app/(tools)/base64/opengraph-image.tsx b/app/(tools)/base64/opengraph-image.tsx
new file mode 100644
index 0000000..7eb1de4
--- /dev/null
+++ b/app/(tools)/base64/opengraph-image.tsx
@@ -0,0 +1,12 @@
+import { getTool } from "@/lib/tools";
+import { renderToolOgImage, OG_SIZE, OG_CONTENT_TYPE } from "@/lib/og";
+
+const tool = getTool("base64")!;
+
+export const alt = tool.ogTitle;
+export const size = OG_SIZE;
+export const contentType = OG_CONTENT_TYPE;
+
+export default function Image() {
+ return renderToolOgImage(tool);
+}
diff --git a/app/(tools)/invoice-generator/opengraph-image.tsx b/app/(tools)/invoice-generator/opengraph-image.tsx
new file mode 100644
index 0000000..9e1b53e
--- /dev/null
+++ b/app/(tools)/invoice-generator/opengraph-image.tsx
@@ -0,0 +1,12 @@
+import { getTool } from "@/lib/tools";
+import { renderToolOgImage, OG_SIZE, OG_CONTENT_TYPE } from "@/lib/og";
+
+const tool = getTool("invoice-generator")!;
+
+export const alt = tool.ogTitle;
+export const size = OG_SIZE;
+export const contentType = OG_CONTENT_TYPE;
+
+export default function Image() {
+ return renderToolOgImage(tool);
+}
diff --git a/app/(tools)/json-formatter/opengraph-image.tsx b/app/(tools)/json-formatter/opengraph-image.tsx
new file mode 100644
index 0000000..fecc74e
--- /dev/null
+++ b/app/(tools)/json-formatter/opengraph-image.tsx
@@ -0,0 +1,12 @@
+import { getTool } from "@/lib/tools";
+import { renderToolOgImage, OG_SIZE, OG_CONTENT_TYPE } from "@/lib/og";
+
+const tool = getTool("json-formatter")!;
+
+export const alt = tool.ogTitle;
+export const size = OG_SIZE;
+export const contentType = OG_CONTENT_TYPE;
+
+export default function Image() {
+ return renderToolOgImage(tool);
+}
diff --git a/app/(tools)/jwt-decoder/opengraph-image.tsx b/app/(tools)/jwt-decoder/opengraph-image.tsx
new file mode 100644
index 0000000..790bede
--- /dev/null
+++ b/app/(tools)/jwt-decoder/opengraph-image.tsx
@@ -0,0 +1,12 @@
+import { getTool } from "@/lib/tools";
+import { renderToolOgImage, OG_SIZE, OG_CONTENT_TYPE } from "@/lib/og";
+
+const tool = getTool("jwt-decoder")!;
+
+export const alt = tool.ogTitle;
+export const size = OG_SIZE;
+export const contentType = OG_CONTENT_TYPE;
+
+export default function Image() {
+ return renderToolOgImage(tool);
+}
diff --git a/app/opengraph-image.tsx b/app/opengraph-image.tsx
index bf49007..f57cb25 100644
--- a/app/opengraph-image.tsx
+++ b/app/opengraph-image.tsx
@@ -1,61 +1,9 @@
-import { ImageResponse } from "next/og";
+import { renderHomeOgImage, OG_SIZE, OG_CONTENT_TYPE } from "@/lib/og";
export const alt = "CODERCOPS Tools — fast, privacy-first developer utilities";
-export const size = { width: 1200, height: 630 };
-export const contentType = "image/png";
+export const size = OG_SIZE;
+export const contentType = OG_CONTENT_TYPE;
export default function OpengraphImage() {
- return new ImageResponse(
- (
-
-
-
- {"{}"}
-
-
CODERCOPS · TOOLS
-
-
-
-
- Sharp, focused tools for shipping software
-
-
- JSON formatter · JWT decoder · Base64 · Invoice generator. Runs entirely in your browser.
-
-
-
-
- {["#5AB5FF", "#9E7BFF", "#FFB547", "#A6F36B"].map((c) => (
-
- ))}
-
-
- ),
- { ...size }
- );
+ return renderHomeOgImage();
}
diff --git a/lib/og.tsx b/lib/og.tsx
new file mode 100644
index 0000000..4a3a2bf
--- /dev/null
+++ b/lib/og.tsx
@@ -0,0 +1,217 @@
+import { readFileSync } from "fs";
+import { join } from "path";
+import { ImageResponse } from "next/og";
+import type { Tool } from "./tools";
+
+// Shared social-card renderer. Every page's opengraph-image route derives its
+// card from here, so the brand, typography, and layout are identical and can
+// never drift between pages. Copy and accent come from the tool registry.
+
+export const OG_SIZE = { width: 1200, height: 630 };
+export const OG_CONTENT_TYPE = "image/png";
+
+// The subset Noto Sans bundled for invoice PDFs (Latin + currency) is reused for
+// every card, so text is real, hinted glyphs rather than the satori fallback.
+// These routes are prerendered at build time, so reading from the working
+// directory is safe (and avoids the edge-only fetch(new URL(import.meta.url))).
+let fonts: { name: string; data: Buffer; weight: 400 | 700; style: "normal" }[] | null = null;
+function ogFonts() {
+ if (!fonts) {
+ const dir = join(process.cwd(), "public", "fonts");
+ fonts = [
+ { name: "Noto Sans", data: readFileSync(join(dir, "NotoSans-Regular.ttf")), weight: 400, style: "normal" },
+ { name: "Noto Sans", data: readFileSync(join(dir, "NotoSans-Bold.ttf")), weight: 700, style: "normal" },
+ ];
+ }
+ return fonts;
+}
+
+const BG = "#0A0B0F";
+const FG = "#E8E6E1";
+const MUTED = "#8A93A8";
+const HAIRLINE = "rgba(255,255,255,0.12)";
+
+// The subset font has no arrow glyph, so keep card text to Latin + punctuation.
+function safe(text: string): string {
+ return text.replace(/\s*(?:→|->)\s*/g, " to ");
+}
+
+interface OgCardProps {
+ badge: string;
+ title: string;
+ description: string;
+ chips: string[];
+ url: string;
+ accent: string;
+ /** Four bars in the footer. Tools pass one accent; the home card passes four. */
+ accentBars: string[];
+}
+
+function OgCard({ badge, title, description, chips, url, accent, accentBars }: OgCardProps) {
+ return (
+
+ {/* Faint grid texture */}
+
+ {/* Accent glow, top-right */}
+
+
+ {/* Header */}
+
+
+
+ {"{}"}
+
+
+ CODERCOPS · TOOLS
+
+
+
+ {badge}
+
+
+
+ {/* Middle */}
+
+
+
+ {title}
+
+
+ {description}
+
+
+ {chips.map((c) => (
+
+ {c}
+
+ ))}
+
+
+
+ {/* Footer */}
+
+
{url}
+
+ {accentBars.map((c, i) => (
+
+ ))}
+
+
+
+ );
+}
+
+function render(props: OgCardProps) {
+ return new ImageResponse(, { ...OG_SIZE, fonts: ogFonts() });
+}
+
+/** Social card for a tool page, driven entirely by its registry entry. */
+export function renderToolOgImage(tool: Tool) {
+ const a = tool.accentHex;
+ return render({
+ badge: tool.tagline.toUpperCase(),
+ title: tool.title,
+ description: safe(tool.ogDescription),
+ chips: tool.features.map(safe),
+ url: `tools.codercops.com${tool.path}`,
+ accent: a,
+ accentBars: [`${a}ff`, `${a}b3`, `${a}73`, `${a}40`],
+ });
+}
+
+/** Social card for the homepage. */
+export function renderHomeOgImage() {
+ return render({
+ badge: "OPEN SOURCE",
+ title: "Sharp, focused tools for shipping software",
+ description:
+ "Free, privacy-first developer utilities. JSON formatter, JWT decoder, Base64, invoice generator. Everything runs in your browser.",
+ chips: ["No login", "No uploads", "No tracking", "Open source", "MIT"],
+ url: "tools.codercops.com",
+ accent: "#00E5C7",
+ accentBars: ["#5AB5FF", "#9E7BFF", "#FFB547", "#A6F36B"],
+ });
+}