Appearance
Vue.js Integration
Generate OG images for Vue.js and Nuxt applications.
Nuxt 3 (Recommended)
Setup
bash
# No additional packages neededEnvironment Variable
bash
# .env
NUXT_OG_IMAGE_API_KEY=og_your_api_key_hereServer Route
javascript
// server/api/og.get.js
export default defineEventHandler(async (event) => {
const query = getQuery(event);
const config = useRuntimeConfig();
const response = await fetch('https://ogimageapi.io/api/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': config.ogImageApiKey
},
body: JSON.stringify({
title: query.title || 'My Site',
subtitle: query.subtitle || '',
template: query.template || 'default',
theme: 'dark'
})
});
const buffer = await response.arrayBuffer();
setResponseHeaders(event, {
'Content-Type': 'image/png',
'Cache-Control': 'public, max-age=86400'
});
return Buffer.from(buffer);
});Nuxt Config
javascript
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
ogImageApiKey: process.env.NUXT_OG_IMAGE_API_KEY
}
});Using useSeoMeta
vue
<!-- pages/blog/[slug].vue -->
<script setup>
const route = useRoute();
const { data: post } = await useFetch(`/api/posts/${route.params.slug}`);
useSeoMeta({
title: post.value.title,
description: post.value.excerpt,
ogTitle: post.value.title,
ogDescription: post.value.excerpt,
ogImage: `/api/og?title=${encodeURIComponent(post.value.title)}&subtitle=${encodeURIComponent(post.value.excerpt)}`,
ogType: 'article',
twitterCard: 'summary_large_image',
twitterTitle: post.value.title,
twitterImage: `/api/og?title=${encodeURIComponent(post.value.title)}`
});
</script>
<template>
<article>
<h1>{{ post.title }}</h1>
<!-- Content -->
</article>
</template>Nuxt 2
Server Middleware
javascript
// server-middleware/og-image.js
export default async function (req, res, next) {
if (!req.url.startsWith('/og-image')) {
return next();
}
const url = new URL(req.url, `http://${req.headers.host}`);
const title = url.searchParams.get('title') || 'My Site';
const subtitle = url.searchParams.get('subtitle') || '';
try {
const response = await fetch('https://ogimageapi.io/api/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.OG_IMAGE_API_KEY
},
body: JSON.stringify({
title,
subtitle,
template: 'default',
theme: 'dark'
})
});
const buffer = await response.arrayBuffer();
res.setHeader('Content-Type', 'image/png');
res.setHeader('Cache-Control', 'public, max-age=86400');
res.end(Buffer.from(buffer));
} catch (error) {
console.error('OG image error:', error);
res.statusCode = 500;
res.end('Error generating image');
}
}Nuxt Config
javascript
// nuxt.config.js
export default {
serverMiddleware: ['~/server-middleware/og-image.js'],
head: {
meta: [
// Default OG image
{ property: 'og:image', content: '/og-image?title=My+Site' }
]
}
};Vue 3 SPA
For SPAs, generate images at build time:
Build Script
javascript
// scripts/generate-og.js
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const pages = [
{ slug: 'home', title: 'Welcome', subtitle: 'Build with Vue' },
{ slug: 'about', title: 'About', subtitle: 'Our story' }
];
async function generateImages() {
const outputDir = path.join(__dirname, '../public/og');
fs.mkdirSync(outputDir, { recursive: true });
for (const page of pages) {
const response = await fetch('https://ogimageapi.io/api/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.VITE_OG_IMAGE_API_KEY
},
body: JSON.stringify({
title: page.title,
subtitle: page.subtitle,
template: 'default',
theme: 'dark'
})
});
const buffer = await response.arrayBuffer();
fs.writeFileSync(
path.join(outputDir, `${page.slug}.png`),
Buffer.from(buffer)
);
console.log(`Generated: ${page.slug}.png`);
}
}
generateImages();Vue Component
vue
<!-- components/SeoHead.vue -->
<script setup>
import { useHead } from '@vueuse/head';
const props = defineProps({
title: { type: String, required: true },
description: { type: String, default: '' },
image: { type: String, default: '/og/home.png' }
});
useHead({
title: props.title,
meta: [
{ name: 'description', content: props.description },
{ property: 'og:title', content: props.title },
{ property: 'og:description', content: props.description },
{ property: 'og:image', content: props.image },
{ property: 'og:type', content: 'website' },
{ name: 'twitter:card', content: 'summary_large_image' },
{ name: 'twitter:title', content: props.title },
{ name: 'twitter:image', content: props.image }
]
});
</script>
<template>
<slot />
</template>Usage
vue
<!-- pages/About.vue -->
<template>
<SeoHead
title="About Us"
description="Learn about our team"
image="/og/about.png"
>
<div class="about-page">
<!-- Content -->
</div>
</SeoHead>
</template>Composable for Dynamic OG
javascript
// composables/useOGImage.js
export function useOGImage() {
const config = useRuntimeConfig();
async function generateOGUrl(params) {
const queryString = new URLSearchParams(params).toString();
return `/api/og?${queryString}`;
}
async function getSignedUrl(params) {
const response = await $fetch('/api/og/signed', {
method: 'POST',
body: params
});
return response.url;
}
return {
generateOGUrl,
getSignedUrl
};
}Best Practices
- Use Nuxt for SSR — Best OG support
- Generate at build time for static content
- Cache server responses — 24h minimum
- Use composables — Reusable logic
- Test with validators — Facebook, Twitter, LinkedIn