March 29, 2026 · 10 min read

How to Add Dynamic OG Images to Nuxt.js with OGPeek API

Nuxt.js gives you SSR, static generation, and hybrid rendering out of the box. But generating unique Open Graph images for every page still means wrestling with server middleware, image libraries, or manual exports from Figma. Here is how to add dynamic, branded social cards to any Nuxt app in under five minutes—using a single API URL, zero npm packages, and Nuxt's built-in useHead and useSeoMeta composables.

Dynamic OG image for Nuxt.js sites generated by OGPeek API

Why Nuxt.js Needs Dynamic OG Images

When someone shares a URL from your Nuxt app on Twitter/X, LinkedIn, Slack, or Discord, the platform's crawler fetches the HTML, reads the og:image meta tag, and requests that image to display as a preview card. If every page points to the same static image—or worse, has no og:image at all—your links blend into the feed and get fewer clicks.

This matters because social preview cards are the first impression of your content. A blog post titled "Building a Multi-Tenant SaaS with Nuxt 3" deserves a social card that says exactly that—not a generic logo or a broken image placeholder.

The traditional approaches each introduce friction that Nuxt developers should not have to deal with:

There is a simpler approach: point your og:image meta tag at an external API that returns a PNG. Your Nuxt app stays lean. The image generation happens somewhere else entirely.

How OGPeek Works

OGPeek is an OG image generation API. You construct a URL with your title, subtitle, template, and brand color as query parameters. When that URL is requested, OGPeek renders the image and returns a 1200×630 PNG—the standard size for Open Graph images across all major platforms.

GET https://ogpeek.com/api/v1/og
  ?title=Your+Page+Title
  &subtitle=yourdomain.com
  &template=gradient
  &theme=midnight
  &brandColor=%23FF7A00

That is the entire integration. A GET request returns a PNG. No API keys for the free tier, no SDK to install, no Nuxt module to configure. You embed this URL in your page's meta tags using useHead or useSeoMeta and every page gets a unique social card based on its title.

Key point: Social platform crawlers follow the URL in your og:image meta tag and fetch whatever is there. They cannot distinguish between a static PNG on your CDN and an API endpoint that generates a PNG on the fly. Both are just HTTP responses with Content-Type: image/png.

Step 1: Set Global Defaults in nuxt.config.ts

Nuxt's app.head configuration in nuxt.config.ts lets you define default meta tags that apply to every page in your application. This is the ideal place to set fallback OG tags for pages that do not define their own.

// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      title: 'Your Site Name',
      meta: [
        { name: 'description', content: 'Your site description.' },
        { property: 'og:site_name', content: 'Your Site Name' },
        { property: 'og:type', content: 'website' },
        {
          property: 'og:image',
          content: 'https://ogpeek.com/api/v1/og'
            + '?title=Your+Site+Name'
            + '&subtitle=yourdomain.com'
            + '&template=gradient'
            + '&theme=midnight'
            + '&brandColor=%23FF7A00'
        },
        { property: 'og:image:width', content: '1200' },
        { property: 'og:image:height', content: '630' },
        { name: 'twitter:card', content: 'summary_large_image' }
      ]
    }
  }
})

This gives your site a branded default OG image. Any page that does not explicitly set its own og:image will fall back to this one. Pages that call useHead or useSeoMeta with their own OG tags will override these defaults automatically—Nuxt handles the merge for you.

Step 2: Dynamic OG Images with useHead

Nuxt 3 provides the useHead composable for setting head elements reactively inside any component or page. This is the most direct way to add a dynamic OG image to an individual page.

For a static page like an About page, the integration looks like this:

<!-- pages/about.vue -->
<script setup>
const title = 'About Us'
const description = 'Learn about our team and mission.'

const ogImage = `https://ogpeek.com/api/v1/og?title=${
  encodeURIComponent(title)
}&subtitle=${
  encodeURIComponent('yourdomain.com')
}&template=gradient&theme=midnight&brandColor=%23FF7A00`

useHead({
  title,
  meta: [
    { name: 'description', content: description },
    { property: 'og:title', content: title },
    { property: 'og:description', content: description },
    { property: 'og:image', content: ogImage },
    { property: 'og:image:width', content: '1200' },
    { property: 'og:image:height', content: '630' },
    { name: 'twitter:card', content: 'summary_large_image' },
    { name: 'twitter:title', content: title },
    { name: 'twitter:image', content: ogImage }
  ]
})
</script>

