Index of every page on this site: llms.txt

Add JSON-LD Structured Data in Vue

Add type-safe JSON-LD to a Vue app with @unhead/schema-org's useSchemaOrg, no Nuxt required.
Harlan Wilton8 mins read Published Updated
What you'll learn
  • Install @unhead/schema-org manually in Vue (Nuxt SEO includes it automatically)
  • JSON-LD is Google's recommended format for structured data
  • Use useSchemaOrg() for type-safe markup with automatic graph linking

Schema.org structured data helps Google display Rich Results: star ratings, recipe cards, and product prices. In Vue, you need to install @unhead/schema-org separately and configure it in your app entry point; Nuxt SEO handles this automatically. Rotten Tomatoes added structured data to 100,000 pages and measured a 25% higher click-through rate compared to pages without it.

<!-- JSON-LD structured data in the head -->
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "Add JSON-LD Structured Data in Vue",
  "author": { "@type": "Person", "name": "Your Name" }
}
</script>

Google recommends JSON-LD as the easiest format to implement and maintain. Google supports rich results for many schema types including Article, Product, and Breadcrumb. See Rich Results for the full list of active types and eligibility details.

Rich results aren't guaranteed. Content must match the markup and follow Google's structured data guidelines.

Setup with Unhead

Vue uses Unhead for head management. You can add JSON-LD via useHead(), but useSchemaOrg() from @unhead/schema-org provides type safety and automatic graph linking.

pnpm add @unhead/vue @unhead/schema-org

Install the head instance on your Vue app before calling the composables. Configure your public host:

main.ts
import { UnheadSchemaOrg } from '@unhead/schema-org/vue'
import { createHead } from '@unhead/vue/client'
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

const head = createHead({
  plugins: [UnheadSchemaOrg({ host: 'https://mysite.com' })]
})

createApp(App).use(router).use(head).mount('#app')

App and router are your application files. For SSR, import createHead from @unhead/vue/server. Create fresh app, router, and head instances for each request. After renderToString, use renderSSRHead to insert headTags and bodyTags into your HTML template. The Vite SSR guide covers that server integration.

import { defineArticle, useSchemaOrg } from '@unhead/schema-org/vue'

useSchemaOrg([
  defineArticle({
    headline: 'Add JSON-LD Structured Data in Vue',
    author: { name: 'Your Name' },
    datePublished: new Date(2024, 0, 15),
  })
])

The defineX() helpers handle boilerplate and align with Google's Structured Data Guidelines:

HelperRich Result Type
defineArticle()Article, NewsArticle, BlogPosting
defineBreadcrumb()Breadcrumb navigation
defineQuestion()FAQPage markup (no active rich result; retired May 2026)
defineProduct()Product listings

Reactive Data

useSchemaOrg() accepts refs and computed getters:

<script setup lang="ts">
import { defineArticle, useSchemaOrg } from '@unhead/schema-org/vue'
import { ref } from 'vue'

const article = ref({
  title: 'My Article',
  description: 'Article description'
})

useSchemaOrg([
  defineArticle({
    headline: () => article.value.title,
    description: () => article.value.description,
  })
])
</script>

Entity Linking

Instead of nesting everything inside one object, use @id to link separate nodes into a graph. useSchemaOrg() does this automatically when you reuse the same @id, or you can link nodes manually:

<script setup lang="ts">
import { defineArticle, definePerson, useSchemaOrg } from '@unhead/schema-org/vue'

useSchemaOrg([
  // Define the Person separately
  definePerson({
    '@id': '#jane', // Unique ID
    'name': 'Jane Doe',
    'jobTitle': 'Editor',
  }),
  // Link the Article to the Person via ID
  defineArticle({
    headline: 'Advanced Schema Patterns',
    author: { '@id': '#jane' }, // Links to the node above
  }),
])
</script>

This produces one connected JSON-LD graph instead of duplicate, disconnected nodes.

Site-Wide Setup

Set up base schema in your root component. The installed plugin uses your public host. A reactive canonical link supplies the current page URL:

App.vue
<script setup lang="ts">
import { defineOrganization, defineWebPage, defineWebSite, useSchemaOrg } from '@unhead/schema-org/vue'
import { useHead, useSeoMeta } from '@unhead/vue'
import { useRoute } from 'vue-router'

const route = useRoute()
useHead({
  link: [{
    rel: 'canonical',
    href: () => new URL(route.path, 'https://mysite.com').href
  }]
})
useSeoMeta({ title: () => String(route.meta.title || 'My Site') })

useSchemaOrg([
  defineWebPage(),
  defineWebSite({ name: 'My Site' }),
  defineOrganization({ name: 'My Company', logo: '/logo.png' })
])
</script>

Set route.meta.title in your router configuration. Article components can add defineArticle nodes. The rendered WebPage node should contain the current canonical URL. Test two routes independently to catch shared server state.

Blog Article Example

Vue's hierarchical head system means you don't need to repeat WebSite and WebPage if they're in the layout:

blog/[slug].vue
<script lang="ts" setup>
import { defineArticle, useSchemaOrg } from '@unhead/schema-org/vue'

// fetchArticle is your application's data loader, not an Unhead API.
const article = await fetchArticle()

useSchemaOrg([
  defineArticle({
    headline: article.title,
    image: article.image,
    datePublished: article.publishedAt,
    dateModified: article.updatedAt,
    author: {
      name: article.author.name,
      url: article.author.url,
    }
  })
])
</script>

Testing Your Markup

Validate your markup with Google's Rich Results Test: it renders JavaScript, so it sees JSON-LD injected client-side. A plain curl request or "View Page Source" won't, since useSchemaOrg() writes the script tag after hydration in a client-only Vue app. For a full testing workflow, see Rich Results.

Checklist

Checklist
  • Install @unhead/schema-org and configure it in your app entry point
  • Set up base WebSite, WebPage, and Organization/Person schema in your root component
  • Add page-specific schema (Article, Product, Breadcrumb) with the defineX() helpers
  • Link related entities with a shared @id instead of nesting duplicates
  • Skip FAQPage markup if you're chasing a rich result: it no longer produces one
  • Validate markup with Google's Rich Results Test

Nuxt SEO handles Schema.org automatically with zero config. See the Nuxt Schema.org guide for the Nuxt-specific approach.

Was this page helpful?