Open Graph
A component to generate Open Graph images
Component SourceInstallation
CLI
npx shadcn add https://ui.tiesen.id.vn/r/open-graph.json
npx shadcn add https://ui.tiesen.id.vn/r/open-graph.json
pnpm dlx shadcn add https://ui.tiesen.id.vn/r/open-graph.json
bunx --bun shadcn add https://ui.tiesen.id.vn/r/open-graph.json
Manual
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 function GET(request: NextRequest) { try { const { searchParams } = new URL(request.url) const appName = 'Yuki' const title = searchParams.get('title') ?? '' const description = searchParams.get('description') ?? '' const logoUrl = `https://tiesen.id.vn/assets/images/logo.svg` const truncatedTitle = truncateText(title, 80) const titleFontSize = getTitleFontSize(truncatedTitle.length) return new ImageResponse( ( <div style={{ height: '100%', width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'flex-start', justifyContent: 'space-between', gap: '32px', backgroundColor: '#000', backgroundImage: 'radial-gradient(circle at 25px 25px, #333 2%, transparent 0%), radial-gradient(circle at 75px 75px, #333 2%, transparent 0%)', backgroundSize: '100px 100px', padding: '64px', }} > <div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}> <div style={{ width: '48', height: '48', backgroundColor: '#fff', borderRadius: '8px', display: 'flex', alignItems: 'center', justifyContent: 'center', }} > <img src={logoUrl} alt='Logo' style={{ width: '80%', height: '80%', }} /> </div> <div style={{ fontSize: '28px', fontWeight: '500', color: '#fff', }} > {appName} </div> </div> <div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: '24px', }} > <h1 style={{ fontSize: `${titleFontSize}px`, fontWeight: '700', color: '#fff', lineHeight: '1.1', margin: '0', background: 'linear-gradient(135deg, #fff 0%, #888 100%)', backgroundClip: 'text', WebkitBackgroundClip: 'text', }} > {truncatedTitle} </h1> <p style={{ fontSize: description.length > 100 ? '24px' : '32px', color: '#888', lineHeight: '1.5', margin: '0', fontWeight: '400', maxWidth: '800px', overflow: 'hidden', display: '-webkit-box', WebkitLineClamp: 3, WebkitBoxOrient: 'vertical', }} > {description} </p> </div> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%', marginTop: '40px', }} > <div style={{ display: 'flex', alignItems: 'center', gap: '16px', }} > <div style={{ width: '60px', height: '4px', background: 'linear-gradient(90deg, #0070f3, #00d9ff)', borderRadius: '2px', }} /> <div style={{ fontSize: '16px', color: '#666', fontWeight: '500', }} > {new URL(request.url).hostname} </div> </div> <div style={{ display: 'flex', alignItems: 'center', gap: '12px', }} > <div style={{ width: '12px', height: '12px', backgroundColor: '#00d9ff', borderRadius: '50%', opacity: 0.8, }} /> <div style={{ width: '8px', height: '8px', backgroundColor: '#0070f3', borderRadius: '50%', opacity: 0.6, }} /> <div style={{ width: '6px', height: '6px', backgroundColor: '#7c3aed', borderRadius: '50%', opacity: 0.4, }} /> </div> </div> </div> ), { width: 1200, height: 630 }, ) } catch (e: unknown) { console.error(e) return new Response(`Failed to generate the image`, { status: 500, }) }}function truncateText(text: string, maxLength: number): string { if (text.length <= maxLength) return text const truncated = text.substring(0, maxLength) const lastSpace = truncated.lastIndexOf(' ') if (lastSpace > maxLength * 0.8) { return truncated.substring(0, lastSpace) + '...' } return truncated + '...'}function getTitleFontSize(titleLength: number): number { if (titleLength <= 20) return 72 if (titleLength <= 40) return 64 if (titleLength <= 60) return 56 return 48}
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. -
Create function to load font
Create helper functions to load your font files. These functions fetch the font files and return them as ArrayBuffer objects that can be used by the ImageResponse API.
async function getRegularFont() {
const response = await fetch(
new URL('../../../public/assets/fonts/Regular.ttf', import.meta.url),
)
return response.arrayBuffer()
}
async function getMediumFont() {
const response = await fetch(
new URL('../../../public/assets/fonts/Medium.ttf', import.meta.url),
)
return response.arrayBuffer()
}
async function getBoldFont() {
const response = await fetch(
new URL('../../../public/assets/fonts/Bold.ttf', import.meta.url),
)
return response.arrayBuffer()
}
-
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 [fontRegular, fontMedium, fontBold] = await Promise.all([
getRegularFont(),
getMediumFont(),
getBoldFont(),
])
// ... rest of the code
return new ImageResponse(
(
// Component
),
{
{
width: 1200,
height: 630,
fonts: [
{
name: 'MyFontRegular',
data: fontRegular,
style: 'normal',
weight: 400,
},
{
name: 'MyFontMedium',
data: fontMedium,
style: 'normal',
weight: 500,
},
{
name: 'MyFontBold',
data: fontBold,
style: 'normal',
weight: 700,
},
],
},
}
)
}
See the complete implementation with custom font loading in this example: here