# OG Image Redesign Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Replace text-only OG images with branded images using dealer hero photos, AMSOIL logo, and dynamic dealer info that respects their display settings.

**Architecture:** Pre-generate OG images on publish using Next.js ImageResponse (Satori). Store cached images at `/public/og-cache/[subdomain]/[slug].png`. Serve cached images directly; regenerate on dealer/page publish. Delete cached images when pages/dealers are deleted.

**Tech Stack:** Next.js 16, Satori/ImageResponse, Prisma, React Server Components

---

## Task 1: Create OG Image Generation Utility

**Files:**
- Create: `lib/og-image-generator.tsx`
- Test: `lib/__tests__/og-image-generator.test.tsx`

**Step 1: Write the failing test for image generation parameters**

```typescript
// lib/__tests__/og-image-generator.test.tsx
import { buildOgImageParams, OgImageConfig } from '../og-image-generator';

describe('buildOgImageParams', () => {
  it('should include AMSOIL logo always', () => {
    const config: OgImageConfig = {
      businessName: 'Test Dealer',
    };
    const params = buildOgImageParams(config);
    expect(params.showAmsoilLogo).toBe(true);
  });

  it('should include dealer logo when logoUrl is set', () => {
    const config: OgImageConfig = {
      businessName: 'Test Dealer',
      logoUrl: 'https://example.com/logo.png',
    };
    const params = buildOgImageParams(config);
    expect(params.dealerLogoUrl).toBe('https://example.com/logo.png');
  });

  it('should not include dealer logo when logoUrl is empty', () => {
    const config: OgImageConfig = {
      businessName: 'Test Dealer',
      logoUrl: '',
    };
    const params = buildOgImageParams(config);
    expect(params.dealerLogoUrl).toBeUndefined();
  });

  it('should show contact name only when showContactName is true', () => {
    const config: OgImageConfig = {
      businessName: 'Test Dealer',
      contactName: 'John Doe',
      showContactName: true,
    };
    const params = buildOgImageParams(config);
    expect(params.contactName).toBe('John Doe');
  });

  it('should hide contact name when showContactName is false', () => {
    const config: OgImageConfig = {
      businessName: 'Test Dealer',
      contactName: 'John Doe',
      showContactName: false,
    };
    const params = buildOgImageParams(config);
    expect(params.contactName).toBeUndefined();
  });

  it('should show location only when hideAddress is false', () => {
    const config: OgImageConfig = {
      businessName: 'Test Dealer',
      city: 'Denver',
      state: 'CO',
      hideAddress: false,
    };
    const params = buildOgImageParams(config);
    expect(params.location).toBe('Denver, CO');
  });

  it('should hide location when hideAddress is true', () => {
    const config: OgImageConfig = {
      businessName: 'Test Dealer',
      city: 'Denver',
      state: 'CO',
      hideAddress: true,
    };
    const params = buildOgImageParams(config);
    expect(params.location).toBeUndefined();
  });

  it('should use "Official AMSOIL Dealer" as fallback business name', () => {
    const config: OgImageConfig = {};
    const params = buildOgImageParams(config);
    expect(params.businessName).toBe('Official AMSOIL Dealer');
  });
});
```

**Step 2: Run test to verify it fails**

Run: `cd /home/momentary/repos/amsoil-dlp && npm test -- og-image-generator --no-coverage`
Expected: FAIL with "Cannot find module '../og-image-generator'"

**Step 3: Write minimal implementation**

```typescript
// lib/og-image-generator.tsx
export interface OgImageConfig {
  businessName?: string | null;
  logoUrl?: string | null;
  contactName?: string | null;
  showContactName?: boolean | null;
  city?: string | null;
  state?: string | null;
  hideAddress?: boolean | null;
  heroImageUrl?: string | null;
}

export interface OgImageParams {
  showAmsoilLogo: boolean;
  dealerLogoUrl?: string;
  businessName: string;
  contactName?: string;
  location?: string;
  heroImageUrl: string;
}

const DEFAULT_HERO_IMAGE = '/images/slider-defaults/hero-1.jpg';
const DEFAULT_BUSINESS_NAME = 'Official AMSOIL Dealer';

export function buildOgImageParams(config: OgImageConfig): OgImageParams {
  const params: OgImageParams = {
    showAmsoilLogo: true,
    businessName: config.businessName || DEFAULT_BUSINESS_NAME,
    heroImageUrl: config.heroImageUrl || DEFAULT_HERO_IMAGE,
  };

  // Dealer logo - only if set
  if (config.logoUrl) {
    params.dealerLogoUrl = config.logoUrl;
  }

  // Contact name - only if showContactName is true
  if (config.showContactName && config.contactName) {
    params.contactName = config.contactName;
  }

  // Location - only if hideAddress is false
  if (!config.hideAddress && config.city && config.state) {
    params.location = `${config.city}, ${config.state}`;
  }

  return params;
}
```

