3 hours ago
Type-Safe meta images with Next.js and @vercel/og
#Dev
#NextJS
If you've ever shared a page from your website only to find that the meta images are broken, you're not alone. This happened to me across all my projects—Glink, Sisi, Benji, and others. The issue stemmed from an outdated service that I was using to generate meta images, which stopped working. So, I decided to fix it once and for all using Next.js.
The Problem
Previously, I had a <MetaTags/>
component that accepted some config that used an external service to generate images. Since this service no longer exists, all my meta images broke, leaving my pages with blank previews when shared.
The Solution
I decided to generate these images dynamically using an API route in Next.js. Here’s how I did it:
1. Creating an API Route for Image Generation
I set up an API route in pages/api/og/user.page.tsx
. This API route dynamically generates images when requested.
(I'm using the .page extension for pages in next.js, but you probably don't, so that's fine. I'm doing this so I can include other types of files in my /pages folder)
Sample /api/og/user.page.tsx
route that returns an image:
import { z } from 'zod';import { createOgImageHandler } from 'app/utils/og-utils';const PropsSchema = z.object({username: z.string(),avatar: z.string().optional(),bio: z.string().optional(),projects: z.string(),});export const OgUserPagePreview = ({ data }: { data: Props }) => {return (<div tw="flex flex-col w-full h-full items-center justify-center bg-white"><img width={200} height={200} src={data.avatar} alt={data.username} tw="rounded-full" /><h2 tw="text-3xl font-bold">{data.username}</h2></div>);};export type Props = z.infer<typeof PropsSchema>;export const config = {runtime: 'edge',};export default createOgImageHandler(PropsSchema, OgUserPagePreview);
This route receives parameters like username
and avatar
, then generates an Open Graph (OG) image on the fly.
2. Implementing Meta Tags with Dynamic Images
I made the MetaTags
component to dynamically generate the correct OG image URLs:
import Head from 'next/head';import { appName, appDomain, appTitle } from 'app/config/appName';import { buildOgImageUrl } from 'app/utils/og-utils';const baseUrl = `https://${appDomain}/`;const MetaTags: React.FC<MetaTagsProps> = (props) => {const {description = `${appName} - Keep your users in the loop`,title = appName,urlPath = '',} = props;const imageUrl = 'imageUrl' in props? props.imageUrl: buildOgImageUrl(props.ogConfig.route, props.ogConfig.props);const url = baseUrl + urlPath;return (<Head><title>{appTitle(title)}</title><meta property="og:image" content={imageUrl} /><meta property="og:title" content={title} /><meta property="og:description" content={description} /></Head>);};
Using the MetaTags component
<MetaTagsurlPath={`/${username}`}title={title}ogConfig={{route: 'ogUser', // <-- typesafe, points to /pages/api/og/userprops: { // <-- typesafe, the props must be same from the zod schemausername,avatar: avatarUrl,projects: String(projects.length),}}}/>
4. Automating API Route Type Generation
To ensure full type safety when using MetaTags
with an og
prop, I created a script that traverses the /api
directory and generates a api-routes.ts
file. This script scans for all API routes and extracts the parameters expected for each OG route.
Now, when using MetaTags
, the og
prop is fully type-safe, ensuring that only valid parameters can be passed.
You can find all the code snippets in this gist.
Final Thoughts
This setup ensures:
Now, whenever I share a page, the OG images load correctly without relying on external services. If you have a similar issue, try this approach and let me know how it works for you!