nuxt-x-schema
nuxt-x-schema
16 renderless Schema.org structured data components for Nuxt 4. Each component injects a <script type="application/ld+json"> tag into <head> via useHead — fully SSR-safe, visible to search engine crawlers on first paint.
Installation
npm install @xenterprises/nuxt-x-schema
// nuxt.config.ts
export default defineNuxtConfig({
extends: ["@xenterprises/nuxt-x-schema"],
});
What the consumer writes
The minimal consumer setup is an extends entry plus an app/app.config.ts with your site's branding — no components, pages, or server code to write:
// nuxt.config.ts
export default defineNuxtConfig({
extends: ["@xenterprises/nuxt-x-schema"],
});
// app/app.config.ts — must live in app/, not the project root
export default defineAppConfig({
xSchema: {
siteUrl: "https://mysite.com", // base for absolute URLs in JSON-LD
siteName: "My Site", // default organization/publisher name
siteLogo: "/logo.png", // optional — publisher ImageObject logo
organizationName: "My Company", // optional — falls back to siteName
},
});
Then drop any XSchema* component into a page. Everything is auto-imported and renderless — the only output is JSON-LD in <head>:
<XSchemaWebSite name="My Site" url="https://mysite.com" />
No environment variables are required — all configuration is via app.config.ts. If you skip the xSchema overrides, JSON-LD falls back to placeholder values (https://example.com, My Website), so set them before going live.
What This Layer Provides
- 16 Schema.org components — all renderless, all auto-imported
useSchema()composable — shared utilities for URL resolution and JSON-LD injection
Configuration (app.config.ts)
export default defineAppConfig({
xSchema: {
siteUrl: "https://mysite.com",
siteName: "My Site",
siteLogo: "/logo.png",
organizationName: "My Company",
},
});
| Option | Type | Default | Description |
|---|---|---|---|
siteUrl | string | undefined (runtime fallback https://example.com) | Base URL for absolute URL construction. |
siteName | string | undefined (runtime fallback My Website) | Default organization/publisher name. |
siteLogo | string | undefined | Logo URL; omitted from JSON-LD output when unset. |
organizationName | string | undefined (falls back to siteName) | Organization name. |
Components
XSchemaArticle — Blog posts, news articles
| Prop | Type | Required | Description |
|---|---|---|---|
headline | string | Yes | Article title. |
datePublished | string | Yes | ISO 8601 publication date. |
author | string | Person | Array | Yes | Author name(s) or Person object(s). |
description | string | No | Article summary. |
image | string | string[] | No | Article image URL(s). |
dateModified | string | No | Last modification date (defaults to datePublished). |
articleType | string | No | Article, BlogPosting, NewsArticle, TechArticle (default: "BlogPosting"). |
url | string | No | Canonical URL. |
wordCount | number | No | Word count. |
XSchemaBreadcrumb — Navigation breadcrumbs
| Prop | Type | Required | Description |
|---|---|---|---|
items | BreadcrumbItem[] | Yes | Array of { label, to } or { name, item }. Relative URLs resolved to absolute. |
XSchemaEvent — Events and conferences
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | — | Event name. |
description | string | Yes | — | Event description. |
startDate | string | Yes | — | ISO 8601 start date/time. |
endDate | string | Yes | — | ISO 8601 end date/time. |
eventAttendanceMode | string | No | "OnlineEventAttendanceMode" | OnlineEventAttendanceMode, OfflineEventAttendanceMode, MixedEventAttendanceMode. |
eventStatus | string | No | "EventScheduled" | EventScheduled, EventCancelled, EventPostponed, etc. |
location | Record<string, unknown> | No | — | Location object (Place or VirtualLocation). |
url | string | No | — | Event URL. |
image | string | string[] | No | — | Event image URL(s). |
organizer | Organization | No | — | Organizer Organization object (defaults to getDefaultOrganization()). |
offers | Offer | No | — | Ticket/registration Offer object. |
XSchemaFAQ — FAQ pages
| Prop | Type | Required | Description |
|---|---|---|---|
items | FAQ[] | Yes | Array of { question, answer } (also accepts label/content or name/text). |
pageTitle | string | No | Page name added to the schema. |
pageDescription | string | No | Page description. |
datePublished | string | No | ISO 8601 publish date. |
dateModified | string | No | ISO 8601 modified date. |
author | string | Person | No | Page author. |
XSchemaHowTo — Step-by-step guides
| Prop | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Guide title. |
description | string | No | How-to description. |
image | string | string[] | No | Illustration image URL(s). |
totalTime | string | No | ISO 8601 duration (e.g. "PT30M"). |
estimatedCost | MonetaryAmount | No | Estimated cost object { currency, value }. |
supply | string[] | No | Required supplies (strings are wrapped as HowToSupply). |
tool | string[] | No | Required tools (strings are wrapped as HowToTool). |
steps | HowToStep[] | Yes | Array of { name, text, image?, url? }. |
XSchemaLocalBusiness — Business locations
| Prop | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Business name. |
businessType | string | Yes | Schema.org type (e.g. "Restaurant", "CafeOrCoffeeShop", "MedicalClinic"). |
url | string | Yes | Business website URL. |
address | PostalAddress | Yes | Address object: { streetAddress, addressLocality, addressRegion, postalCode, addressCountry }. |
telephone | string | No | Phone number. |
email | string | No | Contact email. |
image | string | No | Business image URL. |
description | string | No | Business description. |
openingHoursSpecification | Record<string, unknown>[] | No | Array of opening hours objects. |
aggregateRating | AggregateRating | No | Rating object { ratingValue, reviewCount }. |
priceRange | string | No | Price range indicator (e.g. "$$"). |
sameAs | string[] | No | Social profile and directory URLs. |
XSchemaOffer — Pricing offers
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | — | Offer name. |
description | string | No | — | Offer description. |
price | string | number | Yes | — | Price value. |
priceCurrency | string | No | "USD" | ISO 4217 currency code. |
availability | string | No | "InStock" | InStock, OutOfStock, PreOrder, etc. |
url | string | No | — | Offer URL. |
seller | Organization | No | — | Seller Organization object (defaults to getDefaultOrganization()). |
validFrom | string | No | — | ISO 8601 offer start date. |
validThrough | string | No | — | ISO 8601 offer end date. |
priceValidUntil | string | No | — | ISO 8601 price expiry date. |
itemCondition | string | No | "NewCondition" | NewCondition, UsedCondition, RefurbishedCondition. |
XSchemaOrganization — Company information
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | — | Organization name. |
url | string | Yes | — | Website URL. |
logo | string | Yes | — | Logo image URL. |
legalName | string | No | — | Official legal name. |
description | string | No | — | Organization description. |
email | string | No | — | Contact email. |
telephone | string | No | — | Contact phone. |
address | PostalAddress | No | — | Postal address object. |
sameAs | string[] | No | [] | Social and directory profile URLs. |
foundingDate | string | No | — | ISO 8601 founding date. |
founder | string | Person | No | — | Founder name or Person object. |
XSchemaPerson — Author/team profiles
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | — | Person's full name. |
jobTitle | string | No | — | Job title. |
description | string | No | — | Short bio. |
image | string | No | — | Profile image URL. |
url | string | No | — | Personal website URL. |
email | string | No | — | Email address. |
telephone | string | No | — | Phone number. |
sameAs | string[] | No | [] | Social and profile URLs. |
worksFor | Organization | No | — | Employer Organization object. |
knowsAbout | string[] | No | [] | Topics the person knows about. |
alumniOf | string | Organization | No | — | Educational institution (string wrapped as Organization). |
XSchemaProduct — Products with offers
| Prop | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Product name. |
description | string | Yes | Product description. |
image | string | string[] | No | Product image URL(s). |
brand | string | Organization | No | Brand name or Organization (defaults to siteName). |
offers | Offer | Yes | Offer object with price and availability. |
aggregateRating | AggregateRating | No | Rating object { ratingValue, reviewCount }. |
review | Review[] | No | Array of Review objects. |
sku | string | No | Stock-keeping unit identifier. |
manufacturer | string | Organization | No | Manufacturer name or Organization. |
category | string | No | Product category. |
XSchemaQAPage — Q&A pages
| Prop | Type | Required | Description |
|---|---|---|---|
items | QAItem[] | Yes | Array of Q&A items: { question, answer, answerAuthor?, answerDate?, upvoteCount?, answerCount?, suggestedAnswers? }. Each item accepts label/question/name for the question and content/answer/text for the accepted answer. |
pageTitle | string | No | Page name added to the schema. |
pageDescription | string | No | Page description. |
datePublished | string | No | ISO 8601 publish date. |
dateModified | string | No | ISO 8601 modified date. |
author | string | Person | No | Page author. |
XSchemaReview — Reviews and ratings
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
itemReviewed | Record<string, unknown> | Yes | — | The item being reviewed (must include @type and name). |
reviewRating | Rating | Yes | — | Rating object { ratingValue, bestRating? }. |
author | string | Person | Yes | — | Reviewer name or Person object. |
reviewBody | string | No | — | Full review text. |
datePublished | string | No | — | ISO 8601 review date. |
isAggregate | boolean | No | false | When true, outputs AggregateRating schema instead of individual Review. |
ratingCount | number | No | — | Total rating count (aggregate mode). |
reviewCount | number | No | — | Total review count (aggregate mode). |
XSchemaService — Service pages
| Prop | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Service name. |
description | string | Yes | Service description. |
serviceType | string | Yes | Service type identifier. |
provider | Organization | No | Provider Organization object (defaults to getDefaultOrganization()). |
areaServed | string | string[] | No | Geographic area(s) served. |
offers | Offer | No | Pricing Offer object. |
image | string | string[] | No | Service image URL(s). |
url | string | No | Service page URL. |
category | string | No | Service category. |
XSchemaSoftwareApplication — Web/mobile apps
| Prop | Type | Required | Description |
|---|---|---|---|
name | string | Yes | App name. |
description | string | Yes | App description. |
url | string | Yes | App URL. |
applicationCategory | string | Yes | Schema.org application category (e.g. "BusinessApplication"). |
operatingSystem | string | string[] | No | Supported OS(es). |
offers | Offer | No | Pricing Offer object. |
aggregateRating | AggregateRating | No | Rating object. |
author | string | Person | No | Developer name or Person object. |
screenshot | string | string[] | No | Screenshot URL(s). |
releaseNotes | string | No | Release notes URL or text. |
XSchemaVideoObject — Video content
| Prop | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Video title. |
description | string | Yes | Video description. |
thumbnailUrl | string | string[] | Yes | Thumbnail URL(s). |
uploadDate | string | Yes | ISO 8601 upload date. |
duration | string | No | ISO 8601 duration (e.g. "PT10M30S"). |
contentUrl | string | No | Direct video URL. |
embedUrl | string | No | Embed URL (e.g. YouTube embed). |
interactionCount | number | No | Number of views/interactions. |
author | string | Person | No | Creator name or Person object. |
XSchemaWebSite — Site-wide schema
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | — | Site name. |
url | string | Yes | — | Site URL. |
description | string | No | — | Site description. |
publisher | Organization | No | — | Publisher Organization object. |
enableSearchBox | boolean | No | false | Add Sitelinks Search Box potentialAction. |
searchUrl | string | No | — | Search URL template (required if enableSearchBox is true; must include {search_term_string} placeholder). |
queryInput | string | No | "required name=search_term_string" | query-input value for the SearchAction. |
useSchema() Composable
| Method | Returns | Description |
|---|---|---|
siteUrl | ComputedRef<string> | Configured site URL (no trailing slash). |
siteName | ComputedRef<string> | Configured site name. |
config | ComputedRef<XSchemaConfig> | Reactive app config under the xSchema namespace. |
resolveUrl(path) | string | undefined | Convert relative URL to absolute. |
getDefaultOrganization() | Organization | Default org from app config. |
getDefaultPublisher() | object | Default publisher with ImageObject logo. |
normalizeAuthor(author) | Person | Person[] | undefined | Convert string/array to Person schema. |
normalizeImage(image) | string[] | undefined | Ensure image is array. |
useSchemaHead(fn) | void | Inject JSON-LD into <head> via useHead. |
Environment Variables
None required. All configuration is via app.config.ts.
How It Works
Each XSchema* component calls useSchema().useSchemaHead() which wraps useHead() to inject a <script type="application/ld+json"> tag into the page <head>. Because useHead is SSR-safe, the JSON-LD is rendered server-side and visible to crawlers on first paint. Default values for publisher/organizer come from app.config.ts via useSchema(), so branding is configured once and reused across all components. String values for author, founder, and brand are automatically converted to proper Schema.org typed objects.
Test your output: Google Rich Results Test · Schema.org Validator
Layer Architecture
| Path | Purpose |
|---|---|
nuxt.config.ts | Minimal layer entry — just devtools: { enabled: false }. |
app.config.ts | Default xSchema namespace (siteUrl, siteName, siteLogo, organizationName) with TypeScript type augmentation. |
app/components/X/Schema/ | 16 auto-imported XSchema-prefixed components, one per Schema.org @type. |
app/composables/useSchema.ts | Shared helpers: resolveUrl, normalizeAuthor, normalizeImage, getDefaultOrganization, getDefaultPublisher, useSchemaHead. |
app/types/schema.ts | TypeScript interfaces: Person, Organization, ImageObject, PostalAddress, BreadcrumbItem, FAQ, QAItem, HowToStep, MonetaryAmount, Offer, AggregateRating, Rating, Review, OpeningHoursSpecification, SchemaConfig. |
.playground/ | Dev playground with one demo page per component. |
tests/ | Vitest suite (components.test.ts, useSchema.test.ts). |
Rendering contract
Every component follows the same pattern — no visible <template> block (most are renderless); useSchemaHead(() => schemaObject) inside <script setup lang="ts"> is what produces the JSON-LD. Components are never wrapped in <ClientOnly> — the useHead() injection is SSR-native and must be in the first-byte HTML.
AI Context
package: "@xenterprises/nuxt-x-schema"
version: "0.3.0"
use-when: >
Adding Schema.org structured data to a Nuxt 4 site for SEO and
answer-engine optimization (AEO). 16 renderless XSchema* components
(auto-imported with the XSchema prefix) cover the common @types —
Article, Breadcrumb, Event, FAQ, HowTo, LocalBusiness, Offer,
Organization, Person, Product, QAPage, Review, Service,
SoftwareApplication, VideoObject, WebSite. Each component injects a
<script type="application/ld+json"> tag into <head> via useHead, so
JSON-LD is present in SSR/SSG HTML from the first byte — never behind
<ClientOnly>. Configure branding once under the xSchema namespace in
app.config.ts (siteUrl, siteName, siteLogo, organizationName); the
useSchema() composable exposes resolveUrl(), getDefaultPublisher(),
getDefaultOrganization(), normalizeAuthor(), normalizeImage() for
custom schemas and as the building blocks for the bundled components.
Schema.org type interfaces live in app/types/schema.ts.