**Step 4: Run test to verify it passes**

Run: `cd /home/momentary/repos/amsoil-dlp && npm test -- og-image-generator --no-coverage`
Expected: PASS (8 tests)

**Step 5: Commit**

```bash
git add lib/og-image-generator.tsx lib/__tests__/og-image-generator.test.tsx
git commit -m "feat(og): add OG image parameter builder with dealer settings support"
```

---

## Task 2: Add Hero Image URL Extraction

**Files:**
- Modify: `lib/og-image-generator.tsx`
- Test: `lib/__tests__/og-image-generator.test.tsx`

**Step 1: Write failing tests for hero image extraction**

```typescript
// Add to lib/__tests__/og-image-generator.test.tsx

describe('getHeroImageUrl', () => {
  it('should return first hero slider image when available', () => {
    const heroSlider = [
      { imageUrl: 'https://example.com/hero1.jpg' },
      { imageUrl: 'https://example.com/hero2.jpg' },
    ];
    const result = getHeroImageUrl(heroSlider, null);
    expect(result).toBe('https://example.com/hero1.jpg');
  });

  it('should return default hero when heroSlider is empty', () => {
    const result = getHeroImageUrl([], null);
    expect(result).toBe('/images/slider-defaults/hero-1.jpg');
  });

  it('should return default hero when heroSlider is null', () => {
    const result = getHeroImageUrl(null, null);
    expect(result).toBe('/images/slider-defaults/hero-1.jpg');
  });
});

describe('extractFirstImageFromPageContent', () => {
  it('should extract image URL from Puck content', () => {
    const pageContent = {
      content: [
        { type: 'Hero', props: { imageUrl: 'https://example.com/page-hero.jpg' } },
      ],
    };
    const result = extractFirstImageFromPageContent(pageContent);
    expect(result).toBe('https://example.com/page-hero.jpg');
  });

  it('should return null when no images in content', () => {
    const pageContent = {
      content: [
        { type: 'Text', props: { text: 'Hello world' } },
      ],
    };
    const result = extractFirstImageFromPageContent(pageContent);
    expect(result).toBeNull();
  });

  it('should find nested images', () => {
    const pageContent = {
      content: [
        {
          type: 'Section',
          props: {
            children: [
              { type: 'Image', props: { src: 'https://example.com/nested.jpg' } },
            ],
          },
        },
      ],
    };
    const result = extractFirstImageFromPageContent(pageContent);
    expect(result).toBe('https://example.com/nested.jpg');
  });
});
```

**Step 2: Run test to verify it fails**

Run: `cd /home/momentary/repos/amsoil-dlp && npm test -- og-image-generator --no-coverage`
Expected: FAIL with "getHeroImageUrl is not defined"

**Step 3: Write implementation**

