Components
Open Graph
A component for setting Open Graph meta tags in your application to improve link previews on social media platforms.
import Image from 'next/image'export default function OpenGraphDemo() { const title = 'Open Graph Demo' const description = 'A simple example demonstrating the Open Graph component.' const dsada = [ { name: 'Default', title, description, }, { name: 'With Image', title, description, image: 'https://1.gravatar.com/avatar/48b8ec4ce6c85e06c11bda4381a3ac6cb8161a23e5ea540544c809063090815d?size=400', }, { name: 'Image only', image: 'https://tiesen.id.vn/assets/logotype.png', }, ] return ( <ul className='flex w-full flex-col gap-4 p-4'> {dsada.map((props) => ( <li key={props.name}> <p className='mb-2 text-lg font-semibold'>{props.name}</p> <Image src={`/api/og?title=${encodeURIComponent(props.title ?? '')}&description=${encodeURIComponent(props.description ?? '')}&image=${encodeURIComponent(props.image ?? '')}`} alt={props.title || 'Open Graph Image'} width={1200} height={630} loading='eager' className='border' /> </li> ))} </ul> )}Installation
CLI
npx shadcn add https://ui.tiesen.id.vn/r/open-graph.jsonnpx shadcn add https://ui.tiesen.id.vn/r/open-graph.jsonpnpm dlx shadcn add https://ui.tiesen.id.vn/r/open-graph.jsonbunx --bun shadcn add https://ui.tiesen.id.vn/r/open-graph.jsonManual
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
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,
}
)
}