<template>
  <div>
    <h1>{{ title }}</h1>
    <p>{{ description }}</p>
  </div>
</template>

That is the complete integration for a single page. The og:image meta tag points to OGPeek, which generates a 1200×630 PNG with "About Us" as the title text. Zero dependencies added to your project.

For dynamic routes like a blog at /blog/[slug], use useAsyncData or useFetch to load your content and then pass the data into useHead:

<!-- pages/blog/[slug].vue -->
<script setup>
const route = useRoute()

const { data: post } = await useFetch(
  `/api/posts/${route.params.slug}`
)

const ogImage = computed(() =>
  `https://ogpeek.com/api/v1/og?title=${
    encodeURIComponent(post.value?.title || '')
  }&subtitle=${
    encodeURIComponent('Blog · yourdomain.com')
  }&template=gradient&theme=midnight&brandColor=%23FF7A00`
)

useHead({
  title: () => `${post.value?.title} — Your Site`,
  meta: [
    { name: 'description', content: () => post.value?.excerpt },
    { property: 'og:title', content: () => post.value?.title },
    { property: 'og:description', content: () => post.value?.excerpt },
    { property: 'og:type', content: 'article' },
    { property: 'og:image', content: () => ogImage.value },
    { property: 'og:image:width', content: '1200' },
    { property: 'og:image:height', content: '630' },
    { name: 'twitter:card', content: 'summary_large_image' },
    { name: 'twitter:title', content: () => post.value?.title },
    { name: 'twitter:image', content: () => ogImage.value }
  ]
})
</script>

<template>
  <article v-if="post">
    <h1>{{ post.title }}</h1>
    <p>{{ post.excerpt }}</p>
    <div v-html="post.body" />
  </article>
</template>

Every blog post now gets a branded social card with its own title. A post called "Building Multi-Tenant SaaS with Nuxt 3" produces a social card with that exact text. No manual image creation, no server-side rendering of images on your infrastructure.

Tip: Notice the use of computed() for the OG image URL and arrow functions for reactive meta values. This ensures the meta tags update reactively when the fetched data resolves. Nuxt's useHead supports reactive values natively.

Step 3: Cleaner Syntax with useSeoMeta

Nuxt 3 also provides useSeoMeta—a composable specifically designed for SEO meta tags. Instead of manually specifying property and content for each tag, you pass a flat object with typed keys. This is the cleanest way to handle OG tags in Nuxt.

<!-- pages/pricing.vue -->
<script setup>
const title = 'Pricing'
const description = 'Simple, transparent pricing for teams of all sizes.'

const ogImage = `https://ogpeek.com/api/v1/og?title=${
  encodeURIComponent(title)
}&subtitle=${
  encodeURIComponent('yourdomain.com')
}&template=gradient&theme=midnight&brandColor=%23FF7A00`

useSeoMeta({
  title,
  description,
  ogTitle: title,
  ogDescription: description,
  ogImage: ogImage,
  ogImageWidth: 1200,
  ogImageHeight: 630,
  twitterCard: 'summary_large_image',
  twitterTitle: title,
  twitterImage: ogImage
})
</script>

<template>
  <div>
    <h1>Pricing</h1>
    <!-- pricing content -->
  </div>
</template>

The useSeoMeta composable produces identical HTML output to useHead with meta arrays, but the code is shorter and easier to read. The keys like ogTitle, ogImage, and twitterCard are fully typed—your IDE will autocomplete them and catch typos at development time.

For dynamic pages, use useServerSeoMeta to ensure the tags are rendered during SSR (important for social crawlers that do not execute JavaScript):

<script setup>
const { data: post } = await useFetch(`/api/posts/${useRoute().params.slug}`)

useServerSeoMeta({
  title: () => post.value?.title,
  description: () => post.value?.excerpt,
  ogTitle: () => post.value?.title,
  ogDescription: () => post.value?.excerpt,
  ogImage: () => `https://ogpeek.com/api/v1/og?title=${
    encodeURIComponent(post.value?.title || '')
  }&subtitle=${
    encodeURIComponent('yourdomain.com')
  }&template=gradient&theme=midnight&brandColor=%23FF7A00`,
  ogImageWidth: 1200,
  ogImageHeight: 630,
  twitterCard: 'summary_large_image',
  twitterTitle: () => post.value?.title,
  twitterImage: () => `https://ogpeek.com/api/v1/og?title=${
    encodeURIComponent(post.value?.title || '')
  }&template=gradient&theme=midnight&brandColor=%23FF7A00`
})
</script>

