How to Create Blog Cover Images with Next.js
Create a unique Open Graph cover for each Next.js blog post with the App Router, or use a static image when prepared artwork is enough.
Use the App Router’s opengraph-image file convention to attach a cover image to a route. For a unique image per post, add app/blog/[slug]/opengraph-image.tsx, load the post for that slug, and return an ImageResponse. Next.js documents 1200 × 630 pixels as an example size. If every post can use prepared artwork, put a static opengraph-image.jpg in the appropriate route directory instead.
This creates an Open Graph image for link previews; it is separate from next/image, which optimizes images displayed inside a page. The examples below use the App Router and a small local post lookup so you can run the route without adding a data service.
1. Choose static artwork or a generated image
| Approach | Use it when | Trade-off |
|---|---|---|
| Static file | You have finished artwork, or all posts can share the same cover. | Simple to maintain, but each distinct post image must be prepared separately. |
| Generated route | The cover should include the post title, author, category, or other post data. | Scales across posts, but the design must fit the CSS supported by ImageResponse and the route needs access to post data. |
More specific image files deeper in the route tree take precedence over higher-level Open Graph images. A static file is a good choice for a section-wide default; a generated file under blog/[slug] can give each post its own image.
2. Add post data and a generated image route
This minimal project shape keeps the example self-contained:
app/
blog/
[slug]/
opengraph-image.tsx
lib/
posts.ts
Create a lookup function that returns the same post data your page uses. Replace this example with your CMS or database query as needed.
// lib/posts.ts
export type Post = {
title: string;
category: string;
};
const posts: Record<string, Post> = {
"nextjs-og-images": {
title: "Create Blog Cover Images with Next.js",
category: "Next.js Guides",
},
};
export async function getPost(slug: string): Promise<Post | null> {
return posts[slug] ?? null;
}
Then create the metadata image route. This example uses a 1200 × 630 PNG, flexbox layout, and a clear fallback for an unknown slug.
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from "next/og";
import { getPost } from "@/lib/posts";
export const size = {
width: 1200,
height: 630,
};
export const contentType = "image/png";
export default async function Image({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) {
return new Response("Post not found", { status: 404 });
}
return new ImageResponse(
(
<div
style={{
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "center",
padding: "72px",
background: "#111827",
color: "#ffffff",
fontSize: 64,
fontWeight: 700,
}}
>
<div style={{ display: "flex", fontSize: 26, color: "#a5b4fc" }}>
{post.category}
</div>
<div style={{ display: "flex", marginTop: 28, lineHeight: 1.15 }}>
{post.title}
</div>
</div>
),
size,
);
}
Recent App Router versions type route parameters as a promise, as shown. Check the route parameter types for your installed Next.js version and adapt the signature if your project uses a version with synchronous parameters. The route’s JSX is rendered by the image response pipeline, not a regular browser page.
The file convention causes Next.js to add the corresponding Open Graph metadata. You can export metadata values such as alt, size, and contentType alongside the route. The example explicitly exports dimensions and content type to make the output clear.
3. Use a static image when you already have artwork
For a prepared cover, place a supported image file in the route directory. For example:
app/
blog/
opengraph-image.jpg
[slug]/
opengraph-image.tsx
The file under [slug] is more specific and can supply post-specific metadata images; the parent image can serve routes without a more specific file. Use the static approach if the image is authored outside the app and does not need to be assembled from post data.
4. Keep the design within ImageResponse’s CSS support
ImageResponse supports a subset of CSS rather than a full browser rendering engine. The official approach uses flexbox and absolute positioning; it also supports text wrapping, custom fonts, and nested images. CSS Grid is an example of a layout that is not supported. Build the composition with supported styles and inspect the rendered image in your own project before relying on it.
- Use flex containers and explicit dimensions for predictable layout.
- Keep long titles in mind: allow wrapping, constrain the content area, and choose a font size that can accommodate the longest post title.
- Use a fallback for missing post data instead of rendering undefined values.
- Check any custom font and nested image inputs against the current Next.js documentation and your installed version.
The documented image example uses 1200 × 630 pixels; treat that as a useful baseline, not a guarantee that every social platform requires identical dimensions.
5. Understand metadata, rendering, and caching
Next.js treats metadata image files as specialized route handlers and automatically emits the corresponding head tags. Generated Open Graph images are statically optimized and cached by default. Depending on the route, they may be generated at build time rather than regenerated for every request.
Dynamic APIs, uncached data, or route configuration can change that behavior. If post titles or images are updated after deployment, decide how the route should refresh and configure data caching and route behavior accordingly. Do not assume a content change instantly changes a cached image: verify the chosen revalidation or deployment flow for your Next.js version and data source.
6. Verify the image and metadata
- Start the app and open a real post route, such as
/blog/nextjs-og-images. - Open the generated metadata image URL associated with that route and confirm it returns an image rather than an error.
- Inspect the image dimensions, title wrapping, colors, and missing-data behavior.
- Inspect the rendered page head for the generated Open Graph image metadata.
- Repeat after a production build or deployment if your app’s data fetching and cache behavior differs between development and production.
The metadata convention manages the association between the route and image. Use next/image separately when you need to size, lay out, or optimize images shown in page content; it is not the documented API for generating route-specific OG artwork.
7. Troubleshoot common problems
| Symptom | Likely cause | Fix |
|---|---|---|
| The image route fails to compile or render. | The JSX uses CSS or features outside the image renderer’s supported subset. | Reduce the layout to supported flexbox and positioning styles, then add design details incrementally. |
| A post title is missing or displays incorrectly. | The slug did not resolve, the CMS returned no title, or the title is too long for the design. | Handle not-found data explicitly, verify slug decoding and lookup behavior, and test long titles with wrapping and a suitable font size. |
| The OG image does not update after editing a post. | The generated route or its data is statically optimized and cached. | Check the route’s caching and revalidation configuration, then rebuild or refresh the cache according to the intended publishing workflow. |
| A post uses the section-wide image instead of its own. | The more specific file is missing, misplaced, or not in the expected route directory. | Confirm the generated or static image is under the dynamic post route and check the route tree for filename and nesting errors. |
| The image looks different from the page’s CSS. | ImageResponse does not run a complete browser layout engine. | Use only documented supported styles and inspect the actual generated output rather than assuming browser CSS parity. |
| The image URL is fine locally but unavailable after deployment. | The production build may expose a data, font, asset, or route configuration problem not present in development. | Check deployment logs and ensure all data and assets required by the image route are available to the production renderer. |
8. Performance, reliability, and cost
Static optimization and caching by default can avoid doing the same image rendering for every request. Dynamic data or route settings may change when rendering happens, so balance freshness against repeated work and verify the production behavior. Keep the route’s data lookup focused: it needs only the fields used in the image. A missing post, unsupported style, or unavailable asset should be handled deliberately so the image route does not produce a broken preview.
The documented workflow uses Next.js features and does not require buying a separate image service. Hosting and rendering costs depend on your deployment and route behavior; the research basis does not establish a benchmark or universal cost figure.
Or skip the browser setup
If you also need clean screenshots of live pages for previews or documentation, ScreenshotNeo is a website screenshot API and MCP server for developers. Its API takes one GET request and returns a PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', new Uint8Array(await res.arrayBuffer()));
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; responses report the page verdict and billing status. Its MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.
Create a free ScreenshotNeo account and get 1,000 screenshots a month with no card.
FAQ
Can one Open Graph image be shared across a whole blog?
Yes. Put a static metadata image at the blog or app level when a shared cover is suitable; a more specific route image can override it.
Does this create an image for the page itself?
No. It creates a social metadata image. Use page markup and next/image for images rendered within the page content.
Will the image be regenerated on every request?
Not by default. Next.js statically optimizes and caches generated metadata images unless dynamic APIs, uncached data, or route configuration changes the behavior.
Can I use CSS Grid in the image route?
Do not rely on it: the image renderer supports a CSS subset, and Next.js names Grid as an unsupported example.


