@phucbm/next-og-image
v2.0.0
Published
Tiny helpers to generate OG images and Metadata in Next.js (Edge-ready).
Maintainers
Readme
@phucbm/next-og-image
Tiny helpers to ship Open Graph images and page metadata in Next.js (Edge-ready). Drop-in defaults, easy overrides, and a dead-simple render API.
Install
pnpm add @phucbm/next-og-imageRequires: Next.js 13.4+ (App Router), React 18+, Node 18+. Recommended:
export const runtime = "edge"for the OG route.
Quick start
1) Default with static OG image
app/layout.tsx
import { generatePageMetadata } from "@phucbm/next-og-image";
export const generateMetadata = generatePageMetadata({
baseUrl: process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000",
siteName: "Example Site",
title: "Example Site - Design. Code. Repeat.",
description: "A simple demo page for Open Graph metadata.",
canonicalPath: "/",
imageUrl: "/og-image.jpg", // from /public/og-image.jpg — used for all routes by default
});All routes inherit this static OG image. No /api/og-image route is required.
2) Override with dynamic OG image on specific routes
app/api/og-image/route.ts (only needed if using socialImage)
import { renderOgImage } from "@phucbm/next-og-image";
export const runtime = "edge";
export async function GET(request: Request) {
return renderOgImage(request);
}app/blog/[slug]/page.tsx
import { generatePageMetadata } from "@phucbm/next-og-image";
export const generateMetadata = generatePageMetadata({
baseUrl: process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000",
siteName: "Example Site",
title: "Blog Post Title",
description: "Blog post description",
canonicalPath: "/blog/my-post",
socialImage: {
title: "Custom OG Title", // optional; overrides page title
description: "Custom OG description", // optional; overrides page description
},
});The socialImage object triggers dynamic OG image generation via /api/og-image.
3) Custom OG image component
app/api/og-image/route.tsx
import { type OgImageRenderFn, renderOgImage } from "@phucbm/next-og-image";
import { OgImage } from "@/components/OgImage";
export const runtime = "edge";
export async function GET(request: Request) {
return renderOgImage(request, OgImage);
}Your
OgImagereceives{ siteName, title, description }as props.
OG Image Priority
When generating metadata, images are resolved in this order:
- Dynamic OG — If
socialImageis provided, generate/api/og-image?...URL - Static OG — If
imageUrlis provided, use static image URL - No OG — If neither is provided, no OG image is included
Pass custom data into your OG component (e.g., logo)
app/api/og-image/route.tsx
import { type OgImageRenderFn, renderOgImage } from "@phucbm/next-og-image";
import { OgImage } from "@/components/OgImage";
export const runtime = "edge";
export async function GET(request: Request) {
const origin = new URL(request.url).origin;
const logoUrl = new URL("/logo.png", origin).toString(); // /public/logo.png -> /logo.png
const render: OgImageRenderFn = (props) => <OgImage {...props} logoUrl={logoUrl} />;
return renderOgImage(request, render, { width: 1200, height: 630 });
}components/OgImage.tsx
import type { OgImageInput } from "@phucbm/next-og-image";
type Props = OgImageInput & { logoUrl?: string };
export function OgImage({ siteName, title, description, logoUrl }: Props) {
return (
<div style={{
width: "100%", height: "100%", display: "flex", flexDirection: "column",
justifyContent: "space-between", padding: "48px", color: "#141413",
background: "linear-gradient(135deg, #f0eee6, #faf9f6)"
}}>
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
{logoUrl && <img src={logoUrl} alt="" width="88" height="88" />}
{siteName && <div style={{ fontSize: 70, fontWeight: 800, letterSpacing: "-0.02em" }}>{siteName}</div>}
</div>
<div style={{ display: "flex", flexDirection: "column", gap: "24px" }}>
<h1 style={{ fontSize: 120, fontWeight: 800, margin: 0 }}>{title}</h1>
{description && (
<p style={{ fontSize: 56, margin: 0, maxWidth: "1000px", opacity: 0.95 }}>
{description}
</p>
)}
</div>
</div>
);
}Important:
- Put the image in
/public/logo.pngand reference it as/logo.png. next/ogrequires an absolute URL inside the renderer; always build fromreq.url’s origin (as shown).
API
renderOgImage(request, render?, options?)
request: theRequestfrom your API route.render?:(props: OgImageInput) => ReactElement— custom renderer for the image.propsincludes{ siteName: string; title: string; description: string | null }.
options?:ImageResponseOptions(e.g.,{ width, height, headers, emoji, fonts, ... }).
Returns a new ImageResponse(...).
generatePageMetadata(input)
- Builds a Next.js
Metadataobject and wires the page's OG/Twitter tags. - OG image priority:
- If
socialImageis provided → generates URL to/api/og-image - Else if
imageUrlis provided → uses static image URL - Else → no OG image
- If
- Accepts all your SEO fields plus any native
Metadatakeys.
Null-override behavior (for social image):
If you pass socialImage: { title: null }, that field is intentionally hidden (no fallback to page title).
undefined continues to fallback.
Tips & Gotchas
Route must handle JSX → name it
route.tsxif you inline JSX.Import React when using JSX in API routes:
import React from "react";Absolute URLs only for
<img src>. Build fromnew URL(req.url).origin.SVGs are not supported by
next/og— use PNG/JPEG/WebP.
Showcase
Projects using this package:
Chrome Extension
Use Social Share Preview (by Placid app) to quickly test OG images in the browser.
Example query (dev)
/api/og-image?siteName=Example&title=Hello&description=Ship+itThat’s it. Copy the snippets above into your app and you’re live.
License
MIT © PHUCBM