Reusable Composable Pattern: useOGImage

If your Nuxt app has many pages—a blog, a documentation site, product pages, landing pages—you will end up repeating the same OGPeek URL construction logic everywhere. The solution is to extract it into a reusable composable.

Create a useOGImage composable in your composables/ directory (Nuxt auto-imports these):

// composables/useOGImage.ts
interface OGImageOptions {
  title: string
  description?: string
  subtitle?: string
  template?: 'basic' | 'gradient' | 'split' | 'hero'
  theme?: 'dark' | 'light' | 'midnight' | 'forest' | 'sunset'
  brandColor?: string
}

export function useOGImage(options: OGImageOptions) {
  const {
    title,
    description = '',
    subtitle = 'yourdomain.com',
    template = 'gradient',
    theme = 'midnight',
    brandColor = '#FF7A00'
  } = options

  const ogImageUrl = `https://ogpeek.com/api/v1/og?title=${
    encodeURIComponent(title)
  }&subtitle=${
    encodeURIComponent(subtitle)
  }&template=${template}&theme=${theme}&brandColor=${
    encodeURIComponent(brandColor)
  }`

  useSeoMeta({
    title,
    description,
    ogTitle: title,
    ogDescription: description,
    ogImage: ogImageUrl,
    ogImageWidth: 1200,
    ogImageHeight: 630,
    twitterCard: 'summary_large_image',
    twitterTitle: title,
    twitterImage: ogImageUrl
  })

  return { ogImageUrl }
}

Now every page in your Nuxt app can set its OG image with a single function call:

<!-- pages/features.vue -->
<script setup>
useOGImage({
  title: 'Features',
  description: 'Everything you need to build modern web apps.',
  subtitle: 'yourdomain.com'
})
</script>

<template>
  <div>
    <h1>Features</h1>
  </div>
</template>

One line. One composable. Every page gets a unique, branded social card. The composable handles URL construction, encoding, and all the meta tags. If you change your brand color or template, you update one file and every page reflects the change.

For dynamic pages, pass reactive data into the composable using a watch or compute the values before calling it:

<!-- pages/docs/[section].vue -->
<script setup>
const route = useRoute()
const { data: section } = await useFetch(
  `/api/docs/${route.params.section}`
)

useOGImage({
  title: section.value?.title || 'Documentation',
  description: section.value?.summary || '',
  subtitle: 'Docs · yourdomain.com'
})
</script>

OGPeek vs. Nuxt Image Module vs. nuxt-og-image vs. Manual

Here is how the main approaches compare for Nuxt.js projects that need dynamic social cards:

Feature OGPeek API Nuxt Image Module nuxt-og-image Manual (Figma)
Setup time 2 minutes N/A (not for OG) 15–45 minutes 5–10 min per image
Dependencies added 0 @nuxt/image nuxt-og-image + Satori 0
Dynamic text on images Yes (via URL params) No Yes (Vue components) No
Server load Zero (external API) Image optimization only Renders on your server Zero
Works with nuxt generate (SSG) Yes Yes Yes (prerender step) Yes
Build time impact None Minimal Adds prerender time None
Custom CSS layout Pre-built templates N/A Full control (Vue) Full control
Hosting lock-in None None Needs SSR or prerender None
Scales to 1000+ pages Yes N/A Depends on build/server No
Maintenance burden None Module updates Module + Satori updates Per-page manual effort

The Nuxt Image module (@nuxt/image) is excellent for optimizing and resizing content images, but it is not designed to generate OG social cards with dynamic text. It solves a different problem entirely.

The nuxt-og-image module is the most feature-rich option if you need pixel-perfect custom layouts defined as Vue components. However, it requires Satori, adds build complexity, and renders images on your server or during the prerender step. For most projects that need branded social cards with a title, subtitle, and accent color, OGPeek delivers the same result with zero configuration.

Customizing Your OG Images

OGPeek supports several parameters to match your brand:

For a Nuxt project, you might use Nuxt's signature green as the brand color:

const ogImage = `https://ogpeek.com/api/v1/og?title=${
  encodeURIComponent(title)
}&subtitle=${
  encodeURIComponent('Built with Nuxt · yourdomain.com')
}&template=gradient&theme=dark&brandColor=%2300DC82`

