OpenGraph images are the preview images that appear when you share a link on social media. One supports dynamic OG image generation using API routes and the @vercel/og library.
Install the @vercel/og package:
npm install @vercel/og
Create an API route that generates images dynamically:
app/api/og+api.tsx
import { ImageResponse } from '@vercel/og'
export const GET = async (request: Request) => {
const url = new URL(request.url)
const title = url.searchParams.get('title') || 'One Framework'
return new ImageResponse(
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
height: '100%',
backgroundColor: '#000',
color: '#fff',
fontSize: 60,
fontWeight: 700,
}}
>
{title}
</div>,
{
width: 1200,
height: 630,
}
)
}
This creates an endpoint at /api/og that generates PNG images on the fly.
Reference the dynamic image in your page’s meta tags:
app/blog/[slug].tsx
import { Head, useParams } from 'one'
export default function BlogPost() {
const { slug } = useParams()
const title = 'My Blog Post'
const ogUrl = `/api/og?title=${encodeURIComponent(title)}`
return (
<>
<Head>
<meta property="og:image" content={ogUrl} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content={ogUrl} />
</Head>
{/* page content */}
</>
)
}
Load custom fonts for better typography:
app/api/og+api.tsx
import { ImageResponse } from '@vercel/og'
import { readFile } from 'fs/promises'
import { join } from 'path'
// Load font once at module level
const interBold = readFile(join(process.cwd(), 'public/fonts/Inter-Bold.ttf'))
export const GET = async (request: Request) => {
const url = new URL(request.url)
const title = url.searchParams.get('title') || 'One Framework'
return new ImageResponse(
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
height: '100%',
backgroundColor: '#000',
color: '#fff',
fontFamily: 'Inter',
fontSize: 60,
}}
>
{title}
</div>,
{
width: 1200,
height: 630,
fonts: [
{
name: 'Inter',
data: await interBold,
weight: 700,
},
],
}
)
}
Include images in your OG graphics:
app/api/og+api.tsx
import { ImageResponse } from '@vercel/og'
import { readFile } from 'fs/promises'
import { join } from 'path'
export const GET = async (request: Request) => {
const url = new URL(request.url)
const title = url.searchParams.get('title') || 'One Framework'
// Read local image as base64
const bgImage = await readFile(join(process.cwd(), 'public/og-background.png'))
const bgBase64 = `data:image/png;base64,${bgImage.toString('base64')}`
return new ImageResponse(
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
height: '100%',
backgroundImage: `url(${bgBase64})`,
backgroundSize: 'cover',
}}
>
<div
style={{
fontSize: 60,
fontWeight: 700,
color: '#fff',
textShadow: '0 2px 10px rgba(0,0,0,0.5)',
}}
>
{title}
</div>
</div>,
{ width: 1200, height: 630 }
)
}
Create a utility to generate OG URLs consistently:
features/og.ts
import { getURL } from 'one'
type OgParams = {
title: string
type?: 'blog' | 'docs' | 'default'
author?: string
section?: string
}
export function ogUrl(params: OgParams): string {
const base = getURL()
const search = new URLSearchParams()
Object.entries(params).forEach(([key, value]) => {
if (value) search.set(key, value)
})
return `${base}/api/og?${search.toString()}`
}
Use it in pages:
import { ogUrl } from '~/features/og'
const imageUrl = ogUrl({
title: 'My Blog Post',
type: 'blog',
author: 'Jane Doe',
})
Add cache headers for better performance:
app/api/og+api.tsx
import { ImageResponse } from '@vercel/og'
import { setResponseHeaders } from 'one'
export const GET = async (request: Request) => {
// Cache for 1 day on CDN, 1 week stale-while-revalidate
await setResponseHeaders((headers) => {
headers.set('Cache-Control', 'public, s-maxage=86400, stale-while-revalidate=604800')
})
const url = new URL(request.url)
const title = url.searchParams.get('title') || 'One Framework'
return new ImageResponse(
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
height: '100%',
backgroundColor: '#000',
color: '#fff',
fontSize: 60,
}}
>
{title}
</div>,
{ width: 1200, height: 630 }
)
}
Test your OG images directly in the browser:
Terminal
http://localhost:8081/api/og?title=Test+Title
Use the OpenGraph debugger or social platform preview tools to verify how images appear when shared.
Edit this page on GitHub.