Open Graph
A component to generate Open Graph images
Component SourceInstallation
CLI
npx shadcn add https://yuki-ui.vercel.app/r/open-graph.jsonnpx shadcn add https://yuki-ui.vercel.app/r/open-graph.jsonpnpm dlx shadcn add https://yuki-ui.vercel.app/r/open-graph.jsonbunx --bun shadcn add https://yuki-ui.vercel.app/r/open-graph.jsonManual
Copy and paste the following code into your project. app/api/og/route.tsx
/* eslint-disable @next/next/no-img-element */import type { NextRequest } from 'next/server'import { ImageResponse } from 'next/og'export const runtime = 'edge'export async function GET(request: NextRequest) { try { const { searchParams } = new URL(request.url) const appName = 'Yuki UI' const title = searchParams.get('title') ?? '' const description = searchParams.get('description') ?? '' const image = searchParams.get('image') ?? '' const logoUrl = `https://yuki-ui.vercel.app/assets/logo.svg` const theme = searchParams.get('theme') ?? 'dark' const backgroundColor = theme === 'dark' ? '#000000' : '#fafafa' const foregroundColor = theme === 'dark' ? '#ffffff' : '#000000' const primaryColor = theme === 'dark' ? '#dbe6f6' : '#293478' const [geistRegular, geistMedium, geistBold] = await Promise.all([ getFont('Geist-Regular', 400), getFont('Geist-Medium', 500), getFont('Geist-Bold', 700), ]) return new ImageResponse( <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', justifyContent: 'space-between', gap: '32px', width: '100%', height: '100%', padding: '32px 40px', backgroundColor, }} > <div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '48px', height: '48px', borderRadius: '8px', backgroundColor: primaryColor, }} > <img src={logoUrl} alt='Logo' style={{ width: '80%', height: '80%', margin: 0, objectFit: 'contain', filter: theme === 'dark' ? 'none' : 'invert(1)', }} /> </div> <h1 style={{ fontFamily: 'Geist-Medium, sans-serif', fontSize: '28px', fontWeight: '500', color: foregroundColor, }} > {appName} </h1> </div> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '32px', flex: 1, width: '100%', }} > <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', width: image ? '65%' : '100%', height: '100%', }} > <h2 style={{ fontFamily: 'Geist-Bold, sans-serif', fontSize: '48px', lineHeight: '1.1', fontWeight: '700', color: foregroundColor, margin: '0 0 24px 0', }} > {title} </h2> <p style={{ fontFamily: 'Geist-Regular, sans-serif', fontSize: '24px', lineHeight: '1.2', fontWeight: '400', color: foregroundColor, height: '100%', overflow: 'hidden', margin: 0, opacity: 0.75, }} > {description} </p> </div> {image && ( <img src={image} alt={title} style={{ flex: 1, border: `0.5px solid ${foregroundColor}`, borderRadius: '16px', aspectRatio: '1 / 1', objectFit: 'cover', }} /> )} </div> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%', }} > <div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: '16px', }} > <hr style={{ width: '60px', height: '4px', borderRadius: '2px', background: `linear-gradient(90deg, ${primaryColor}, ${backgroundColor})`, }} /> <p style={{ fontFamily: 'Geist-Medium, sans-serif', fontSize: '16px', fontWeight: '500', color: foregroundColor, margin: 0, opacity: 0.75, }} > {new URL(request.url).hostname} </p> </div> </div> </div>, { width: 1200, height: 630, // @ts-expect-error The 'weight' property in the returned font object is a number, // but 'FontOptions' expects a specific type. This is intentional for font loading. fonts: [geistRegular, geistMedium, geistBold], }, ) } catch (e: unknown) { console.error(e) return new Response(`Failed to generate the image`, { status: 500 }) }}async function getFont(font: string, weight = 400) { const response = await fetch( new URL(`../../../public/assets/fonts/${font}.ttf`, import.meta.url), ) return { name: font, data: await response.arrayBuffer(), style: 'normal' as const, weight, }}Usage
This component uses next/og to dynamically generate Open Graph images at the /api/og endpoint. You can customize the title and description by passing search params to the URL.
/api/og?title=Your%20Title&description=Your%20Description
Change font
-
Download font to public folder
Place your custom font files (Regular.ttf, Medium.ttf, Bold.ttf) in the
public/assets/fonts/directory of your Next.js project. -
Load font in your route handler
In your API route, load all fonts using Promise.all for better performance, then configure them in the ImageResponse options. Each font needs a unique name, the font data, style, and weight properties.
export async function GET(request: NextRequest) {
const [regular, medium, bold] = await Promise.all([
getFont('Regular', 400),
getFont('Medium', 500),
getFont('Bold', 700),
])
// ... rest of the code
return new ImageResponse(
(
// Component
),
{
{
width: 1200,
height: 630,
fonts: [
{
name: 'MyFontRegular',
data: regular,
style: 'normal',
weight: 400,
},
{
name: 'MyFontMedium',
data: medium,
style: 'normal',
weight: 500,
},
{
name: 'MyFontBold',
data: bold,
style: 'normal',
weight: 700,
},
],
},
}
)
}See the complete implementation with custom font loading in this example: here