Components

Open Graph

A component for setting Open Graph meta tags in your application to improve link previews on social media platforms.

Installation

CLI

npx shadcn add https://ui.tiesen.id.vn/r/open-graph.json

Manual

Copy and paste the following code into your project.

interface OpenGraphProps {  appName?: string  logo?: React.ReactNode  title?: string  description?: string  caption?: string  image?: string  primaryColor?: string  secondaryColor?: string  corner?: { margin?: number; length?: number; strokeWidth?: number }  style?: React.CSSProperties}function OpenGraph({  appName = 'My App',  caption = `Generated by ${appName}`,  primaryColor = '#3f5ec2',  secondaryColor = '#a4a4a4',  ...props}: OpenGraphProps) {  const corner = { margin: 24, length: 52, strokeWidth: 1, ...props.corner }  const s = corner.strokeWidth / 2  const max = corner.length - s  const isImageOnly = !props.title && !props.description  const MAX_TITLE_LENGTH = props.image ? 35 : 60  const MAX_DESCRIPTION_LENGTH = props.image ? 160 : 300  const truncatedTitle = props.title    ? truncateText(props.title, MAX_TITLE_LENGTH)    : ''  const truncatedDescription = props.description    ? truncateText(props.description, MAX_DESCRIPTION_LENGTH)    : ''  return (    <div      style={{        position: 'relative',        width: '1200px',        height: '630px',        padding: '3.5rem',        backgroundColor: '#000000',        display: 'flex',        justifyContent: isImageOnly ? 'center' : 'space-between',        alignItems: 'center',        gap: '2.5rem',        ...props.style,      }}    >      <div        style={{          position: 'absolute',          width: '100%',          height: '100%',          backgroundSize: '48px 48px',          backgroundImage: `            linear-gradient(to right, ${secondaryColor}20 1px, transparent 1px),            linear-gradient(to bottom, ${secondaryColor}20 1px, transparent 1px)          `,          maskImage:            'radial-gradient(circle at center, black 20%, transparent 100%)',          WebkitMaskImage:            'radial-gradient(circle at center, black 20%, transparent 100%)',          pointerEvents: 'none',        }}      />      <Corner        corner={corner}        color={secondaryColor}        position={{ top: corner.margin, left: corner.margin }}        path={`M ${corner.length} ${s} L ${s} ${s} L ${s} ${corner.length}`}      />      <Corner        corner={corner}        color={secondaryColor}        position={{ top: corner.margin, right: corner.margin }}        path={`M 0 ${s} L ${max} ${s} L ${max} ${corner.length}`}      />      <Corner        corner={corner}        color={secondaryColor}        position={{ bottom: corner.margin, left: corner.margin }}        path={`M ${s} 0 L ${s} ${max} L ${corner.length} ${max}`}      />      <Corner        corner={corner}        color={secondaryColor}        position={{ bottom: corner.margin, right: corner.margin }}        path={`M ${max} 0 L ${max} ${max} L 0 ${max}`}      />      <div        style={{          flex: 1,          display: isImageOnly ? 'none' : 'flex',          flexDirection: 'column',          width: '100%',          height: '100%',        }}      >        <div style={{ display: 'flex', alignItems: 'center', gap: '1.25rem' }}>          {props.logo && (            <div              style={{                width: '52px',                height: '52px',                display: 'flex',                alignItems: 'center',                justifyContent: 'center',                border: `1px solid ${primaryColor}80`,                borderRadius: '16px',                overflow: 'hidden',              }}            >              {props.logo}            </div>          )}          <h2            style={{              fontSize: '1.5rem',              fontWeight: 600,              letterSpacing: '1px',              color: primaryColor,              textTransform: 'uppercase',              margin: 0,            }}          >            {appName}          </h2>        </div>        <div          className='line-clamp-2'          style={{            flex: 1,            display: 'flex',            flexDirection: 'column',            alignItems: props.image ? 'flex-start' : 'center',            justifyContent: 'center',            gap: '1.75rem',          }}        >          <h1            style={{              textAlign: props.image ? 'left' : 'center',              fontSize: '3.75rem',              fontWeight: 700,              lineHeight: '1.15',              color: '#ffffff',              margin: 0,            }}          >            {truncatedTitle}          </h1>          <p            style={{              textAlign: props.image ? 'left' : 'center',              fontSize: '1.875rem',              fontWeight: 400,              lineHeight: '1.5',              color: secondaryColor,              margin: 0,            }}          >            {truncatedDescription}          </p>        </div>      </div>      {props.image && (        <div          style={{            flexShrink: 0,            display: 'flex',            width: isImageOnly ? '920px' : '400px',            height: isImageOnly ? '518px' : '400px',            borderRadius: '1rem',            overflow: 'hidden',          }}        >          {/* oxlint-disable-next-line next/no-img-element */}          <img            src={props.image}            alt={props.title}            style={{              objectFit: isImageOnly ? 'contain' : 'cover',              width: '100%',              height: '100%',              borderRadius: '1rem',            }}          />        </div>      )}      <div        style={{          display: 'flex',          position: 'absolute',          bottom: corner.margin - 8,          right: corner.margin + corner.length + 8,        }}      >        <p          style={{            fontSize: '0.875rem',            fontWeight: 400,            color: secondaryColor,            margin: 0,          }}        >          {caption}        </p>      </div>    </div>  )}export { OpenGraph }const Corner = ({  corner,  color,  position,  path,}: {  corner: Required<OpenGraphProps['corner']>  color: string  position: React.CSSProperties  path: string}) => (  <svg    style={{      position: 'absolute',      width: corner?.length,      height: corner?.length,      pointerEvents: 'none',      ...position,    }}    viewBox={`0 0 ${corner?.length} ${corner?.length}`}  >    <path      d={path}      fill='none'      stroke={`${color}40`}      strokeWidth={corner?.strokeWidth}      strokeLinecap='round'      strokeLinejoin='round'    />  </svg>)function truncateText(text: string, maxLength: number) {  if (text.length <= maxLength) return text  return `${text.slice(0, maxLength - 3)}...`}

Features

  • Responsive Layout: Automatically adjusts the layout if an image is missing, centering the text for a balanced look.
  • Image-Only Mode: If no title or description is provided, the component focuses entirely on the provided image.
  • Grid Background: Includes a subtle, masked grid pattern for a modern, technical aesthetic.
  • Dynamic Corners: Features SVG corner accents that frame the content beautifully.
  • Text Optimization: Built-in line-clamping (via -webkit-line-clamp) ensures your titles and descriptions don't overflow the 630px height.

Usage

app/api/og/route.tsx
import { ImageResponse } from 'next/og'

import { OpenGraph } from '@/components/ui/open-graph'
import { appName } from '@/lib/shared'

export const revalidate = false

export async function GET(req: Request, _: RouteContext<'/api/og'>) {
  const url = new URL(req.url)

  const title = url.searchParams.get('title') ?? ''
  const description = url.searchParams.get('description') ?? ''
  let image = url.searchParams.get('image') ?? ''
  if (image && !image.startsWith('http'))
    image = new URL(image, req.url).toString()

  const logoUrl = new URL('/icon-512.png', req.url).toString()

  return new ImageResponse(
    <OpenGraph
      appName={appName}
      title={title}
      description={description}
      image={image}
      logo={<img src={logoUrl} width={56} height={56} />}
      caption={url.hostname}
    />,
    {
      width: 1200,
      height: 630,
    }
  )
}

On this page