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