danielkhoo.xyz

twitter

github

Dynamic OpenGraph Cards with VercelOG

Having a personal site is great for standing out and expressing your creativity. But one of the underrated elements of popular sites like medium or substack is the automatic unfurling of content. Luckily there's an easy way to add them to any site.

Twitter

<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:creator" content="@jadenkore" />
<meta property="twitter:domain" content="danielkhoo.xyz" />
<meta property="twitter:url" content="https://danielkhoo.xyz/dynamic-open-graph-cards" />
<meta name="twitter:title" content="Twitter and OpenGraph Cards" />
<meta name="twitter:description" content="Having a personal site is great for standing out and expressing your creativity..." />
<meta name="twitter:image" content="https://danielkhoo.xyz/sd2.png" />

OpenGraph

<meta property="og:url" content="https://danielkhoo.xyz/" />
<meta property="og:type" content="article" />
<meta property="og:title" content="Twitter and OpenGraph Cards" />
<meta property="og:description" content="Having a personal site is great for standing out and expressing your creativity..." />
<meta property="og:image" content="https://danielkhoo.xyz/sd2.png" />

Taking it further

This is great for showing a static image, but what if you want to dynamically generate the image?


Static Card

For example, if you want to show a preview of your latest blog post. It would be cool to generate cover images on the fly with specific the title and description like github does.


Github Dynamic OG Card

Vercel OG

Luckily, Vercel has a great tool for generating dynamic OG cards called Vercel OG. It leverages Satori to generate images from HTML/CSS. This let's you pull data from the query params.

So instead of a static url image like:

<meta name="twitter:image" content="https://danielkhoo.xyz/sd2.png" />

The url is a call to an api endpoint with the query params like:

<meta name="twitter:image" content="https://danielkhoo.xyz/api/og?title=YourTitle&description=YourDescription" />

Implementation is pretty simple, you just need to create a function in the api folder and return the image. The Vercel OG docs have some great examples of how to do this. I've chosen to style my cards similar to Github's

import { ImageResponse } from '@vercel/og'
export const config = {
  runtime: 'edge',
}

export default async function handler(req) {
  const HOST = 'https://danielkhoo.xyz'
  const { searchParams } = req.nextUrl
  const title = searchParams.get('title') || 'danielkhoo.xyz'
  const description = searchParams.get('description') || `Hello there! I'm Daniel. Welcome to my online home for ideas, writing and side projects.`

  const robotoArrayBuffer = await fetch(`${HOST}/fonts/Roboto-Regular.ttf`).then(res => res.arrayBuffer())
  const robotoBoldArrayBuffer = await fetch(`${HOST}/fonts/Roboto-Bold.ttf`).then(res => res.arrayBuffer())
  const truncatedDescription = description.length > 120 ? description.slice(0, 120) + "..." : description
  return new ImageResponse(
    (
      <div
        style={{
          background: '#fff',
          width: '100%',
          height: '100%',
          padding: 32,
          justifyContent: 'center',
          alignItems: 'center',
          display: 'flex',
        }}
      >
        <div style={{ display: 'flex', flexDirection: 'column', flex: 3, marginRight: 24 }}>
          <p style={{ fontSize: 60, fontWeight: 700 }}>{title}</p>
          <p style={{ fontSize: 32, color: '#777', lineHeight: '50px' }}>{truncatedDescription}</p>
        </div>
        {/* eslint-disable-next-line @next/next/no-img-element */}
        <img
          alt="avatar"
          width="256"
          src={`${HOST}/dp.jpeg`}
          style={{ flex: 1, borderRadius: 32 }}
        />
      </div>
    ),
    {
      width: 1200,
      height: 600,
      fonts: [
        {
          name: 'Roboto',
          data: robotoArrayBuffer,
          weight: 400,
          style: 'normal',
        },
        {
          name: 'Roboto',
          data: robotoBoldArrayBuffer,
          weight: 700,
          style: 'normal',
        },
      ],
    }
  )
}

Conclusion

Put it all together and you have dynamic image generation with fully customisability. You can easily extend this to multiple images, more elaborate styling, etc.


Fully Customisable OG Card