```typescript
// Add to lib/og-image-generator.tsx

interface HeroSlide {
  imageUrl?: string;
  image?: string;
  src?: string;
}

export function getHeroImageUrl(
  heroSlider: HeroSlide[] | null | undefined,
  pageContent: unknown | null
): string {
  // Try page content first (for CMS pages)
  if (pageContent) {
    const pageImage = extractFirstImageFromPageContent(pageContent);
    if (pageImage) return pageImage;
  }

  // Then try hero slider
  if (heroSlider && heroSlider.length > 0) {
    const firstSlide = heroSlider[0];
    const imageUrl = firstSlide.imageUrl || firstSlide.image || firstSlide.src;
    if (imageUrl) return imageUrl;
  }

  // Fallback to default
  return DEFAULT_HERO_IMAGE;
}

export function extractFirstImageFromPageContent(content: unknown): string | null {
  if (!content || typeof content !== 'object') return null;

  const imageProps = ['imageUrl', 'image', 'src', 'backgroundImage'];

  function findImage(obj: unknown): string | null {
    if (!obj || typeof obj !== 'object') return null;

    const record = obj as Record<string, unknown>;

    // Check direct image properties
    for (const prop of imageProps) {
      if (typeof record[prop] === 'string' && record[prop]) {
        const url = record[prop] as string;
        if (url.startsWith('http') || url.startsWith('/')) {
          return url;
        }
      }
    }

    // Check props object
    if (record.props && typeof record.props === 'object') {
      const found = findImage(record.props);
      if (found) return found;
    }

    // Check children array
    if (Array.isArray(record.children)) {
      for (const child of record.children) {
        const found = findImage(child);
        if (found) return found;
      }
    }

    // Check content array
    if (Array.isArray(record.content)) {
      for (const item of record.content) {
        const found = findImage(item);
        if (found) return found;
      }
    }

    return null;
  }

  return findImage(content);
}
```

**Step 4: Run test to verify it passes**

Run: `cd /home/momentary/repos/amsoil-dlp && npm test -- og-image-generator --no-coverage`
Expected: PASS (all tests)

**Step 5: Commit**

```bash
git add lib/og-image-generator.tsx lib/__tests__/og-image-generator.test.tsx
git commit -m "feat(og): add hero image extraction from slider and page content"
```

---

## Task 3: Create OG Image React Component

**Files:**
- Create: `lib/og-image-component.tsx`
- Test: `lib/__tests__/og-image-component.test.tsx`

**Step 1: Write failing test for component rendering**

```typescript
// lib/__tests__/og-image-component.test.tsx
import { OgImageComponent } from '../og-image-component';
import { OgImageParams } from '../og-image-generator';

// Mock for testing component structure
describe('OgImageComponent', () => {
  it('should render without crashing', () => {
    const params: OgImageParams = {
      showAmsoilLogo: true,
      businessName: 'Test Dealer',
      heroImageUrl: '/images/slider-defaults/hero-1.jpg',
    };

    // Component should be a valid React element
    const element = OgImageComponent({ params, baseUrl: 'http://localhost:3000' });
    expect(element).toBeDefined();
    expect(element.type).toBe('div');
  });

  it('should include AMSOIL logo', () => {
    const params: OgImageParams = {
      showAmsoilLogo: true,
      businessName: 'Test Dealer',
      heroImageUrl: '/images/slider-defaults/hero-1.jpg',
    };

    const element = OgImageComponent({ params, baseUrl: 'http://localhost:3000' });
    const html = JSON.stringify(element);
    expect(html).toContain('amsoil-logo.png');
  });
});
```

**Step 2: Run test to verify it fails**

Run: `cd /home/momentary/repos/amsoil-dlp && npm test -- og-image-component --no-coverage`
Expected: FAIL with "Cannot find module '../og-image-component'"

**Step 3: Write implementation**

