feat(og): adapt premium industrial style for OG images
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🏗️ Build (push) Successful in 5m20s
Build & Deploy / 🧪 QA (push) Successful in 5m38s
Build & Deploy / 🚀 Deploy (push) Successful in 19s
Build & Deploy / 🩺 Health Check (push) Failing after 11s
Build & Deploy / 🔔 Notify (push) Successful in 2s
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🏗️ Build (push) Successful in 5m20s
Build & Deploy / 🧪 QA (push) Successful in 5m38s
Build & Deploy / 🚀 Deploy (push) Successful in 19s
Build & Deploy / 🩺 Health Check (push) Failing after 11s
Build & Deploy / 🔔 Notify (push) Successful in 2s
This commit is contained in:
@@ -1,89 +1,42 @@
|
|||||||
import { ImageResponse } from 'next/og';
|
import { ImageResponse } from "next/og";
|
||||||
import { blogPosts } from '../../../../src/data/blogPosts';
|
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(
|
export async function GET(
|
||||||
request: Request,
|
request: Request,
|
||||||
{ params }: { params: Promise<{ slug?: string[] }> }
|
{ params }: { params: Promise<{ slug?: string[] }> },
|
||||||
) {
|
) {
|
||||||
const { slug: slugArray } = await params;
|
const { slug: slugArray } = await params;
|
||||||
const slug = slugArray?.[0] || 'home';
|
const slug = slugArray?.[0] || "home";
|
||||||
|
|
||||||
let title: string;
|
let title: string;
|
||||||
let description: string;
|
let description: string;
|
||||||
|
let label: string | undefined;
|
||||||
|
|
||||||
if (slug === 'home') {
|
if (slug === "home") {
|
||||||
title = 'Marc Mintel';
|
title = "Marc Mintel";
|
||||||
description = 'Technical problem solver\'s blog - practical insights and learning notes';
|
description =
|
||||||
|
"Technical problem solver's blog - practical insights and learning notes";
|
||||||
|
label = "Engineering";
|
||||||
} else {
|
} else {
|
||||||
const post = blogPosts.find(p => p.slug === slug);
|
const post = blogPosts.find((p) => p.slug === slug);
|
||||||
title = post?.title || 'Marc Mintel';
|
title = post?.title || "Marc Mintel";
|
||||||
description = (post?.description || 'Technical problem solver\'s blog - practical insights and learning notes').slice(0, 100);
|
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(
|
return new ImageResponse(
|
||||||
(
|
<OGImageTemplate title={title} description={description} label={label} />,
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
height: '100%',
|
|
||||||
width: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
justifyContent: 'center',
|
|
||||||
backgroundColor: '#fff',
|
|
||||||
padding: '60px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: '60px',
|
|
||||||
right: '60px',
|
|
||||||
width: '120px',
|
|
||||||
height: '4px',
|
|
||||||
backgroundColor: '#3b82f6',
|
|
||||||
borderRadius: '2px',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontSize: '48px',
|
|
||||||
fontWeight: 700,
|
|
||||||
color: '#1e293b',
|
|
||||||
marginBottom: '20px',
|
|
||||||
fontFamily: 'sans-serif',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontSize: '24px',
|
|
||||||
fontWeight: 400,
|
|
||||||
color: '#64748b',
|
|
||||||
marginBottom: 'auto',
|
|
||||||
fontFamily: 'sans-serif',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{description}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontSize: '18px',
|
|
||||||
fontWeight: 500,
|
|
||||||
color: '#94a3b8',
|
|
||||||
fontFamily: 'sans-serif',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
mintel.me
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
{
|
{
|
||||||
width: 1200,
|
...OG_IMAGE_SIZE,
|
||||||
height: 630,
|
fonts: fonts as any,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
apps/web/public/fonts/Inter-Bold.woff
Normal file
BIN
apps/web/public/fonts/Inter-Bold.woff
Normal file
Binary file not shown.
BIN
apps/web/public/fonts/Inter-Regular.woff
Normal file
BIN
apps/web/public/fonts/Inter-Regular.woff
Normal file
Binary file not shown.
154
apps/web/src/components/OGImageTemplate.tsx
Normal file
154
apps/web/src/components/OGImageTemplate.tsx
Normal file
@@ -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 (
|
||||||
|
<div style={containerStyle}>
|
||||||
|
{/* Background Technographic Accent */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "-100px",
|
||||||
|
right: "-100px",
|
||||||
|
width: "500px",
|
||||||
|
height: "500px",
|
||||||
|
borderRadius: "250px",
|
||||||
|
backgroundColor: "#f1f5f9",
|
||||||
|
display: "flex",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
position: "relative",
|
||||||
|
zIndex: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Label / Category */}
|
||||||
|
{label && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "20px",
|
||||||
|
fontWeight: 700,
|
||||||
|
color: accentBlue,
|
||||||
|
textTransform: "uppercase",
|
||||||
|
letterSpacing: "0.2em",
|
||||||
|
marginBottom: "24px",
|
||||||
|
display: "flex",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: title.length > 40 ? "64px" : "82px",
|
||||||
|
fontWeight: 700,
|
||||||
|
color: slateDark,
|
||||||
|
lineHeight: "1.1",
|
||||||
|
maxWidth: "950px",
|
||||||
|
marginBottom: "32px",
|
||||||
|
display: "flex",
|
||||||
|
letterSpacing: "-0.025em",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
{description && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "30px",
|
||||||
|
color: slateLight,
|
||||||
|
maxWidth: "850px",
|
||||||
|
lineHeight: "1.4",
|
||||||
|
display: "flex",
|
||||||
|
fontWeight: 400,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{description.length > 160
|
||||||
|
? description.substring(0, 157) + "..."
|
||||||
|
: description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Brand Footer */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
bottom: "80px",
|
||||||
|
left: "80px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "60px",
|
||||||
|
height: "4px",
|
||||||
|
backgroundColor: slateDark,
|
||||||
|
borderRadius: "2px",
|
||||||
|
marginRight: "20px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "22px",
|
||||||
|
fontWeight: 700,
|
||||||
|
color: slateDark,
|
||||||
|
textTransform: "lowercase",
|
||||||
|
letterSpacing: "0.05em",
|
||||||
|
display: "flex",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
mintel.me
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Blue Brand Strip */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
width: "12px",
|
||||||
|
height: "100%",
|
||||||
|
backgroundColor: accentBlue,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
54
apps/web/src/lib/og-helper.tsx
Normal file
54
apps/web/src/lib/og-helper.tsx
Normal file
@@ -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,
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user