# Next.js

## Getting Started

## Create a connection

1. Sign in to [OpenGraph+](/)
2. Go to your website's **Meta Tags** page
3. Create a new connection and copy your connection URL

Your connection URL looks like `https://$OGPLUS_KEY.ogplus.net`.

## Find your layout

**App Router:** `app/layout.tsx` is the root layout. All pages inherit its metadata.

**Pages Router:** `pages/_app.tsx` wraps every page.

## Add Open Graph meta tags

Use `generateMetadata` in your root layout to set default Open Graph tags site-wide. These control how your pages appear when shared on Twitter, Slack, LinkedIn, and other platforms.

```tsx
// app/layout.tsx
import type { Metadata } from 'next'
import { headers } from 'next/headers'

export async function generateMetadata(): Promise<Metadata> {
  const headersList = await headers()
  const pathname = headersList.get('x-invoke-path') || '/'

  return {
    openGraph: {
      title: 'My Site',
      description: 'Welcome to my site',
      url: `https://mysite.com${pathname}`,
      siteName: 'My Site',
      type: 'website',
      images: [`https://$OGPLUS_KEY.ogplus.net${pathname}`],
    },
    twitter: {
      card: 'summary_large_image',
    },
  }
}
```

Replace `https://$OGPLUS_KEY.ogplus.net` with your connection URL, and `"My Site"` with your actual site name.

### What each tag does

| Tag | Purpose |
|-----|---------|
| `og:title` | The title shown in the link preview |
| `og:description` | The description shown below the title |
| `og:url` | The canonical URL of the page |
| `og:site_name` | Your website's name |
| `og:type` | Content type (`website`, `article`, `product`) |
| `og:image` | The social card image (powered by OpenGraph+) |
| `twitter:card` | Tells Twitter to show a large image card |

## Dynamic tags for pages

Page-level `generateMetadata` overrides the layout defaults. Use this to set tags from your data.

### Blog posts

```tsx
// app/posts/[slug]/page.tsx
import { getPost } from '@/lib/posts'

export async function generateMetadata({ params }) {
  const post = await getPost(params.slug)

  return {
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: 'article',
      images: [`https://$OGPLUS_KEY.ogplus.net/posts/${params.slug}`],
    },
    twitter: {
      card: 'summary_large_image',
    },
  }
}
```

### Products

```tsx
// app/products/[id]/page.tsx
import { getProduct } from '@/lib/products'

export async function generateMetadata({ params }) {
  const product = await getProduct(params.id)

  return {
    openGraph: {
      title: product.name,
      description: `${product.price} — ${product.description}`,
      type: 'product',
      images: [`https://$OGPLUS_KEY.ogplus.net/products/${params.id}`],
    },
    twitter: {
      card: 'summary_large_image',
    },
  }
}
```

## Static metadata for simple pages

For pages with known paths, use the static `metadata` export:

```tsx
// app/about/page.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  openGraph: {
    title: 'About Us',
    description: 'Learn more about our company',
    images: ['https://$OGPLUS_KEY.ogplus.net/about'],
  },
  twitter: {
    card: 'summary_large_image',
  },
}
```

## Pages Router (legacy)

If you're using the Pages Router, add meta tags via `next/head`:

```tsx
// pages/_app.tsx
import Head from 'next/head'
import { useRouter } from 'next/router'