```typescript
// lib/og-image-component.tsx
import { OgImageParams } from './og-image-generator';

interface OgImageComponentProps {
  params: OgImageParams;
  baseUrl: string;
}

export function OgImageComponent({ params, baseUrl }: OgImageComponentProps) {
  const {
    showAmsoilLogo,
    dealerLogoUrl,
    businessName,
    contactName,
    location,
    heroImageUrl,
  } = params;

  // Resolve relative URLs to absolute
  const resolveUrl = (url: string) => {
    if (url.startsWith('http')) return url;
    return `${baseUrl}${url.startsWith('/') ? '' : '/'}${url}`;
  };

  const amsoilLogoUrl = resolveUrl('/images/logo/amsoil-logo.png');
  const backgroundUrl = resolveUrl(heroImageUrl);

  return (
    <div
      style={{
        width: '100%',
        height: '100%',
        display: 'flex',
        position: 'relative',
      }}
    >
      {/* Background Hero Image */}
      <img
        src={backgroundUrl}
        alt=""
        style={{
          position: 'absolute',
          top: 0,
          left: 0,
          width: '100%',
          height: '100%',
          objectFit: 'cover',
        }}
      />

      {/* Dark overlay for text contrast */}
      <div
        style={{
          position: 'absolute',
          top: 0,
          left: 0,
          width: '100%',
          height: '100%',
          background: 'linear-gradient(135deg, rgba(0,0,0,0.5) 0%, rgba(0,0,0,0.2) 50%, rgba(0,0,0,0.4) 100%)',
        }}
      />

      {/* Top-Left Stack: AMSOIL Logo, Dealer Logo, Business Name */}
      <div
        style={{
          position: 'absolute',
          top: '40px',
          left: '40px',
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'flex-start',
          gap: '12px',
        }}
      >
        {/* AMSOIL Logo */}
        {showAmsoilLogo && (
          <img
            src={amsoilLogoUrl}
            alt="AMSOIL"
            style={{
              height: '50px',
              filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.5))',
            }}
          />
        )}

        {/* Dealer Logo */}
        {dealerLogoUrl && (
          <img
            src={resolveUrl(dealerLogoUrl)}
            alt=""
            style={{
              maxHeight: '40px',
              maxWidth: '150px',
              filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.5))',
            }}
          />
        )}

        {/* Business Name */}
        <div
          style={{
            display: 'flex',
            padding: '8px 16px',
            backgroundColor: 'rgba(0,0,0,0.6)',
            borderRadius: '4px',
          }}
        >
          <span
            style={{
              color: 'white',
              fontSize: '28px',
              fontWeight: 'bold',
              textShadow: '0 2px 4px rgba(0,0,0,0.5)',
            }}
          >
            {businessName}
          </span>
        </div>
      </div>

      {/* Bottom-Right Stack: Contact Name, Location */}
      {(contactName || location) && (
        <div
          style={{
            position: 'absolute',
            bottom: '40px',
            right: '40px',
            display: 'flex',
            flexDirection: 'column',
            alignItems: 'flex-end',
            gap: '8px',
          }}
        >
          {/* Contact Name */}
          {contactName && (
            <div
              style={{
                display: 'flex',
                padding: '6px 12px',
                backgroundColor: 'rgba(0,0,0,0.6)',
                borderRadius: '4px',
              }}
            >
              <span
                style={{
                  color: 'white',
                  fontSize: '20px',
                  textShadow: '0 2px 4px rgba(0,0,0,0.5)',
                }}
              >
                {contactName}
              </span>
            </div>
          )}

          {/* Location Badge */}
          {location && (
            <div
              style={{
                display: 'flex',
                alignItems: 'center',
                gap: '6px',
                padding: '8px 16px',
                backgroundColor: 'rgba(0,0,0,0.6)',
                borderRadius: '4px',
              }}
            >
              <span style={{ fontSize: '18px' }}>📍</span>
              <span
                style={{
                  color: 'white',
                  fontSize: '22px',
                  fontWeight: '500',
                  textShadow: '0 2px 4px rgba(0,0,0,0.5)',
                }}
              >
                {location}
              </span>
            </div>
          )}
        </div>
      )}
    </div>
  );
}
```

**Step 4: Run test to verify it passes**

Run: `cd /home/momentary/repos/amsoil-dlp && npm test -- og-image-component --no-coverage`
Expected: PASS

**Step 5: Commit**

```bash
git add lib/og-image-component.tsx lib/__tests__/og-image-component.test.tsx
git commit -m "feat(og): add OG image React component with hero background and text overlays"
```

---

## Task 4: Create OG Image Cache Manager

**Files:**
- Create: `lib/og-cache.ts`
- Test: `lib/__tests__/og-cache.test.ts`

**Step 1: Write failing tests**

