Tiesen Logo
Components

Open Graph

A component to generate Open Graph images

GitHubComponent Source

Open Graph

Installation

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

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

app/api/og/route.tsx
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()
}
  1. 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 [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