export default function App({ Component, pageProps }) {
  const router = useRouter()

  return (
    <>
      <Head>
        <meta property="og:title" content="My Site" />
        <meta property="og:description" content="Welcome to my site" />
        <meta property="og:url" content={`https://mysite.com${router.asPath}`} />
        <meta property="og:site_name" content="My Site" />
        <meta property="og:type" content="website" />
        <meta property="og:image" content={`https://$OGPLUS_KEY.ogplus.net${router.asPath}`} />
        <meta name="twitter:card" content="summary_large_image" />
      </Head>
      <Component {...pageProps} />
    </>
  )
}
```

Override tags in individual pages by adding another `<Head>` block with page-specific values.

## Verify

Start your development server and view the page source. Check that all `og:` meta tags are present with the correct values.

Open the preview tool in your OpenGraph+ dashboard and paste a URL from your site to see the social card image.


## Customize

OpenGraph+ captures your page in a headless browser and renders it as an image. You control what gets captured using meta tags in your layout and CSS in your stylesheets.

All of the rendering options below are standard HTML meta tags and CSS, so they work the same regardless of framework. The [HTML, CSS, & HTTP guide](/docs/html-css) covers each one in detail. This page shows how to wire them into a Next.js app.

## Meta tags

Add these to your layout's metadata alongside your `og:image` tag. They're all optional.

```tsx
// app/layout.tsx
export async function generateMetadata(): Promise<Metadata> {
  const headersList = await headers()
  const pathname = headersList.get('x-invoke-path') || '/'

  return {
    openGraph: {
      images: [`https://$OGPLUS_KEY.ogplus.net${pathname}`],
    },
    twitter: {
      card: 'summary_large_image',
    },
    other: {
      // Render at 800px wide instead of the default
      'og:plus:viewport:width': '800',
      // Only capture this element instead of the full page
      'og:plus:selector': '.post-header',
      // Inject inline styles on the captured element
      'og:plus:style': 'padding: 60px; background: #0f172a; color: white;',
    },
  }
}
```

See the [Rendering](/docs/html-css/rendering) guide for what each meta tag does and how they interact.

## CSS styling

OpenGraph+ adds a `data-ogplus` attribute to your `<html>` element during capture. Use it in your stylesheets to hide navigation, adjust spacing, or restyle anything for the social card without affecting your actual site.

```css
/* app/globals.css */
html[data-ogplus] {
  nav { display: none; }
  footer { display: none; }
  .hero { padding: 60px; }
}
```

If you're using Tailwind, there's a plugin that gives you `ogplus:` variants like `ogplus:hidden` and `ogplus-twitter:bg-sky-500`. See [CSS Styling](/docs/html-css/data-attributes) for plain CSS examples and [Tailwind setup](/docs/html-css/data-attributes#tailwind-css).

## Templates

For fully custom social card layouts that pull content from your page, use `<template>` elements. These let you build a completely different layout for screenshots without touching your visible page.

```tsx
// app/layout.tsx (inside the <body>)
<template id="ogplus" dangerouslySetInnerHTML={{ __html: `
  <div style="padding: 48px; background: #0f172a; color: white; height: 100%;">
    <h1 style="font-size: 48px;">
      \${document.querySelector('h1')?.textContent}
    </h1>
  </div>
` }} />
```

See the [Templates](/docs/html-css/templates) guide for expression syntax, platform-specific templates, and full examples.

## Testing locally

The [Preview Bookmarklet](/docs/html-css/bookmarklet) sets the `data-ogplus` attribute in your browser so you can see how your CSS and Tailwind variants look without deploying or waiting for a real crawler to hit your page.

## Full example

A blog post page with all the pieces together:

```tsx
export async function generateMetadata({ params }): Promise<Metadata> {
  const post = await getPost(params.slug)

  return {
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: 'article',
      images: [`https://$OGPLUS_KEY.ogplus.net/posts/${params.slug}`],
    },
    twitter: {
      card: 'summary_large_image',
    },
    other: {
      'og:plus:selector': '.post-header',
      'og:plus:style': 'padding: 60px; background-color: #0f172a; color: white;',
      'og:plus:viewport:width': '800',
    },
  }
}
```


## Caching

OpenGraph+ reads HTTP cache headers from your responses to decide when to re-render social card images. Next.js gives you several ways to control these headers.

This page covers the Next.js side. For how OpenGraph+ handles caching at the HTTP level, see the [HTTP Caching](/docs/html-css/caching) guide.

## Route segment config

Set `revalidate` in a page or layout to control how long content is cached:

```tsx
// app/blog/[slug]/page.tsx