```typescript
// lib/__tests__/og-cache.test.ts
import { getOgCachePath, deleteOgCache, ogCacheExists } from '../og-cache';
import * as fs from 'fs/promises';
import * as path from 'path';

jest.mock('fs/promises');

describe('getOgCachePath', () => {
  it('should return path for dealer homepage', () => {
    const result = getOgCachePath('jacks-amsoil');
    expect(result).toBe('public/og-cache/jacks-amsoil.png');
  });

  it('should return path for CMS page', () => {
    const result = getOgCachePath('jacks-amsoil', 'services');
    expect(result).toBe('public/og-cache/jacks-amsoil/services.png');
  });

  it('should return path for blog post', () => {
    const result = getOgCachePath('jacks-amsoil', 'blog/my-post');
    expect(result).toBe('public/og-cache/jacks-amsoil/blog/my-post.png');
  });
});

describe('deleteOgCache', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('should delete single page cache file', async () => {
    (fs.unlink as jest.Mock).mockResolvedValue(undefined);

    await deleteOgCache('jacks-amsoil', 'services');

    expect(fs.unlink).toHaveBeenCalledWith('public/og-cache/jacks-amsoil/services.png');
  });

  it('should delete entire dealer cache folder', async () => {
    (fs.rm as jest.Mock).mockResolvedValue(undefined);

    await deleteOgCache('jacks-amsoil');

    expect(fs.rm).toHaveBeenCalledWith('public/og-cache/jacks-amsoil', { recursive: true, force: true });
    expect(fs.unlink).toHaveBeenCalledWith('public/og-cache/jacks-amsoil.png');
  });
});
```

**Step 2: Run test to verify it fails**

Run: `cd /home/momentary/repos/amsoil-dlp && npm test -- og-cache --no-coverage`
Expected: FAIL with "Cannot find module '../og-cache'"

**Step 3: Write implementation**

```typescript
// lib/og-cache.ts
import * as fs from 'fs/promises';
import * as path from 'path';

const OG_CACHE_DIR = 'public/og-cache';

/**
 * Get the cache file path for an OG image
 */
export function getOgCachePath(subdomain: string, slug?: string): string {
  if (slug) {
    return path.join(OG_CACHE_DIR, subdomain, `${slug}.png`);
  }
  return path.join(OG_CACHE_DIR, `${subdomain}.png`);
}

/**
 * Check if a cached OG image exists
 */
export async function ogCacheExists(subdomain: string, slug?: string): Promise<boolean> {
  try {
    await fs.access(getOgCachePath(subdomain, slug));
    return true;
  } catch {
    return false;
  }
}

/**
 * Save an OG image to the cache
 */
export async function saveOgCache(
  subdomain: string,
  imageBuffer: Buffer,
  slug?: string
): Promise<string> {
  const filePath = getOgCachePath(subdomain, slug);
  const dir = path.dirname(filePath);

  // Ensure directory exists
  await fs.mkdir(dir, { recursive: true });

  // Write image file
  await fs.writeFile(filePath, imageBuffer);

  return filePath;
}

/**
 * Delete cached OG image(s)
 * - If slug provided: delete single page cache
 * - If no slug: delete dealer homepage + entire dealer folder
 */
export async function deleteOgCache(subdomain: string, slug?: string): Promise<void> {
  if (slug) {
    // Delete single page cache
    try {
      await fs.unlink(getOgCachePath(subdomain, slug));
    } catch {
      // File may not exist, that's OK
    }
  } else {
    // Delete dealer homepage cache
    try {
      await fs.unlink(getOgCachePath(subdomain));
    } catch {
      // File may not exist, that's OK
    }

    // Delete entire dealer folder (all page caches)
    try {
      await fs.rm(path.join(OG_CACHE_DIR, subdomain), { recursive: true, force: true });
    } catch {
      // Folder may not exist, that's OK
    }
  }
}

/**
 * Get the public URL for a cached OG image
 */
export function getOgCacheUrl(subdomain: string, slug?: string): string {
  const filePath = getOgCachePath(subdomain, slug);
  // Remove 'public' prefix for URL
  return '/' + filePath.replace(/^public\//, '');
}
```

**Step 4: Run test to verify it passes**

Run: `cd /home/momentary/repos/amsoil-dlp && npm test -- og-cache --no-coverage`
Expected: PASS

**Step 5: Commit**

```bash
git add lib/og-cache.ts lib/__tests__/og-cache.test.ts
git commit -m "feat(og): add OG image cache manager for storing and deleting cached images"
```

---

## Task 5: Create OG Image Generation Service

**Files:**
- Create: `lib/og-image-service.ts`

**Step 1: Write the service that ties everything together**