Testing Your OG Images

After deploying your Nuxt app, verify your OG images work correctly:

  1. View the page source and confirm the og:image meta tag contains a valid OGPeek URL with your page title encoded in it. In Nuxt SSR mode, the meta tags are rendered in the initial HTML response.
  2. Open the OGPeek URL directly in your browser. You should see a 1200×630 PNG with your title rendered on it.
  3. Use a debugger tool like the Twitter Card Validator or Facebook Sharing Debugger to see exactly what the platform will display.
  4. Share a link in a private Slack channel or Discord server to see the preview card in a real context.

If the image does not appear, check that encodeURIComponent() is encoding special characters correctly. Ampersands, quotes, and hash symbols in titles can break the URL if left unencoded. Also verify that your Nuxt app renders the meta tags server-side—most social crawlers do not execute JavaScript.

SSR is critical: Social platform crawlers (Facebook, Twitter/X, LinkedIn, Slack) do not run JavaScript. If you are using Nuxt in SPA mode, your OG meta tags will not be visible to crawlers. Use SSR (ssr: true in nuxt.config.ts, which is the default) or prerender your pages with nuxt generate to ensure crawlers can read the tags.

Performance Considerations

OGPeek caches generated images at the CDN edge. The first request for a given URL renders the image and caches it. Subsequent requests—including from different social platforms sharing the same link—are served from the cache with sub-50ms response times.

Since the og:image URL is only fetched by social platform crawlers (not by your site's actual visitors in their browsers), there is zero impact on your Nuxt app's performance metrics. Core Web Vitals, Lighthouse scores, and page load times are completely unaffected. Nuxt's built-in performance optimizations—code splitting, lazy hydration, payload extraction—remain untouched.

Frequently Asked Questions

Does OGPeek work with Nuxt.js static site generation (nuxt generate)?

Yes. OGPeek works perfectly with nuxt generate and SSG mode. The og:image meta tag contains a URL to OGPeek's API, so the image is generated externally when a social platform requests it. Your Nuxt site can be fully prerendered to static HTML and hosted anywhere—Netlify, Cloudflare Pages, Firebase Hosting, or any static file server.

Do I need to install any npm packages to use OGPeek with Nuxt?

No. OGPeek requires zero npm packages. You use Nuxt's built-in useHead or useSeoMeta composables to add an og:image meta tag pointing to an OGPeek URL. There is nothing to install, configure, or maintain in your project dependencies.

How is OGPeek different from the nuxt-og-image module?

The nuxt-og-image module renders OG images on your own server using Satori and requires server-side rendering or a prerender step. It adds build complexity, configuration files, and a dependency on Satori's CSS subset. OGPeek is an external API—you pass a URL with your title as a query parameter and get a PNG back. No server-side rendering of images, no build step, no module configuration.

Can I use useHead and useSeoMeta together for OG images in Nuxt?

Yes. Nuxt's useHead and useSeoMeta composables can be used together in the same component. useSeoMeta provides a cleaner, flattened API specifically for SEO meta tags, while useHead handles the full range of head elements including title, link, and script tags. Both support reactivity and will merge correctly.

What image size does OGPeek return for Nuxt social cards?

OGPeek returns 1200×630 PNG images by default, which is the recommended size for Open Graph images across Facebook, LinkedIn, Twitter/X, Slack, Discord, and other platforms that render link previews. This size is standard regardless of which framework you use.

Start generating OG images for your Nuxt.js app

Read the full API documentation, explore templates, and grab your integration URL. Free tier available—no signup required.

Get started free → Read the API docs

Conclusion

Nuxt.js makes building modern web applications straightforward with its file-based routing, auto-imports, and flexible rendering modes. Adding dynamic OG images should be equally straightforward—and with OGPeek, it is.

Set global defaults in nuxt.config.ts. Override per-page with useHead or useSeoMeta. Extract the pattern into a useOGImage composable so every page gets a branded social card in a single line. No npm packages. No headless browsers. No server-side image rendering on your infrastructure.

Whether you deploy with SSR, SSG, or hybrid rendering, OGPeek works the same way: a GET request returns a PNG. Your Nuxt app stays lean. Your social cards stay dynamic.

More developer APIs from the Peek Suite

← Back to all articles