npm package: @phucbm/next-og-image
Description: Tiny helpers to generate OG images and Metadata in Next.js (Edge-ready).
Author: phucbm
Latest version: 2.0.0

# @phucbm/next-og-image

[![npm version](https://badgen.net/npm/v/@phucbm/next-og-image?icon=npm)](https://www.npmjs.com/package/@phucbm/next-og-image)
[![npm downloads](https://badgen.net/npm/dm/@phucbm/next-og-image?icon=npm)](https://www.npmjs.com/package/@phucbm/next-og-image)
[![npm dependents](https://badgen.net/npm/dependents/@phucbm/next-og-image?icon=npm)](https://www.npmjs.com/package/@phucbm/next-og-image)
[![github stars](https://badgen.net/github/stars/phucbm/next-og-image?icon=github)](https://github.com/phucbm/next-og-image/)
[![github license](https://badgen.net/github/license/phucbm/next-og-image?icon=github)](https://github.com/phucbm/next-og-image/blob/main/LICENSE)

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

```bash
pnpm add @phucbm/next-og-image
```

> Requires: 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**

```ts
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`)

```ts
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**

```ts
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**

```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 `OgImage` receives `{ siteName, title, description }` as props.

---

## OG Image Priority

When generating metadata, images are resolved in this order:

1. **Dynamic OG** — If `socialImage` is provided, generate `/api/og-image?...` URL
2. **Static OG** — If `imageUrl` is provided, use static image URL
3. **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**

```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**

```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.png` and reference it as `/logo.png`.
* `next/og` requires an **absolute URL** inside the renderer; always build from `req.url`’s origin (as shown).

---

## API

### `renderOgImage(request, render?, options?)`

* `request`: the `Request` from your API route.
* `render?`: `(props: OgImageInput) => ReactElement` — custom renderer for the image.

  * `props` includes `{ 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 `Metadata` object and wires the page's OG/Twitter tags.
* **OG image priority:**
  1. If `socialImage` is provided → generates URL to `/api/og-image`
  2. Else if `imageUrl` is provided → uses static image URL
  3. Else → no OG image
* Accepts all your SEO fields plus any native `Metadata` keys.

**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.tsx` if you inline JSX.
* **Import React** when using JSX in API routes:

  ```ts
  import React from "react";
  ```
* **Absolute URLs only** for `<img src>`. Build from `new URL(req.url).origin`.
* **SVGs** are not supported by `next/og` — use PNG/JPEG/WebP.

---

## Showcase

Projects using this package:

* [tich.vn](https://tich.vn)
* [perxel.com](https://perxel.com)

---

## Chrome Extension

Use **[Social Share Preview](https://chromewebstore.google.com/detail/social-share-preview/ggnikicjfklimmffbkhknndafpdlabib?pli=1)** (by Placid app) to quickly test OG images in the browser.

---

## Example query (dev)

```
/api/og-image?siteName=Example&title=Hello&description=Ship+it
```

That’s it. Copy the snippets above into your app and you’re live.

## License
MIT © [PHUCBM](https://github.com/phucbm)