```typescript
// lib/og-image-service.ts
import { ImageResponse } from 'next/og';
import { prisma } from './prisma';
import {
  buildOgImageParams,
  getHeroImageUrl,
  OgImageConfig,
} from './og-image-generator';
import { OgImageComponent } from './og-image-component';
import { saveOgCache, getOgCacheUrl, ogCacheExists } from './og-cache';
import { getSiteUrl } from './site-url';

interface GenerateOgImageOptions {
  subdomain: string;
  slug?: string;
  forceRegenerate?: boolean;
}

/**
 * Generate and cache an OG image for a dealer page
 */
export async function generateOgImage(options: GenerateOgImageOptions): Promise<string> {
  const { subdomain, slug, forceRegenerate = false } = options;

  // Check cache first (unless forcing regeneration)
  if (!forceRegenerate && (await ogCacheExists(subdomain, slug))) {
    return getOgCacheUrl(subdomain, slug);
  }

  // Fetch dealer data
  const dealer = await prisma.dealer.findFirst({
    where: { subdomain },
    select: {
      businessName: true,
      logoUrl: true,
      contactName: true,
      showContactName: true,
      city: true,
      state: true,
      hideAddress: true,
      heroSlider: true,
    },
  });

  if (!dealer) {
    throw new Error(`Dealer not found: ${subdomain}`);
  }

  // Fetch page content if this is a CMS page
  let pageContent = null;
  if (slug) {
    const page = await prisma.page.findFirst({
      where: {
        dealer: { subdomain },
        slug: slug.replace(/^blog\//, ''),
      },
      select: { content: true },
    });
    pageContent = page?.content;
  }

  // Build image configuration
  const heroImageUrl = getHeroImageUrl(
    dealer.heroSlider as Array<{ imageUrl?: string }> | null,
    pageContent
  );

  const config: OgImageConfig = {
    businessName: dealer.businessName,
    logoUrl: dealer.logoUrl,
    contactName: dealer.contactName,
    showContactName: dealer.showContactName,
    city: dealer.city,
    state: dealer.state,
    hideAddress: dealer.hideAddress,
    heroImageUrl,
  };

  const params = buildOgImageParams(config);
  const baseUrl = getSiteUrl().origin;

  // Generate image using ImageResponse
  const imageResponse = new ImageResponse(
    OgImageComponent({ params, baseUrl }),
    {
      width: 1200,
      height: 630,
    }
  );

  // Convert to buffer and save
  const arrayBuffer = await imageResponse.arrayBuffer();
  const buffer = Buffer.from(arrayBuffer);

  await saveOgCache(subdomain, buffer, slug);

  return getOgCacheUrl(subdomain, slug);
}

/**
 * Regenerate OG image for a dealer (homepage)
 * Call this when dealer settings change
 */
export async function regenerateDealerOgImage(subdomain: string): Promise<string> {
  return generateOgImage({ subdomain, forceRegenerate: true });
}

/**
 * Regenerate OG image for a specific page
 * Call this when a page is published
 */
export async function regeneratePageOgImage(subdomain: string, slug: string): Promise<string> {
  return generateOgImage({ subdomain, slug, forceRegenerate: true });
}
```

**Step 2: Commit**

```bash
git add lib/og-image-service.ts
git commit -m "feat(og): add OG image generation service that ties together all components"
```

---

## Task 6: Update Dealer OG Route to Serve Cached Images

**Files:**
- Modify: `app/dealers/[subdomain]/og/route.tsx`

**Step 1: Update the route to serve cached images or generate on demand**

