Adding Fancy OG Images to your NextJS MDX Blog

Adding Fancy OG Images to your NextJS MDX Blog

A lifetime ago, I was primarily a UX designer. Nothing was more fun than openeing my design tool of choice and creating a beautiful, shareable image for a blog post.

Nowawdays, with engineering as the priority, I don't have time to create these image manually, but I still want them to look good.

Enter @vercel/og. 🚀

With this library from Vercel, we'll get set up to dynamically generate nice-looking shareable images for our posts.

Getting Started

First, we'll need to install the package.

$ yarn add @vercel/og

Building the base API Route

To start, we'll create a file at /pages/api/og.js. This will be our base API route for generating our images.

/src/pages/api/og.js
import { ImageResponse } from '@vercel/og';
 
export const config = {
  runtime: 'experimental-edge',
};
 
export default async function handler() {
  return new ImageResponse(
    (
      <div
        style={{
          display: 'flex',
          background: '#fff',
          width: '100%',
          height: '100%',
          flexDirection: 'column',
          justifyContent: 'center',
          alignItems: 'center',
        }}
      >
        <p>Hello world</p>
      </div>
    ),
    {
      width: 1200,
      height: 630,
    },
  );
}

Visiting http://localhost:3000/api/og should now render a simple image with the text "Hello world". Like this:

Note: Image generation can be slow...

OG Sample 1

Adding a custom background image

I use TailwindCSS for my styling, and @vercel/og has experimental support for it, so let's try it out.

src/pages/api/og.js

import { ImageResponse } from '@vercel/og';
 
export const config = {
  runtime: 'experimental-edge',
};
 
export default async function handler() {
  return new ImageResponse(
    (
      <div
        style={{
          display: 'flex',
          background: '#fff',
          width: '100%',
          height: '100%',
          flexDirection: 'column',
          justifyContent: 'center',
          alignItems: 'center',
          background: `linear-gradient( 135deg, #FD6585 10%, #0D25B9 100%)`
        }}
      >
        <div tw="flex relative w-[100%] h-[100%]">
          <p tw="text-white text-7xl tracking-tighter top-10 right-10 absolute text-right max-w-4xl">Hello World</p>
        </div>
      </div>
    ),
    {
      width: 1200,
      height: 630,
    },
  );
}

This should land you with something that looks like this:

OG Sample 2

Handling custom sizes

Now, not all platforms want the same size image, so we'll want to customize the returned size based on the platforms we're targeting. Let's go ahead and make that dynamic.

src/pages/api/og.js
import { ImageResponse } from '@vercel/og';
 
export const config = {
  runtime: 'experimental-edge',
};
 
export default async function handler(req) {
  const { searchParams } = req.nextUrl;

  const width = searchParams.get('w') ? parseInt(searchParams.get('w'), 10) : 1200;
  const height = searchParams.get('h') ? parseInt(searchParams.get('h'), 10) : 630;

  return new ImageResponse(
    (
      <div
        style={{
          display: 'flex',
          background: '#fff',
          width: '100%',
          height: '100%',
          flexDirection: 'column',
          justifyContent: 'center',
          alignItems: 'center',
          background: `linear-gradient( 135deg, #FD6585 10%, #0D25B9 100%)`
        }}
      >
        <div tw="flex relative w-[100%] h-[100%]">
          <p tw="text-white text-7xl tracking-tighter top-10 right-10 absolute text-right max-w-4xl">Hello World</p>
        </div>
      </div>
    ),
    {
      width,
      height,
    },
  );
}

This should let you query your image with new parameters. For example if you wanted and 800x800 image, simply query the url http://localhost:3000/api/og?w=800&h=800, and you should get this:

OG Sample 3

See where we're going with this? 😎

Enabling dynamic text in your OG images

Now that we're familiar with adding parameters, let's add dynamic text to our image. While we're at it, let's add a little textShadow to make it pop.

src/pages/api/og.js
import { ImageResponse } from '@vercel/og'

export const config = {
  runtime: 'experimental-edge',
}

export default async function handler(req) {
  const { searchParams } = req.nextUrl

  const text = searchParams.get('c') || 'Check out tybarho.com!'
  const width = searchParams.get('w')
    ? parseInt(searchParams.get('w'), 10)
    : 1200
  const height = searchParams.get('h')
    ? parseInt(searchParams.get('h'), 10)
    : 630

  return new ImageResponse(
    (
      <div
        style={{
          display: 'flex',
          background: '#fff',
          width: '100%',
          height: '100%',
          flexDirection: 'column',
          justifyContent: 'center',
          alignItems: 'center',
          background: `linear-gradient( 135deg, #FD6585 10%, #0D25B9 100%)`,
        }}
      >
        <div tw="flex relative w-[100%] h-[100%] p-8">
          <p
            tw="text-white text-7xl tracking-tighter text-right"
            style={{ textShadow: '1px 1px 30px #00000070' }}
          >
            {text}
          </p>
        </div>
      </div>
    ),
    {
      width,
      height,
    }
  )
}

