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:
| Setup | Best Solution | SEO Score |
|---|---|---|
| Nuxt 3 | useHead() composable | Excellent |
| Vue 3 + Vite | @unhead/vue + vite-ssg | Good with SSG |
| Vue 3 SPA | Static HTML or prerender | Requires 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-ssgSetup
// 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:
- Run
nuxi generateand inspect the generated HTML files - Check that each page has the correct og:image meta tag
- Use our validator to test how platforms see your pages
- If using nuxt-og-image, visit
/__og-image__/image/your-pathto 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.