```typescript
// app/dealers/[subdomain]/og/route.tsx
import { NextRequest, NextResponse } from 'next/server';
import * as fs from 'fs/promises';
import * as path from 'path';
import { generateOgImage } from '@/lib/og-image-service';
import { getOgCachePath, ogCacheExists } from '@/lib/og-cache';

export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ subdomain: string }> }
) {
  try {
    const { subdomain } = await params;

    // Check if cached image exists
    if (await ogCacheExists(subdomain)) {
      // Serve cached image
      const filePath = getOgCachePath(subdomain);
      const absolutePath = path.join(process.cwd(), filePath);
      const imageBuffer = await fs.readFile(absolutePath);

      return new NextResponse(imageBuffer, {
        headers: {
          'Content-Type': 'image/png',
          'Cache-Control': 'public, max-age=86400', // 24 hours
        },
      });
    }

    // Generate on demand if not cached
    await generateOgImage({ subdomain });

    // Read and serve the newly generated image
    const filePath = getOgCachePath(subdomain);
    const absolutePath = path.join(process.cwd(), filePath);
    const imageBuffer = await fs.readFile(absolutePath);

    return new NextResponse(imageBuffer, {
      headers: {
        'Content-Type': 'image/png',
        'Cache-Control': 'public, max-age=86400',
      },
    });
  } catch (error) {
    console.error('OG image generation failed:', error);

    return new NextResponse('Error generating image', {
      status: 500,
      headers: { 'Content-Type': 'text/plain' },
    });
  }
}
```

**Step 2: Commit**

```bash
git add app/dealers/[subdomain]/og/route.tsx
git commit -m "feat(og): update dealer OG route to serve cached images"
```

---

## Task 7: Add OG Route for CMS Pages

**Files:**
- Create: `app/dealers/[subdomain]/[...slug]/og/route.tsx`

**Step 1: Create the CMS page OG route**

```typescript
// app/dealers/[subdomain]/[...slug]/og/route.tsx
import { NextRequest, NextResponse } from 'next/server';
import * as fs from 'fs/promises';
import * as path from 'path';
import { generateOgImage } from '@/lib/og-image-service';
import { getOgCachePath, ogCacheExists } from '@/lib/og-cache';

export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ subdomain: string; slug: string[] }> }
) {
  try {
    const { subdomain, slug: slugParts } = await params;
    const slug = slugParts.join('/');

    // Check if cached image exists
    if (await ogCacheExists(subdomain, slug)) {
      const filePath = getOgCachePath(subdomain, slug);
      const absolutePath = path.join(process.cwd(), filePath);
      const imageBuffer = await fs.readFile(absolutePath);

      return new NextResponse(imageBuffer, {
        headers: {
          'Content-Type': 'image/png',
          'Cache-Control': 'public, max-age=86400',
        },
      });
    }

    // Generate on demand if not cached
    await generateOgImage({ subdomain, slug });

    const filePath = getOgCachePath(subdomain, slug);
    const absolutePath = path.join(process.cwd(), filePath);
    const imageBuffer = await fs.readFile(absolutePath);

    return new NextResponse(imageBuffer, {
      headers: {
        'Content-Type': 'image/png',
        'Cache-Control': 'public, max-age=86400',
      },
    });
  } catch (error) {
    console.error('OG image generation failed:', error);

    return new NextResponse('Error generating image', {
      status: 500,
      headers: { 'Content-Type': 'text/plain' },
    });
  }
}
```

**Step 2: Commit**

```bash
git add "app/dealers/[subdomain]/[...slug]/og/route.tsx"
git commit -m "feat(og): add OG route for CMS pages with page-specific hero images"
```

---

## Task 8: Hook OG Regeneration into Publish Flow

**Files:**
- Modify: `app/api/cms/publish/route.ts` (find existing publish endpoint)

**Step 1: Find and review publish endpoint**

Run: `grep -r "publish" app/api/cms/ --include="*.ts" -l`

**Step 2: Add OG regeneration call after successful publish**

Add import at top:
```typescript
import { regenerateDealerOgImage, regeneratePageOgImage } from '@/lib/og-image-service';
```

Add regeneration after publish succeeds (find the success response section):
```typescript
// After successful publish, regenerate OG image
try {
  if (pageSlug) {
    await regeneratePageOgImage(subdomain, pageSlug);
  } else {
    await regenerateDealerOgImage(subdomain);
  }
} catch (ogError) {
  // Log but don't fail the publish
  console.error('OG image regeneration failed:', ogError);
}
```

**Step 3: Commit**

```bash
git add app/api/cms/publish/route.ts
git commit -m "feat(og): regenerate OG images on publish"
```

---

## Task 9: Hook OG Deletion into Page/Dealer Delete

**Files:**
- Find: Page deletion endpoint
- Find: Dealer deletion/cancellation endpoint

