Skip to the solution
HardFrontend4 min read

How do you implement dynamic Open Graph images in Next.js (ImageResponse)?

373 views

The problem

Question presented to candidate: "Marketing wants every blog post to have a generated social preview image with the post title. How would you build that in Next.js?"

What a strong answer should cover:

  • Add an opengraph-image.tsx (or twitter-image.tsx) file in the route segment; its default export returns an ImageResponse from next/og.
  • ImageResponse renders JSX with a subset of CSS (flexbox) to a PNG using Satori; export size, contentType and alt.
  • Next.js serves the image at a route and adds og:image, width, height, type and alt tags to that page automatically.
  • It can read params and fetch data, so each post gets its own image; static ones are generated at build time.
  • Set metadataBase, otherwise the og:image URL points at localhost.

Clarifying questions expected:

  • "Does every page need its own image or one per section?" β€” the file applies to its segment and below.
  • "Which fonts and brand assets are needed?" β€” they must be loaded explicitly into ImageResponse.

Code / implementation expected: Yes β€” an opengraph-image file for a dynamic route.

  • nextjs
  • og-image
  • imageresponse
  • seo

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

4 min read Β· 8 sections

Read the code

A per-post Open Graph image
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from "next/og";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { getPost } from "@/lib/posts";

export const size = { width: 1200, height: 630 };
export const contentType = "image/png";
export const alt = "Blog post preview";

export default async function Image({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const post = await getPost(slug);
  const inter = await readFile(join(process.cwd(), "assets/Inter-Bold.ttf"));
  return new ImageResponse(
    (
      <div style={{ width: "100%", height: "100%", display: "flex", flexDirection: "column", justifyContent: "center", padding: 80, background: "#0b0b0f", color: "white" }}>
        <div style={{ fontSize: 64, fontFamily: "Inter" }}>{post.title}</div>
        <div style={{ fontSize: 28, opacity: 0.7 }}>shop.example.com/blog</div>
      </div>
    ),
    { ...size, fonts: [{ name: "Inter", data: inter, weight: 700 }] },
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 117 of 129 in Next.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Next.js questions
Up nextHard

What is streaming SSR with Suspense in Next.js?

Continue