// Revalidate every hour
export const revalidate = 3600
```

This tells Next.js (and OpenGraph+) that the content is valid for one hour before revalidation.

## Custom headers in next.config.js

Set `Cache-Control` headers across routes:

```js
// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/blog/:path*',
        headers: [
          { key: 'Cache-Control', value: 'public, max-age=86400' },
        ],
      },
      {
        source: '/',
        headers: [
          { key: 'Cache-Control', value: 'public, max-age=3600' },
        ],
      },
    ]
  },
}
```

## Route handlers

For API routes or route handlers that generate responses:

```ts
// app/api/og/route.ts
export async function GET() {
  return new Response('...', {
    headers: {
      'Cache-Control': 'public, max-age=3600',
    },
  })
}
```

## Meta tag overrides

If you can't control HTTP headers (e.g., static hosting that strips custom headers), use meta tags instead:

```tsx
export const metadata: Metadata = {
  other: {
    'og:plus:cache:max_age': '86400',
    'og:plus:cache:etag': 'v1-about-page',
  },
}
```

See the [HTTP Caching guide](/docs/html-css/caching) for the full list of meta tag overrides.

## Purging cached images

When you need to force a refresh immediately, go to your website dashboard, find the page, and click purge. This clears the cached image and triggers a re-render on the next request.

## Recommendations

| Page type | Strategy |
|-----------|----------|
| Static pages (about, contact) | `Cache-Control: public, max-age=604800` (1 week) |
| Blog posts | `Cache-Control: public, max-age=86400` (1 day) |
| Index / listing pages | `Cache-Control: public, max-age=3600` (1 hour) |
| Dynamic / user content | No caching or short TTL |


## Troubleshooting

Something not working? Here are the most common issues and how to fix them.

## Meta tags not appearing

Client Components don't render meta tags on the server. Social crawlers (and OpenGraph+) only see server-rendered HTML.

Make sure your meta tags are set via:
- `generateMetadata` (async, dynamic)
- The static `metadata` export
- Both are server-side only

If you're using the Pages Router, use `next/head` in `_app.tsx` or individual pages.

**Check:** View your page source, not DevTools. DevTools shows client-hydrated HTML. Page source shows what crawlers see.

## Wrong image showing

This is almost always a caching issue. Social platforms and OpenGraph+ both cache images.

1. Open the preview tool in your OpenGraph+ dashboard
2. Paste your URL to see what OpenGraph+ currently has
3. If the image is stale, purge it from the dashboard
4. Re-check with the preview tool

## Image shows wrong page content

Your pathname logic in `generateMetadata` may not be resolving correctly. Double-check:

1. The `x-invoke-path` header is available in your environment
2. Your middleware is forwarding the correct path
3. The connection URL plus pathname matches your actual page URL

Test by hardcoding a known path and checking if the correct image appears.

## Social platforms not updating

Twitter, LinkedIn, and Slack cache images aggressively on their end. After confirming the correct image appears in the OpenGraph+ preview tool:

- **Twitter:** Use the [Card Validator](https://cards-dev.twitter.com/validator) to force a refresh
- **LinkedIn:** Use the [Post Inspector](https://www.linkedin.com/post-inspector/) to clear their cache
- **Facebook:** Use the [Sharing Debugger](https://developers.facebook.com/tools/debug/) to scrape again

This is platform-side caching that OpenGraph+ cannot control.

## Purging cached images

1. Go to your website dashboard in OpenGraph+
2. Find the page you want to refresh
3. Click purge to clear the cached image

The next request from a social platform will trigger a fresh render.

## Testing

Use the preview tool in your OpenGraph+ dashboard to verify your setup before sharing URLs. This shows you exactly what social platforms will see.

