Skip to main content
Documentation

Vue.js Open Graph Image Guide

Vue.js has come a long way. With Vue 3 and Nuxt 3, handling OG images is more elegant than ever. Whether you're building a simple SPA or a full-stack app with Nuxt, this guide shows you exactly how to make your social shares look professional.

This guide covers Vue 3 with Composition API. If you're using Nuxt 3, skip ahead to the Nuxt section—it has built-in features that make OG images trivial to implement.

The Vue Ecosystem: Your Options

Just like React, Vue itself doesn't handle meta tags. But the ecosystem has solid solutions. Here's the landscape:

SetupBest SolutionSEO Score
Nuxt 3useHead() composableExcellent
Vue 3 + Vite@unhead/vue + vite-ssgGood with SSG
Vue 3 SPAStatic HTML or prerenderRequires work

Nuxt 3: The Easy Path

If you're using Nuxt 3, you've already won. Nuxt has first-class support for meta tags through the useHead() and useSeoMeta() composables. Since Nuxt server-renders by default, crawlers see your meta tags immediately.

Global Configuration

// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      title: 'Your Site Name',
      meta: [
        { name: 'description', content: 'Your site description' },
        { property: 'og:type', content: 'website' },
        { property: 'og:site_name', content: 'Your Site Name' },
        { property: 'og:image', content: 'https://yoursite.com/og-image.png' },
        { property: 'og:image:width', content: '1200' },
        { property: 'og:image:height', content: '630' },
        { name: 'twitter:card', content: 'summary_large_image' },
      ],
    },
  },
})

Page-Specific Meta Tags

<!-- pages/about.vue -->
<script setup lang="ts">
useSeoMeta({
  title: 'About Us',
  ogTitle: 'About Us | Your Site Name',
  description: 'Learn about our mission and team.',
  ogDescription: 'Learn about our mission and team.',
  ogImage: 'https://yoursite.com/og-about.png',
  ogImageWidth: 1200,
  ogImageHeight: 630,
  twitterCard: 'summary_large_image',
})
</script>

<template>
  <div>
    <h1>About Us</h1>
    <!-- content -->
  </div>
</template>

The useSeoMeta() composable is type-safe and covers all standard OG and Twitter tags. Nuxt handles the server-rendering, so crawlers see everything correctly.

Dynamic Routes with Async Data

<!-- pages/blog/[slug].vue -->
<script setup lang="ts">
const route = useRoute()
const { data: post } = await useFetch(`/api/posts/${route.params.slug}`)

useSeoMeta({
  title: () => post.value?.title,
  ogTitle: () => `${post.value?.title} | Your Blog`,
  description: () => post.value?.excerpt,
  ogDescription: () => post.value?.excerpt,
  ogImage: () => `https://yoursite.com/og/blog/${route.params.slug}.png`,
  ogType: 'article',
  articlePublishedTime: () => post.value?.publishedAt,
  articleAuthor: () => post.value?.author,
})
</script>

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

Notice how we pass functions to useSeoMeta()? This makes the values reactive—they update as your data loads. Perfect for dynamic content.

Vue 3 + Vite: Static Site Generation

If you're not using Nuxt, you can still get excellent SEO with Vite's SSG plugins. The approach: use @unhead/vue for meta management and vite-ssg for pre-rendering at build time.

Installation

npm install @unhead/vue vite-ssg

Setup

// src/main.ts
import { ViteSSG } from 'vite-ssg'
import { createHead } from '@unhead/vue'
import App from './App.vue'
import routes from './routes'

export const createApp = ViteSSG(
  App,
  { routes },
  ({ app }) => {
    const head = createHead()
    app.use(head)
  }
)

Using in Components

<!-- src/pages/About.vue -->
<script setup lang="ts">
import { useHead, useSeoMeta } from '@unhead/vue'

useHead({
  title: 'About Us | Your Site',
})

useSeoMeta({
  description: 'Learn about our mission and team.',
  ogTitle: 'About Us | Your Site',
  ogDescription: 'Learn about our mission and team.',
  ogImage: 'https://yoursite.com/og-about.png',
  ogImageWidth: 1200,
  ogImageHeight: 630,
  twitterCard: 'summary_large_image',
})
</script>

<template>
  <div>
    <h1>About Us</h1>
  </div>
</template>

Build Configuration

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  ssgOptions: {
    script: 'async',
    formatting: 'minify',
    // Specify routes to pre-render
    includedRoutes(paths) {
      return paths.filter(p => !p.includes(':'))
    },
  },
})

When you build, vite-ssg generates static HTML for each route with all meta tags baked in. Crawlers get exactly what they need.

Plain Vue 3 SPA: The Manual Approach

Running a pure Vue SPA without SSG or Nuxt? You'll need to work a bit harder. Here's the most practical approach:

