---
title: "Add JSON-LD Structured Data in Vue"
description: "Add type-safe JSON-LD to a Vue app with @unhead/schema-org's useSchemaOrg, no Nuxt required."
canonical_url: "https://nuxtseo.com/learn-seo/vue/mastering-meta/schema-org"
last_updated: "2026-10-05"
---

> Index of every page on this site: [llms.txt](https://nuxtseo.com/llms.txt)

::key-takeaways
- Install `@unhead/schema-org` manually in Vue (Nuxt SEO includes it automatically)
- JSON-LD is Google's recommended format for structured data
- Use `useSchemaOrg()`{lang="ts"} for type-safe markup with automatic graph linking
::

[Schema.org](http://Schema.org) structured data helps Google display [Rich Results](https://developers.google.com/search/docs/appearance/structured-data/search-gallery): 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](https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data) compared to pages without it.

```html
<!-- 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](https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data) as the easiest format to implement and maintain. Google supports rich results for many schema types including Article, Product, and Breadcrumb. See [Rich Results](https://nuxtseo.com/learn-seo/vue/mastering-meta/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](https://developers.google.com/search/docs/appearance/structured-data/sd-policies).

## Setup with Unhead

Vue uses [Unhead](https://unhead.unjs.io/) for head management. You can add JSON-LD via `useHead()`{lang="ts"}, but `useSchemaOrg()`{lang="ts"} from `@unhead/schema-org` provides type safety and automatic [graph linking](https://schema.org/docs/data-and-datasets.html).

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

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

```ts [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](https://nuxtseo.com/learn-seo/vue/ssr-frameworks/vite-ssr) covers that server integration.

::code-group
```ts [useSchemaOrg (recommended)]
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),
  })
])
```

```ts [useHead (manual)]
import { useHead } from '@unhead/vue'

useHead({
  script: [
    {
      type: 'application/ld+json',
      innerHTML: JSON.stringify({
        '@context': 'https://schema.org',
        '@type': 'Article',
        'headline': 'Add JSON-LD Structured Data in Vue',
        'author': { '@type': 'Person', 'name': 'Your Name' }
      })
    }
  ],
})
```
::

The `defineX()`{lang="ts"} helpers handle boilerplate and align with [Google's Structured Data Guidelines](https://developers.google.com/search/docs/guides/sd-policies):

| Helper                                                                                          | Rich Result Type                                                                                         |
| ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| [`defineArticle()`{lang="ts"}](https://unhead.unjs.io/docs/schema-org/api/schema/article)       | Article, NewsArticle, BlogPosting                                                                        |
| [`defineBreadcrumb()`{lang="ts"}](https://unhead.unjs.io/docs/schema-org/api/schema/breadcrumb) | Breadcrumb navigation                                                                                    |
| [`defineQuestion()`{lang="ts"}](https://unhead.unjs.io/docs/schema-org/api/schema/question)     | FAQPage markup (no active rich result; [retired May 2026](https://developers.google.com/search/updates)) |
| [`defineProduct()`{lang="ts"}](https://unhead.unjs.io/docs/schema-org/api/schema/product)       | Product listings                                                                                         |

## Reactive Data

`useSchemaOrg()`{lang="ts"} accepts refs and computed getters:

```vue
<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()`{lang="ts"} does this automatically when you reuse the same `@id`, or you can link nodes manually:

```vue
<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:

```vue [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:

```vue [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](https://search.google.com/test/rich-results): it renders JavaScript, so it sees JSON-LD injected client-side. A plain `curl`{lang="bash"} request or "View Page Source" won't, since `useSchemaOrg()`{lang="ts"} writes the script tag after hydration in a client-only Vue app. For a full testing workflow, see [Rich Results](https://nuxtseo.com/learn-seo/vue/mastering-meta/rich-results#testing-your-markup).

## Checklist

::checklist{#vue-schema-org}
- 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()`{lang="ts"} helpers
- Link related entities with a shared `@id` instead of nesting duplicates
- Skip `FAQPage`{lang="ts"} 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](http://Schema.org) [automatically](https://nuxtseo.com/docs/schema-org/getting-started/introduction) with zero config. See the [Nuxt Schema.org guide](https://nuxtseo.com/learn-seo/nuxt/mastering-meta/schema-org) for the Nuxt-specific approach.

## Sitemap

See the full [sitemap](https://nuxtseo.com/sitemap.md) for all pages.