That should give you something that looks like this:

OG Sample 4

Adding a friendly avatar

I'd also like my ugly mug to show up on these posts, to give them a little more personality. Let's add that in.

We'll be a little fancy, and use a dynamic Github username to pull the current profile pic from Github.

src/pages/api/og.js
import { ImageResponse } from '@vercel/og'

export const config = {
  runtime: 'experimental-edge',
}

export default async function handler(req) {
  const { searchParams } = req.nextUrl

  
  const text = searchParams.get('c') || 'Check out tybarho.com!'
  const username = searchParams.get('username') || 'tbarho'
  const width = searchParams.get('w')
    ? parseInt(searchParams.get('w'), 10)
    : 1200
  const height = searchParams.get('h')
    ? parseInt(searchParams.get('h'), 10)
    : 630

  return new ImageResponse(
    (
      <div
        style={{
          display: 'flex',
          background: '#fff',
          width: '100%',
          height: '100%',
          flexDirection: 'column',
          justifyContent: 'center',
          alignItems: 'center',
          background: `linear-gradient( 135deg, #FD6585 10%, #0D25B9 100%)`,
        }}
      >
        <div tw="flex relative w-[100%] h-[100%] p-8">
          <p
            tw="text-white text-7xl tracking-tighter text-right"
            style={{ textShadow: '1px 1px 30px #00000070' }}
          >
            {text}
          </p>
          <div tw="bg-white rounded-full w-64 h-64 absolute bottom-8 left-8 flex justify-center items-center shadow-xl">
            <img tw="w-60 h-60 rounded-full" src={`https://www.github.com/${username}.png`} />
          </div>
        </div>
      </div>
    ),
    {
      width,
      height,
    }
  )
}

This should result in something that looks like this...

OG Sample 5

Adding automatic OG Images to our blog route

Finally, let's add these images to our blog route.

I need a number of different sizes to be generated for different social networks, so I'm going to create a little helper function that I can import into my pages.

src/lib/ogSizes.js
const BASE_URL = __DEV__ ? 'http://localhost:3000' : 'https://www.mysite.com'

export const ogSizes = [
  {
    path: 'Facebook',
    width: 940,
    height: 788,
  },
  {
    path: 'Instagram',
    width: 1080,
    height: 1080,
  },
  {
    path: 'LinkedIn',
    width: 1200,
    height: 1200,
  },
  {
    path: 'Twitter',
    width: 1600,
    height: 900,
  },
  {
    path: 'Pinterest',
    width: 1000,
    height: 1500,
  },
];

The path attribute is really just there for my own reference.

Next, let's update the layout file for our blog posts. If you haven't set up next-seo yet, check out this article on setting up NextSeo on your site.

src/pages/blog/[slug].js
// ... other imports
import { ogSizes, BASE_URL } from '../../lib/ogSizes'

export default function Page({ meta }) {
  // ... other code

  const title = `${meta.title} - ${meta.author}`;
  const description = meta.description;
  const canonical = `https://www.tybarho.com${router.asPath}`;

  // Map our images helper
  const images = ogSizes.map(({ width, height }) => ({
    url: `${BASE_URL}/api/og?c=${encodeURIComponent(meta.title)}&username=[your_gh_username]&w=${width}&h=${height}`,
    width,
    height,
    type: 'image/png'
  }));

  return (
    <>
      <NextSeo
        title={title}
        description={description}
        canonical={canonical}
        openGraph={{
          url: canonical,
          title,
          description,
          images,
        }}
      />
      {/* ... other page code*/}
    </>
  );
}

Now, if you check out the developer console in your browser, you should see the OG image tags being added to your <head> tag.

OG Images Tags

Wrap Up

So there you go. Automatic, "fancy," OG images on your NextJS site. Feel free to experiment and go as hog-wild as you want.

I recommend checking out the @vercel/og docs for more interesting things you can do.

Happy coding! ✨

If you enjoyed this article, please consider following me on Twitter

Subscribe to the Newsletter

Subscribe for exclusive tips, strategies, and resources to launch, grow, & build your SaaS team.

Share this article on: