---
name: og-images
description: Generates Open Graph and Twitter Card images for social media sharing. Use when someone needs to create or update OG images, social media preview images, or Twitter cards. Triggers include "generate OG images", "create social images", "update twitter card", "social media previews", or when discussing meta tags for social sharing.
---

# OG Image Generator

This skill generates Open Graph (OG) and Twitter Card images for social media sharing using a Next.js page and Playwright screenshots.

## Overview

The system consists of:
1. **OG Page** (`/app/og/page.tsx`) - A Next.js page that renders the image at exact dimensions
2. **OG Layout** (`/app/og/layout.tsx`) - A wrapper layout for the OG route
3. **Playwright** - Takes screenshots at the correct viewport sizes
4. **Static Images** - Saved to `/public/` for use in meta tags

## Image Formats & Dimensions

| Format | Dimensions | Use Case |
|--------|------------|----------|
| Landscape | 1200×628 | Twitter, LinkedIn, Facebook, Open Graph |
| Square | 1200×1200 | Instagram, general social |
| Portrait | 1080×1350 | Instagram feed, mobile-first platforms |
| LinkedIn Banner | 4200×700 | LinkedIn company page cover image |

## Workflow

### Step 1: Create/Update the OG Page

The OG page at `/app/og/page.tsx` should:

1. Accept a `format` query parameter (`landscape`, `square`, `portrait`)
2. Render at exact dimensions using inline styles with min/max constraints
3. Use the site's branding (colors, fonts, logo)
4. Include the hero image with proper styling (rounded corners, glow/shadow)

**Key styling requirements:**
- Set explicit `width`, `height`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight` on the container
- Use `overflow: "hidden"` to prevent content overflow
- Apply `borderRadius` and `boxShadow` directly to `<Image>` elements (not containers) when using `objectFit: "contain"`
- For images that should fill their container, use `fill` prop with `objectFit: "cover"`

**Example structure:**
```tsx
const dimensions: Record<Format, { width: number; height: number }> = {
  landscape: { width: 1200, height: 628 },
  square: { width: 1200, height: 1200 },
  portrait: { width: 1080, height: 1350 },
  "linkedin-banner": { width: 4200, height: 700 },
};

export default async function OGImage({
  searchParams,
}: {
  searchParams: Promise<{ format?: string }>;
}) {
  const params = await searchParams;
  const format = (params.format as Format) || "landscape";
  const { width, height } = dimensions[format] || dimensions.landscape;

  return (
    <div style={{
      width,
      height,
      minWidth: width,
      maxWidth: width,
      minHeight: height,
      maxHeight: height,
      display: "flex",
      // ... rest of styles
    }}>
      {/* Content */}
    </div>
  );
}
```

### Step 2: Create the Layout

Create `/app/og/layout.tsx` to wrap the OG page:

```tsx
export default function OGLayout({ children }: { children: React.ReactNode }) {
  return (
    <div
      style={{
        margin: 0,
        padding: 0,
        overflow: "hidden",
        backgroundColor: "#1a1f25",
        minHeight: "100vh",
      }}
    >
      {children}
    </div>
  );
}
```

### Step 3: Hide from Search Engines

Add to `/public/robots.txt`:
```
# Hide internal OG image generation page
Disallow: /og
```

### Step 4: Generate Images with Playwright

Run the dev server, then use Playwright to screenshot each format:

```bash
# Portrait (1080×1350)
npx playwright screenshot --viewport-size="1080,1350" "http://localhost:3001/og?format=portrait" public/og-portrait.png

# Landscape (1200×628)
npx playwright screenshot --viewport-size="1200,628" "http://localhost:3001/og?format=landscape" public/og-landscape.png

# Square (1200×1200)
npx playwright screenshot --viewport-size="1200,1200" "http://localhost:3001/og?format=square" public/og-square.png

# LinkedIn Banner (4200×700)
npx playwright screenshot --viewport-size="4200,700" "http://localhost:3001/og?format=linkedin-banner" public/og-linkedin-banner.png
```

### Step 5: Update Meta Tags

In `/app/layout.tsx`, add the OG and Twitter meta tags:

```tsx
export const metadata: Metadata = {
  openGraph: {
    title: "Your Title",
    description: "Your description",
    images: [
      {
        url: "/og-landscape.png",
        width: 1200,
        height: 628,
        alt: "Description",
      },
    ],
    type: "website",
  },
  twitter: {
    card: "summary_large_image",
    title: "Your Title",
    description: "Your description",
    images: ["/og-landscape.png"],
  },
};
```

## LinkedIn Banner Layout

The LinkedIn company page cover image (4200×700) uses a split layout: the left 50% is empty space showing only the background wallpaper, and the right 50% contains all text and the company logo. This accounts for the LinkedIn profile photo and company info that overlay the left side of the banner on the LinkedIn page.

**Layout principles:**
- Use a two-column flex layout with each half at 50% width
- Left half: empty — only the background image/wallpaper shows through
- Right half: company logo and any text, right-aligned or centered within that half
- Keep text minimal and large enough to read at reduced sizes (LinkedIn scales the banner down)
- The background wallpaper should span the full 4200×700 area behind both halves

**Example structure:**
```tsx
{format === "linkedin-banner" && (
  <div style={{
    width: 4200,
    height: 700,
    display: "flex",
    position: "relative",
    overflow: "hidden",
  }}>
    {/* Background wallpaper spanning full width */}
    <Image src="/background.jpg" alt="" fill style={{ objectFit: "cover" }} />

    {/* Left half: intentionally empty (background only) */}
    <div style={{ width: "50%", flexShrink: 0 }} />

    {/* Right half: logo and text */}
    <div style={{
      width: "50%",
      display: "flex",
      flexDirection: "column",
      alignItems: "flex-end",
      justifyContent: "center",
      padding: "32px 48px",
      position: "relative",
      zIndex: 1,
    }}>
      <Image src="/logo.png" alt="Company Logo" width={120} height={120} />
      <h1 style={{ color: "#fff", fontSize: 28, marginTop: 16 }}>Company Name</h1>
      <p style={{ color: "#ccc", fontSize: 16 }}>Tagline goes here</p>
    </div>
  </div>
)}
```

## Common Issues & Solutions

### Image appears truncated
- Use `objectFit: "contain"` instead of `"cover"`
- Ensure container has enough height for the image

### Rounded corners not visible
- Apply `borderRadius` directly to the `<Image>` style prop, not the container
- This is required when using `objectFit: "contain"` because the image doesn't fill the container

### Shadow not visible
- Use a colored glow (e.g., cyan) instead of pure black on dark backgrounds
- Example: `boxShadow: "0 0 60px rgba(0, 212, 255, 0.3), 0 20px 60px rgba(0, 0, 0, 0.5)"`

### Icon not filling container
- Use `fill` prop on Next.js Image component
- Add `position: "relative"` to container and `objectFit: "cover"` to image

### Wrong dimensions in screenshot
- Set explicit min/max constraints on the outer container
- Use the exact viewport size in Playwright command

## Regenerating Images

When making changes to the OG page, always regenerate all three images:

```bash
npx playwright screenshot --viewport-size="1080,1350" "http://localhost:3001/og?format=portrait" public/og-portrait.png && \
npx playwright screenshot --viewport-size="1200,628" "http://localhost:3001/og?format=landscape" public/og-landscape.png && \
npx playwright screenshot --viewport-size="1200,1200" "http://localhost:3001/og?format=square" public/og-square.png && \
npx playwright screenshot --viewport-size="4200,700" "http://localhost:3001/og?format=linkedin-banner" public/og-linkedin-banner.png
```

## Verification

After generating, verify:
1. Images exist in `/public/` with correct file sizes
2. Dimensions are correct (use an image viewer or `file` command)
3. Visual appearance matches design (open the files to inspect)
