diff --git a/apps/web/app/api/og/[[...slug]]/route.tsx b/apps/web/app/api/og/[[...slug]]/route.tsx index b7d8701..8a36f60 100644 --- a/apps/web/app/api/og/[[...slug]]/route.tsx +++ b/apps/web/app/api/og/[[...slug]]/route.tsx @@ -1,89 +1,42 @@ -import { ImageResponse } from 'next/og'; -import { blogPosts } from '../../../../src/data/blogPosts'; +import { ImageResponse } from "next/og"; +import { blogPosts } from "../../../../src/data/blogPosts"; +import { OGImageTemplate } from "../../../../src/components/OGImageTemplate"; +import { getOgFonts, OG_IMAGE_SIZE } from "../../../../src/lib/og-helper"; -export const runtime = 'edge'; +export const runtime = "nodejs"; export async function GET( request: Request, - { params }: { params: Promise<{ slug?: string[] }> } + { params }: { params: Promise<{ slug?: string[] }> }, ) { const { slug: slugArray } = await params; - const slug = slugArray?.[0] || 'home'; + const slug = slugArray?.[0] || "home"; let title: string; let description: string; + let label: string | undefined; - if (slug === 'home') { - title = 'Marc Mintel'; - description = 'Technical problem solver\'s blog - practical insights and learning notes'; + if (slug === "home") { + title = "Marc Mintel"; + description = + "Technical problem solver's blog - practical insights and learning notes"; + label = "Engineering"; } else { - const post = blogPosts.find(p => p.slug === slug); - title = post?.title || 'Marc Mintel'; - description = (post?.description || 'Technical problem solver\'s blog - practical insights and learning notes').slice(0, 100); + const post = blogPosts.find((p) => p.slug === slug); + title = post?.title || "Marc Mintel"; + description = + post?.description || + "Technical problem solver's blog - practical insights and learning notes"; + label = post ? "Blog Post" : "Engineering"; } + const fonts = await getOgFonts(); + return new ImageResponse( - ( -
-
-
- {title} -
-
- {description} -
-
- mintel.me -
-
- ), + , { - width: 1200, - height: 630, - } + ...OG_IMAGE_SIZE, + fonts: fonts as any, + }, ); } diff --git a/apps/web/public/fonts/Inter-Bold.woff b/apps/web/public/fonts/Inter-Bold.woff new file mode 100644 index 0000000..161b01e Binary files /dev/null and b/apps/web/public/fonts/Inter-Bold.woff differ diff --git a/apps/web/public/fonts/Inter-Regular.woff b/apps/web/public/fonts/Inter-Regular.woff new file mode 100644 index 0000000..2f21ed4 Binary files /dev/null and b/apps/web/public/fonts/Inter-Regular.woff differ diff --git a/apps/web/src/components/OGImageTemplate.tsx b/apps/web/src/components/OGImageTemplate.tsx new file mode 100644 index 0000000..4ccfc0d --- /dev/null +++ b/apps/web/src/components/OGImageTemplate.tsx @@ -0,0 +1,154 @@ +import React from "react"; + +interface OGImageTemplateProps { + title: string; + description?: string; + label?: string; +} + +export function OGImageTemplate({ + title, + description, + label, +}: OGImageTemplateProps) { + const accentBlue = "#3b82f6"; + const slateDark = "#0f172a"; + const slateText = "#1e293b"; + const slateLight = "#64748b"; + + const containerStyle: React.CSSProperties = { + height: "100%", + width: "100%", + display: "flex", + flexDirection: "column", + alignItems: "flex-start", + justifyContent: "center", + backgroundColor: "#ffffff", + padding: "80px", + position: "relative", + fontFamily: "Inter", + }; + + return ( +
+ {/* Background Technographic Accent */} +
+ +
+ {/* Label / Category */} + {label && ( +
+ {label} +
+ )} + + {/* Title */} +
40 ? "64px" : "82px", + fontWeight: 700, + color: slateDark, + lineHeight: "1.1", + maxWidth: "950px", + marginBottom: "32px", + display: "flex", + letterSpacing: "-0.025em", + }} + > + {title} +
+ + {/* Description */} + {description && ( +
+ {description.length > 160 + ? description.substring(0, 157) + "..." + : description} +
+ )} +
+ + {/* Brand Footer */} +
+
+
+ mintel.me +
+
+ + {/* Blue Brand Strip */} +
+
+ ); +} diff --git a/apps/web/src/lib/og-helper.tsx b/apps/web/src/lib/og-helper.tsx new file mode 100644 index 0000000..284ddc9 --- /dev/null +++ b/apps/web/src/lib/og-helper.tsx @@ -0,0 +1,54 @@ +import { readFileSync } from "fs"; +import { join } from "path"; + +/** + * Loads the Inter fonts for use in Satori (Next.js OG Image generation). + * Since we are using runtime = 'nodejs', we can read them from the filesystem. + */ +export async function getOgFonts() { + const boldFontPath = join( + process.cwd(), + "apps/web/public/fonts/Inter-Bold.woff", + ); + const regularFontPath = join( + process.cwd(), + "apps/web/public/fonts/Inter-Regular.woff", + ); + + try { + console.log( + `[OG] Loading fonts: bold=${boldFontPath}, regular=${regularFontPath}`, + ); + const boldFont = readFileSync(boldFontPath); + const regularFont = readFileSync(regularFontPath); + console.log( + `[OG] Fonts loaded successfully (${boldFont.length} and ${regularFont.length} bytes)`, + ); + + return [ + { + name: "Inter", + data: boldFont, + weight: 700 as const, + style: "normal" as const, + }, + { + name: "Inter", + data: regularFont, + weight: 400 as const, + style: "normal" as const, + }, + ]; + } catch (error) { + console.error(`[OG] Failed to load fonts from ${process.cwd()}:`, error); + return []; + } +} + +/** + * Common configuration for OG images + */ +export const OG_IMAGE_SIZE = { + width: 1200, + height: 630, +};