OG Images in Vue & Nuxt
Generate OG images in Vue and Nuxt with the FastOG API — sign up free for 100 credits (create your API key in the dashboard), credit-based, HMAC-secured. This guide shows you how to create a signer utility for HMAC-signed OG image URLs and render dynamic Open Graph images on the fly. Preview with the Free OG Image Tester before you ship.
Quick Start
1. Create the signer utility
javascript
// lib/og-image-signer.js
import crypto from "crypto";
export function generateSignedOgUrl(params, options = {}) {
const {
baseUrl = "https://fastog.com/api/v1/og",
apiKey = process.env.FASTOG_API_KEY,
hmacSecret = process.env.FASTOG_HMAC_SECRET,
} = options;
if (!apiKey || !hmacSecret) {
throw new Error("Missing FASTOG_API_KEY or FASTOG_HMAC_SECRET");
}
const timestamp = Math.floor(Date.now() / 1000);
const url = new URL(baseUrl);
for (const [key, value] of Object.entries(params)) {
url.searchParams.set(key, value);
}
const apiKey = apiKey;
const path = url.pathname;
const message = `GET${path}${timestamp}${apiKey}`;
const signature = crypto
.createHmac("sha256", hmacSecret)
.update(message)
.digest("hex");
url.searchParams.set("key", apiKey);
url.searchParams.set("s", signature);
url.searchParams.set("t", timestamp);
return url.toString();
}2. Environment variables
bash
# .env
FASTOG_API_KEY=sk_your_full_api_key_here
FASTOG_HMAC_SECRET=your_64_char_hex_secret_hereNuxt 3
Page with useHead
vue
<!-- pages/blog/[slug].vue -->
<script setup>
const route = useRoute();
const { data: post } = await useAsyncData("post", () =>
getPost(route.params.slug),
);
const { data: ogImageUrl } = await useAsyncData("og-image", () =>
$fetch("/api/og", {
query: {
title: post.value.title,
template: "blog",
subtitle: post.value.excerpt,
},
}).then((res) => res.ogImageUrl),
);
useHead({
title: post.value.title,
meta: [
{ property: "og:image", content: ogImageUrl.value },
{ property: "og:title", content: post.value.title },
{ property: "og:description", content: post.value.excerpt },
],
});
</script>
<template>
<article>
<h1>{{ post.title }}</h1>
<p>{{ post.excerpt }}</p>
</article>
</template>Server API Route
javascript
// server/api/og.get.js
import { generateSignedOgUrl } from "~/lib/og-image-signer";
export default defineEventHandler(async (event) => {
const query = getQuery(event);
const { title, template = "default", ...params } = query;
if (!title) {
throw createError({ statusCode: 400, message: "Missing title" });
}
const ogImageUrl = generateSignedOgUrl({
template,
title,
...params,
});
return { ogImageUrl };
});Vue 3 (SPA)
Composable
javascript
// composables/useOgImage.js
import { ref, watch } from "vue";
export function useOgImage(params) {
const ogImageUrl = ref(null);
watch(
() => params,
async (newParams) => {
try {
const searchParams = new URLSearchParams(newParams);
const res = await fetch(`/api/og?${searchParams}`);
const data = await res.json();
ogImageUrl.value = data.ogImageUrl;
} catch (err) {
console.error("Failed to generate OG image:", err);
}
},
{ immediate: true },
);
return ogImageUrl;
}Component
vue
<!-- components/BlogPost.vue -->
<script setup>
import { useOgImage } from "~/composables/useOgImage";
const props = defineProps({
post: Object,
});
const ogImageUrl = useOgImage({
title: props.post.title,
template: "blog",
subtitle: props.post.excerpt,
});
</script>
<template>
<article>
<h1>{{ post.title }}</h1>
<p>{{ post.excerpt }}</p>
</article>
</template>Backend API (Express)
javascript
// server/index.js
import express from "express";
import crypto from "crypto";
const app = express();
app.get("/api/og", (req, res) => {
const { title, template = "default", ...params } = req.query;
if (!title) {
return res.status(400).json({ error: "Missing title" });
}
const timestamp = Math.floor(Date.now() / 1000);
const apiKey = process.env.FASTOG_API_KEY;
const hmacSecret = process.env.FASTOG_HMAC_SECRET;
const apiKey = apiKey;
const message = `GET/api/v1/og${timestamp}${apiKey}`;
const signature = crypto
.createHmac("sha256", hmacSecret)
.update(message)
.digest("hex");
const url = new URL("https://fastog.com/api/v1/og");
url.searchParams.set("template", template);
url.searchParams.set("title", title);
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
url.searchParams.set("key", apiKey);
url.searchParams.set("s", signature);
url.searchParams.set("t", timestamp);
res.json({ ogImageUrl: url.toString() });
});
app.listen(3001, () => {
console.log("API server running on port 3001");
});E-commerce Example
vue
<!-- pages/products/[id].vue -->
<script setup>
const route = useRoute();
const { data: product } = await useAsyncData("product", () =>
getProduct(route.params.id),
);
const { data: ogImageUrl } = await useAsyncData("og-image", () =>
$fetch("/api/og", {
query: {
title: product.value.name,
template: "ecommerce",
price: product.value.price,
image: product.value.image,
},
}).then((res) => res.ogImageUrl),
);
useHead({
meta: [
{ property: "og:image", content: ogImageUrl.value },
{ property: "og:title", content: product.value.name },
],
});
</script>
<template>
<main>
<h1>{{ product.name }}</h1>
<p>{{ product.price }}</p>
</main>
</template>User Profile Example
vue
<!-- pages/users/[username].vue -->
<script setup>
const route = useRoute();
const { data: user } = await useAsyncData("user", () =>
getUser(route.params.username),
);
const { data: ogImageUrl } = await useAsyncData("og-image", () =>
$fetch("/api/og", {
query: {
title: `${user.value.name} - Profile`,
template: "profile",
name: user.value.name,
avatar: user.value.avatarUrl,
bio: user.value.bio,
},
}).then((res) => res.ogImageUrl),
);
useHead({
meta: [
{ property: "og:image", content: ogImageUrl.value },
{ property: "og:title", content: `${user.value.name} - Profile` },
],
});
</script>Common Pitfalls
Client-side Signing
Never sign OG URLs in browser code.
❌ Wrong:
vue
<script setup>
import { generateSignedOgUrl } from "~/lib/og-image-signer";
// Runs in browser — secret exposed!
const ogUrl = generateSignedOgUrl({ title: post.title });
</script>✅ Correct:
javascript
// server/api/og.get.js(runs on server only)
import { generateSignedOgUrl } from "~/lib/og-image-signer";
export default defineEventHandler((event) => {
return { ogImageUrl: generateSignedOgUrl({ title: query.title }) };
});Timestamp in Milliseconds
❌ Wrong: Date.now() → 1722718800000
✅ Correct: Math.floor(Date.now() / 1000) → 1722718800
Full API Reference
Related
Try it free — no signup required
Preview and test dynamic OG images in seconds.
Open the Free OG Image Tester