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/ogCreate an API route that generates images dynamically:
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:
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:
import { ImageResponse } from '@vercel/og'import { readFile } from 'fs/promises'import { join } from 'path'
// Load font once at module levelconst 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:
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:
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:
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:
http://localhost:8081/api/og?title=Test+TitleUse the OpenGraph debugger or social platform preview tools to verify how images appear when shared.
Edit this page on GitHub.