**Step 1: Find deletion endpoints**

Run: `grep -r "delete" app/api/ --include="*.ts" -l | head -10`

**Step 2: Add OG cache deletion**

Add import where needed:
```typescript
import { deleteOgCache } from '@/lib/og-cache';
```

In page deletion:
```typescript
await deleteOgCache(subdomain, pageSlug);
```

In dealer deletion/cancellation:
```typescript
await deleteOgCache(subdomain); // Deletes all cached images for dealer
```

**Step 3: Commit**

```bash
git add [modified files]
git commit -m "feat(og): delete cached OG images when pages/dealers are deleted"
```

---

## Task 10: Update SEO Utils to Point to Cached Images

**Files:**
- Modify: `lib/seo-utils.ts`

**Step 1: Update getOgImageUrl to use cached path**

```typescript
// In lib/seo-utils.ts, update getOgImageUrl function:

export function getOgImageUrl(
  dealer: DealerSeoInfo,
  params?: OgImageParams
): string {
  const baseUrl = getCanonicalUrl(dealer);

  // For CMS pages, include the slug in the path
  if (params?.slug) {
    return `${baseUrl}/${params.slug}/og`;
  }

  // For dealer homepage
  return `${baseUrl}/og`;
}
```

**Step 2: Commit**

```bash
git add lib/seo-utils.ts
git commit -m "feat(og): update SEO utils to use new OG image routes"
```

---

## Task 11: Clean Up Old OG Implementation

**Files:**
- Delete: `lib/og-image.tsx` (old shared utility)
- Modify: `app/og/route.tsx` (root route - keep for main domain fallback)

**Step 1: Remove old og-image.tsx**

```bash
rm lib/og-image.tsx
rm lib/__tests__/og-image.test.tsx
```

**Step 2: Simplify root /og route to redirect or serve generic image**

```typescript
// app/og/route.tsx - simplified for main domain only
import { NextResponse } from 'next/server';
import * as fs from 'fs/promises';
import * as path from 'path';

export async function GET() {
  // Serve a generic AMSOIL branded image for main domain
  const imagePath = path.join(process.cwd(), 'public/images/og-default.png');

  try {
    const imageBuffer = await fs.readFile(imagePath);
    return new NextResponse(imageBuffer, {
      headers: {
        'Content-Type': 'image/png',
        'Cache-Control': 'public, max-age=86400',
      },
    });
  } catch {
    return new NextResponse('Image not found', { status: 404 });
  }
}
```

**Step 3: Commit**

```bash
git add -A
git commit -m "chore(og): clean up old OG implementation files"
```

---

## Task 12: Create Default OG Image Asset

**Files:**
- Create: `public/images/og-default.png`

**Step 1: Generate a static default OG image**

Use the OG image generator to create a generic AMSOIL branded image:
- AMSOIL logo centered
- Dark branded background
- "Official AMSOIL Dealer" text
- Save to `public/images/og-default.png`

**Step 2: Commit**

```bash
git add public/images/og-default.png
git commit -m "feat(og): add default OG image for main domain"
```

---

## Task 13: Manual Testing Checklist

**Test scenarios:**

1. **Dealer homepage OG** - Visit `http://test-growth.localhost:3001/og`
   - Should show hero image background
   - Should show AMSOIL logo
   - Should show business name
   - Should respect showContactName setting
   - Should respect hideAddress setting

2. **CMS page OG** - Create a test page, visit `/services/og`
   - Should pull first image from page content
   - Should fall back to hero slider if no page images

3. **Cache behavior**
   - First request generates and caches
   - Second request serves from cache (faster)
   - Publish regenerates cache

4. **Deletion behavior**
   - Delete page → OG cache file deleted
   - Cancel dealer → All OG cache files deleted

**Step: Run manual tests and fix any issues**

---

## Task 14: Final Commit and PR

**Step 1: Ensure all tests pass**

Run: `npm test -- --no-coverage`

**Step 2: Create final commit if needed**

```bash
git add -A
git commit -m "feat(og): complete OG image redesign with hero backgrounds and dealer settings"
```

**Step 3: Push and create PR**

```bash
git push -u origin fix/og-image-urls
```

Create PR with description summarizing the changes.
