Pinia + Vue 3 Open Graph Tags: Dynamic OG Meta from Store State
How to drive Open Graph meta tags from Pinia store state in Vue 3 + Nuxt 3 apps — covers useHead, useSeoMeta, and SSR-safe reactive meta updates.
Why Pinia and OG tags need SSR
Pinia is the official Vue 3 state management library — it replaced Vuex and integrates natively with Nuxt 3. Using Pinia to drive OG meta tags is a natural pattern: your product store, blog post store, or user store already has the data you want in your og:title and og:image tags.
The critical constraint: social crawlers (Twitterbot, LinkedInBot, Discordbot) never execute JavaScript. If your OG tags are driven by Pinia state that is only hydrated client-side, crawlers will see empty or default values. You must ensure Pinia stores are populated on the server during SSR — which Nuxt 3 handles automatically via useAsyncData and useFetch.
Define a Pinia store for content data
Create a store that holds the OG-relevant fields for a resource (e.g., a blog post):
// stores/post.ts
import { defineStore } from "pinia";
interface Post {
title: string;
excerpt: string;
slug: string;
ogImage: string | null;
}
export const usePostStore = defineStore("post", {
state: () => ({
currentPost: null as Post | null,
loading: false,
}),
actions: {
async fetchPost(slug: string) {
this.loading = true;
try {
const data = await $fetch<Post>(`/api/posts/${slug}`);
this.currentPost = data;
} finally {
this.loading = false;
}
},
},
getters: {
ogTitle: (state) => state.currentPost?.title ?? "My Site",
ogDescription: (state) => state.currentPost?.excerpt ?? "",
ogImage: (state) =>
state.currentPost?.ogImage ?? "https://yourdomain.com/og/default.png",
},
});Populate store during SSR in a Nuxt page
Use useAsyncData to populate the Pinia store on the server. Then drive OG tags from the store via useSeoMeta:
<!-- pages/blog/[slug].vue -->
<script setup lang="ts">
import { usePostStore } from "~/stores/post";
const route = useRoute();
const postStore = usePostStore();
// Runs on server during SSR — store is populated before HTML is sent
await useAsyncData(
`post-${route.params.slug}`,
() => postStore.fetchPost(route.params.slug as string)
);
// useSeoMeta is reactive — reads from store getters
useSeoMeta({
title: () => `${postStore.ogTitle} | My Blog`,
description: () => postStore.ogDescription,
ogType: "article",
ogTitle: () => postStore.ogTitle,
ogDescription: () => postStore.ogDescription,
ogImage: () => postStore.ogImage,
ogUrl: () => `https://yourdomain.com/blog/${route.params.slug}`,
twitterCard: "summary_large_image",
twitterTitle: () => postStore.ogTitle,
twitterImage: () => postStore.ogImage,
});
</script>
<template>
<article v-if="postStore.currentPost">
<h1>{{ postStore.currentPost.title }}</h1>
<p>{{ postStore.currentPost.excerpt }}</p>
</article>
<div v-else>Loading...</div>
</template>Because useAsyncData runs on the server, the Pinia store is populated before the HTML is serialized. Nuxt serializes the store state into a __NUXT__ JSON payload embedded in the HTML, and useSeoMeta reads the reactive values to generate the correct meta tags in the <head>.
useHead vs useSeoMeta
Nuxt 3 provides two composables for head management:
useSeoMeta— type-safe, purpose-built for SEO and OG tags. Preferred for OG meta. Internally calls useHead.useHead— lower-level, accepts any head element. Use when you need non-SEO head tags (scripts, link preloads, etc.).
// Using useHead directly (equivalent, more verbose)
useHead({
title: () => `${postStore.ogTitle} | My Blog`,
meta: [
{ name: "description", content: () => postStore.ogDescription },
{ property: "og:title", content: () => postStore.ogTitle },
{ property: "og:description",content: () => postStore.ogDescription },
{ property: "og:image", content: () => postStore.ogImage },
{ name: "twitter:card", content: "summary_large_image" },
],
});SSR gotcha: avoid client-only data for OG
If you populate Pinia store data inside onMounted or from a client-side event, OG tags will be empty in the SSR response. Always use useAsyncData or useFetch at the top level of your setup() (not inside lifecycle hooks) for anything that drives OG tags.
Verify your Nuxt + Pinia OG tags
Paste any Nuxt 3 URL into OGFixer to confirm OG tags are in the server-rendered HTML — not just client-side.