Static Meta Tags in index.html

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />

  <title>Your App Name</title>
  <meta name="description" content="Your app description" />

  <!-- Open Graph -->
  <meta property="og:type" content="website" />
  <meta property="og:url" content="https://yoursite.com" />
  <meta property="og:title" content="Your App Name" />
  <meta property="og:description" content="Your app description" />
  <meta property="og:image" content="https://yoursite.com/og-image.png" />
  <meta property="og:image:width" content="1200" />
  <meta property="og:image:height" content="630" />

  <!-- Twitter -->
  <meta name="twitter:card" content="summary_large_image" />
  <meta name="twitter:title" content="Your App Name" />
  <meta name="twitter:description" content="Your app description" />
  <meta name="twitter:image" content="https://yoursite.com/og-image.png" />
</head>
<body>
  <div id="app"></div>
  <script type="module" src="/src/main.ts"></script>
</body>
</html>

This works for your main pages. For dynamic routes, you'll need either pre-rendering or server-side handling. Honestly, at that point, consider Nuxt—it's built exactly for this.

Generating OG Images for Vue Apps

Now let's talk about the images themselves. You've got the same options as any other framework:

Option 1: Manual Creation

Use og-image.org to create images for each page. Drop them in your public folder and reference them in your meta tags.

Option 2: Build-Time Generation with Nuxt

Nuxt Image module can generate OG images at build time:

npm install @nuxt/image
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxt/image'],
  image: {
    // Configuration for OG image generation
  },
})

Option 3: Nuxt OG Image Module

For full dynamic generation, the nuxt-og-image module is incredibly powerful:

npm install nuxt-og-image
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['nuxt-og-image'],
})
<!-- pages/blog/[slug].vue -->
<script setup lang="ts">
defineOgImage({
  component: 'BlogPost',
  props: {
    title: post.value?.title,
    description: post.value?.excerpt,
    author: post.value?.author,
  },
})
</script>

The module generates images on-demand using Vue components. You design the OG image layout in Vue, and Nuxt handles the rendering. Same technology we use at og-image.org (Satori).

Complete Nuxt 3 Blog Example

Let's put together a complete blog with proper OG images:

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxt/content', 'nuxt-og-image'],

  app: {
    head: {
      titleTemplate: '%s | My Blog',
      meta: [
        { property: 'og:site_name', content: 'My Blog' },
        { name: 'twitter:site', content: '@yourusername' },
      ],
    },
  },

  ogImage: {
    defaults: {
      component: 'OgImageTemplate',
    },
  },
})
<!-- components/OgImageTemplate.vue -->
<script setup lang="ts">
defineProps<{
  title: string
  description?: string
  publishedAt?: string
}>()
</script>

<template>
  <div class="w-full h-full flex flex-col items-center justify-center bg-gradient-to-br from-gray-900 to-gray-800 p-16">
    <h1 class="text-6xl font-bold text-white text-center mb-8 leading-tight">
      {{ title }}
    </h1>
    <p v-if="description" class="text-2xl text-gray-300 text-center max-w-3xl">
      {{ description }}
    </p>
    <div v-if="publishedAt" class="absolute bottom-8 text-gray-500">
      {{ new Date(publishedAt).toLocaleDateString() }}
    </div>
  </div>
</template>
<!-- pages/blog/[slug].vue -->
<script setup lang="ts">
const route = useRoute()
const { data: post } = await useAsyncData(`blog-${route.params.slug}`, () =>
  queryContent('blog', route.params.slug as string).findOne()
)

if (!post.value) {
  throw createError({ statusCode: 404, message: 'Post not found' })
}

useSeoMeta({
  title: post.value.title,
  description: post.value.description,
  ogType: 'article',
  articlePublishedTime: post.value.publishedAt,
})

defineOgImage({
  props: {
    title: post.value.title,
    description: post.value.description,
    publishedAt: post.value.publishedAt,
  },
})
</script>

<template>
  <article class="prose prose-lg mx-auto py-12">
    <h1>{{ post.title }}</h1>
    <ContentRenderer :value="post" />
  </article>
</template>

Testing in Vue/Nuxt

Before deploying, verify your OG setup:

  1. Run nuxi generate and inspect the generated HTML files
  2. Check that each page has the correct og:image meta tag
  3. Use our validator to test how platforms see your pages
  4. If using nuxt-og-image, visit /__og-image__/image/your-path to see the generated image

Common Issues and Fixes

Meta tags not showing in source

If you're using SPA mode, crawlers won't see dynamic meta tags. Switch to SSR or SSG mode in your Nuxt config: ssr: true

OG images not updating on social platforms

Social platforms cache OG images aggressively. Use platform debuggers to force a refresh, or add a cache-busting query parameter during development.

Relative URLs not working

OG image URLs must be absolute. In Nuxt, set the site.url in your runtime config, and use useRuntimeConfig().public.siteUrl to build absolute URLs.

Summary

For Vue apps, Nuxt 3 is the path of least resistance. It handles SSR, meta tags, and even OG image generation out of the box. For plain Vue SPAs, use static images or add pre-rendering with vite-ssg.