Schema.org JSON-LD
Schema.org JSON-LD
The useSchema composable is the SEO foundation for every review site. Each export auto-injects a <script type="application/ld+json"> block into the page head — no manual template wiring needed.
Available composables
| Composable | Emits | Drives |
|---|---|---|
useOrganizationSchema() | Organization | Knowledge Panel site link |
usePersonSchema() | Person | E-E-A-T author signal |
useProductSchema() | Product + Review + AggregateRating + Offer | Star SERPs (the #1 ROI block) |
useFAQSchema() | FAQPage | FAQ rich results in SERPs |
useBreadcrumbSchema() | BreadcrumbList | Breadcrumb SERPs |
useWebSiteSchema({ searchUrlTemplate }) | WebSite (+ optional SearchAction) | Sitelinks searchbox |
useArticleSchema({ type: 'BlogPosting', ... }) | BlogPosting / Article / NewsArticle | Article rich results |
useSiteMeta() | <title>, <meta>, OG, Twitter Card, canonical, hreflang, robots | Standard meta tags |
Recommended pattern per review page
<script setup>
const route = useRoute()
// 1) Page meta (title, description, OG, Twitter, canonical, robots)
useSiteMeta({
title: 'Logitech K1 Wireless Keyboard Review',
description: 'After 3 months of daily testing, the K1 is our top pick for typists.',
image: '/reviews/k1/og.jpg',
type: 'article',
publishedTime: '2026-01-15',
modifiedTime: '2026-04-20',
author: 'Jane Reviewer',
section: 'Reviews',
tags: ['wireless keyboards', 'mechanical', 'logitech'],
})
// 2) Article schema (BlogPosting — the article side of the review)
useArticleSchema({
type: 'BlogPosting',
headline: 'Logitech K1 Wireless Keyboard Review',
description: 'After 3 months of daily testing, the K1 is our top pick for typists.',
image: ['/reviews/k1/hero.jpg', '/reviews/k1/side.jpg'],
datePublished: '2026-01-15',
dateModified: '2026-04-20',
articleSection: 'Reviews',
keywords: ['wireless keyboards', 'mechanical', 'logitech'],
wordCount: 1850,
articleBody: 'After 3 months of daily use...',
inLanguage: 'en-US',
})
// 3) Product + Review + AggregateRating + Offer (the product side)
useProductSchema({
product: {
name: 'Logitech K1',
description: 'Compact mechanical wireless keyboard with hot-swappable switches',
image: ['/reviews/k1/hero.jpg', '/reviews/k1/side.jpg'],
brand: 'Logitech',
sku: 'K1-001',
},
ratingValue: 4.6,
ratingCount: 1234,
reviewCount: 89,
offer: {
price: 79.99,
priceCurrency: 'USD',
availability: 'InStock',
priceValidUntil: '2026-12-31',
},
reviewBody: 'After 3 months of daily use...',
datePublished: '2026-01-15',
})
// 4) Breadcrumbs
useBreadcrumbSchema({
items: [
{ label: 'Home', href: '/' },
{ label: 'Reviews', href: '/reviews' },
{ label: 'Wireless Keyboards', href: '/reviews/wireless-keyboards' },
{ label: 'Logitech K1' },
],
})
// 5) FAQ
useFAQSchema({
faqs: [
{ question: 'Is the K1 hot-swappable?', answer: 'Yes — all 87 switches are hot-swappable with 3-pin and 5-pin support.' },
{ question: 'Battery life?', answer: 'Up to 200 hours with backlight off; ~40 hours with backlight on full.' },
],
})
</script>
This is the dual-block pattern Google recommends for review content: one BlogPosting JSON-LD for the article side, one Product + Review + AggregateRating + Offer JSON-LD for the product side. Each composable handles its own block — you just call both.
Recommended pattern for /articles/[slug].vue
Pure blog posts (no product) need only the article side. The dual-block is overkill — just useArticleSchema() + useSiteMeta() + useBreadcrumbSchema():
<script setup>
const route = useRoute()
const article = await useArticle(route.params.slug)
useSiteMeta({
title: article.title,
description: article.excerpt,
image: article.heroImage,
type: 'article',
publishedTime: article.publishedAt,
modifiedTime: article.updatedAt,
author: article.author.name,
section: article.category,
tags: article.tags,
keywords: article.seoKeywords, // separate list for <meta name="keywords">
alternate: article.translations?.map(t => ({
hreflang: t.lang,
href: t.url,
})),
})
useArticleSchema({
type: 'BlogPosting',
headline: article.title,
description: article.excerpt,
image: article.heroImage,
datePublished: article.publishedAt,
dateModified: article.updatedAt,
articleSection: article.category,
keywords: article.tags,
wordCount: article.wordCount,
articleBody: article.bodyText,
inLanguage: 'en-US',
})
useBreadcrumbSchema({
items: [
{ label: 'Home', href: '/' },
{ label: article.category, href: `/category/${article.categorySlug}` },
{ label: article.title },
],
})
</script>
This replaces the ~80-line hand-rolled BlogPosting JSON-LD block that most consuming sites still ship on articles/[slug].vue.
Site-wide setup (in app.vue or a layout)
<script setup>
// Fire once per app — site identity + searchbox
useOrganizationSchema()
usePersonSchema() // uses xAffiliate.author
useWebSiteSchema({
searchUrlTemplate: 'https://example.com/search?q={search_term_string}',
})
</script>
useArticleSchema() API
| Field | Type | Default | Description |
|---|---|---|---|
headline | string | required | Article headline. Drives <h1> in rich results. |
type | "Article" | "BlogPosting" | "NewsArticle" | "Article" | Schema.org @type. Use "BlogPosting" for /articles/[slug].vue pages. "NewsArticle" for newsroom content. |
description | string | Short description / excerpt. | |
image | string | string[] | Hero image (or array of images). | |
datePublished | string | required | ISO 8601 publish date. |
dateModified | string | ISO 8601 last-modified date. | |
url | string | route path | Canonical article URL. |
author | { name: string; url?: string } | xAffiliate.author | Author. Defaults to site-wide author if not provided. |
publisherName | string | xAffiliate.brand.name | Publisher name. |
publisherLogo | string | xAffiliate.brand.logo | Publisher logo URL. |
articleSection | string | Section / category (e.g. "Reviews"). Drives articleSection. | |
keywords | string[] | Tags / keywords. Joined with commas in the output. | |
wordCount | number | Approximate word count. Drives Google's "x min read" annotation. | |
articleBody | string | Article body (plain text). Used by Google's article parser. | |
inLanguage | string | xAffiliate.brand.locale or "en_US" | BCP-47 language tag. |
isAccessibleForFree | boolean | true | SubscribeWithGoogle signal. |
useProductSchema() API
| Field | Type | Required | Description |
|---|---|---|---|
product.name | string | ✅ | Product / review target name. |
product.description | string | Product description. | |
product.image | string | string[] | Image URL or array. | |
product.brand | string | Brand name. Renders as Brand schema. | |
product.sku / product.mpn | string | Identifiers. | |
product.url | string | Canonical URL. Defaults to current route path. | |
ratingValue | number | ✅ | Numeric rating. Clamped to [0, bestRating]. |
ratingCount | number | ✅ | Total ratings count. Required by Google for star SERPs. |
reviewCount | number | Separate review count (subset of ratingCount). | |
bestRating | number | 5 | Best possible rating. |
offer.price | number | Offer price for Offer schema. | |
offer.priceCurrency | string | 'USD' | ISO 4217. |
offer.availability | string | 'InStock' | InStock / OutOfStock / PreOrder / Discontinued. |
offer.priceValidUntil | string | ISO date. | |
reviewBody | string | Review snippet shown in SERPs. | |
datePublished / dateModified | string | ISO 8601 dates. |
useSiteMeta() API
| Field | Type | Default | Description |
|---|---|---|---|
title | string | site name | Page title. |
description | string | site tagline | Meta description. |
image | string | brand OG image | OG / Twitter image. |
url | string | current route | Canonical URL. |
type | 'website' | 'article' | 'product' | 'website' | OG type. |
publishedTime | string | — | article:published_time (article only). |
modifiedTime | string | — | article:modified_time (article only). |
author | string | — | article:author (article only). |
section | string | — | article:section (article only). |
tags | string[] | — | article:tag (article only, one per tag). |
keywords | string[] | — | <meta name="keywords">. Distinct from tags so you can have separate lists for meta vs article schema. |
alternate | { hreflang: string; href: string }[] | — | <link rel="alternate" hreflang="..." href="..."> for international SEO. Pass { hreflang: 'x-default', ... } for the default-locale variant. |
noindex | boolean | false | Set robots to noindex, nofollow. |
Sets title template "{page} | {site}" automatically. Does not emit empty <meta property="..." content=""> tags — when an image / twitter handle / publish date isn't set, the corresponding tag is skipped entirely.
cleanSchema()
Strips undefined / null / empty-array values from a schema object. Useful when building custom schema shapes:
const schema = cleanSchema({
'@context': 'https://schema.org',
'@type': 'Thing',
name: 'X',
description: undefined, // removed
image: [], // removed
url: 'https://...',
})
AI Context
composable: useSchema
package: "@xenterprises/nuxt-x-affiliate"
use-when: >
Building a review or product page that needs Google rich-result eligibility.
useProductSchema() is the highest-ROI call — drives star SERPs.
useArticleSchema({ type: 'BlogPosting', ... }) is the swap target for
hand-rolled BlogPosting JSON-LD on articles/[slug].vue pages — it
supports the full Schema.org article shape (articleSection, keywords,
wordCount, articleBody, inLanguage, isAccessibleForFree).
For review pages, call useArticleSchema + useProductSchema together
(the dual-block pattern Google recommends). useFAQSchema() drives FAQ
rich results. useBreadcrumbSchema() drives breadcrumb SERPs.
useOrganizationSchema() + usePersonSchema() are called once site-wide
from app.vue or a layout. useWebSiteSchema({ searchUrlTemplate })
enables the Google sitelinks searchbox. useSiteMeta() handles all
standard meta tags (title, description, OG, Twitter Card, canonical,
hreflang via alternate[], robots) — single call replaces the
useSeoMeta + useHead boilerplate that lived on every page.
Call useOrganizationSchema() + usePersonSchema() once in app.vue.
Call useSiteMeta() + useArticleSchema() + useBreadcrumbSchema() on
articles/[slug].vue. Call useSiteMeta() + useArticleSchema() +
useProductSchema() + useBreadcrumbSchema() + useFAQSchema() on
reviews/[slug].vue. Every composable auto-injects a
<script type="application/ld+json"> block — no template wiring required.
useAffiliateContent
Review-site composable — merchant config lookup, idempotent link tagging, Intl price formatting, and GTM click tracking.
Affiliate Impression
IntersectionObserver-based impression tracking — fires affiliate_impression to dataLayer when a buy button enters the viewport. Measure view-through conversion.
