Tiesen LogoYuki UI
Components

Open Graph

A component to generate Open Graph images

GitHubComponent Source

Open Graph

Installation

CLI

npx shadcn add https://yuki-ui.vercel.app/r/open-graph.json
npx shadcn add https://yuki-ui.vercel.app/r/open-graph.json
pnpm dlx shadcn add https://yuki-ui.vercel.app/r/open-graph.json
bunx --bun shadcn add https://yuki-ui.vercel.app/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 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

  1. 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.

  2. 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.

app/api/og/route.tsx
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

On this page