Dynamic schema markup with Velo: generating JSON-LD from your database

Module 20: Wix Studio & Velo Advanced SEO | Lesson 251 of 687 | 45 min read

By Michael Andrews, Wix SEO Expert UK

Static structured data only works when your pages are static. The moment you build dynamic pages powered by Wix CMS collections, you need schema markup that generates itself from your data, unique JSON-LD for every product, every FAQ, every event, every review. The Velo wix-seo API includes a setStructuredData method that lets you inject any valid JSON-LD schema into the page head at render time, fully visible to Googlebot. This lesson teaches you to build dynamic schema generators for the most valuable schema types.

How-to diagram showing Wix Studio and Velo advanced SEO capabilities including dynamic meta tags, custom schema markup, CMS database pages, multilingual hreflang, and A/B testing
Wix Studio and Velo unlock advanced SEO capabilities that go far beyond what the standard Wix editor provides.

Understanding setStructuredData in the wix-seo API

The wix-seo module exposes a structuredData property that accepts an array of JSON-LD objects. Each object should be a complete schema.org entity with a @context and @type. When you set this property during page render, Wix injects the JSON-LD into a script tag in the page head, exactly where Google expects to find it. This happens during server-side rendering, so the structured data is present in the initial HTML response.

You can set multiple structured data objects on a single page. A product page might include a Product schema, a BreadcrumbList schema, and an Organization schema. An event listing page might include an Event schema alongside an FAQ schema for common questions about the event. There is no practical limit to the number of schema objects you can include, though each must be valid according to the schema.org specification.

import wixSeo from 'wix-seo';

$w.onReady(function () {
  wixSeo.structuredData = [
    {
      '@context': 'https://schema.org',
      '@type': 'Organization',
      'name': 'YourBusiness',
      'url': 'https://www.yourbusiness.com',
      'logo': 'https://www.yourbusiness.com/logo.png',
      'sameAs': [
        'https://www.facebook.com/yourbusiness',
        'https://www.instagram.com/yourbusiness',
        'https://www.linkedin.com/company/yourbusiness'
      ]
    }
  ];
});

Dynamic Product Schema from Wix Collections

Product schema is the highest-value structured data for e-commerce sites. It enables rich results in Google search including price, availability, review stars, and images directly in the search listing. These rich results dramatically increase click-through rates, often by 20-30% compared to standard blue link results. For Wix stores with hundreds or thousands of products, generating this schema dynamically from your Products collection is essential.

A complete Product schema requires the product name, description, image URLs, SKU or identifier, price and currency, availability status, and brand. Optional but highly recommended fields include aggregateRating (if you have reviews), review (individual review objects), and offers for detailed pricing information. The more complete your schema, the more likely Google is to display rich results.

import wixSeo from 'wix-seo';
import wixData from 'wix-data';

async function generateProductSchema(productSlug) {
  const result = await wixData.query('Products')
    .eq('slug', productSlug)
    .include('reviews')
    .find();

  if (result.items.length === 0) return null;

  const product = result.items[0];

  const schema = {
    '@context': 'https://schema.org',
    '@type': 'Product',
    'name': product.name,
    'description': product.description,
    'image': product.images || [product.mainImage],
    'sku': product.sku,
    'brand': {
      '@type': 'Brand',
      'name': product.brand || 'YourStore'
    },
    'offers': {
      '@type': 'Offer',
      'url': 'https://www.yourstore.com/products/' + product.slug,
      'priceCurrency': product.currency || 'USD',
      'price': product.price.toFixed(2),
      'availability': product.inStock
        ? 'https://schema.org/InStock'
        : 'https://schema.org/OutOfStock',
      'seller': {
        '@type': 'Organization',
        'name': 'YourStore'
      }
    }
  };

  if (product.reviews && product.reviews.length > 0) {
    const totalRating = product.reviews.reduce((sum, r) => sum + r.rating, 0);
    schema.aggregateRating = {
      '@type': 'AggregateRating',
      'ratingValue': (totalRating / product.reviews.length).toFixed(1),
      'reviewCount': product.reviews.length.toString(),
      'bestRating': '5',
      'worstRating': '1'
    };
    schema.review = product.reviews.slice(0, 5).map(r => ({
      '@type': 'Review',
      'author': { '@type': 'Person', 'name': r.authorName },
      'datePublished': r.date,
      'reviewRating': {
        '@type': 'Rating',
        'ratingValue': r.rating.toString(),
        'bestRating': '5'
      },
      'reviewBody': r.text
    }));
  }

  return schema;
}

Auto-Generating FAQ Schema from a CMS Collection

FAQ schema is one of the easiest rich result types to earn and one of the most visually impactful in search results. Each FAQ item expands directly in the search listing, giving your result significantly more real estate on the page. For businesses with FAQ sections, knowledge bases, or Q&A content stored in Wix CMS collections, you can generate FAQPage schema dynamically for every page that displays questions and answers.

The approach is simple: query your FAQ collection filtered by the current page or category, transform each item into a Question entity with an acceptedAnswer, and wrap them in a FAQPage schema object. Google requires at least two question-answer pairs for FAQPage schema to be eligible for rich results. If a page has only one FAQ item, the schema is still valid but unlikely to trigger the rich result.

import wixSeo from 'wix-seo';
import wixData from 'wix-data';

async function generateFaqSchema(categorySlug) {
  const result = await wixData.query('FAQs')
    .eq('category', categorySlug)
    .ascending('sortOrder')
    .limit(20)
    .find();

  if (result.items.length < 2) return null;

  const faqSchema = {
    '@context': 'https://schema.org',
    '@type': 'FAQPage',
    'mainEntity': result.items.map(faq => ({
      '@type': 'Question',
      'name': faq.question,
      'acceptedAnswer': {
        '@type': 'Answer',
        'text': faq.answer
      }
    }))
  };

  return faqSchema;
}

$w.onReady(async function () {
  const currentCategory = wixLocationFrontend.path[0];
  const faqSchema = await generateFaqSchema(currentCategory);

  if (faqSchema) {
    wixSeo.structuredData = [faqSchema];
  }
});
FAQ Content Tip: Keep FAQ answers in your CMS concise but complete, ideally 2-4 sentences each. Google may truncate very long answers in rich results. Store the full answer in the CMS for the page display but consider adding a separate "shortAnswer" field specifically for schema markup that stays under 300 characters.

Event Schema for Dynamic Event Listings

Event schema enables rich results that show event dates, times, locations, and ticket information directly in Google search. For venues, event organizers, or any business that lists events from a Wix CMS collection, dynamic Event schema is extremely valuable. Events have time-sensitive relevance, and Google actively promotes upcoming events in search results and Google Maps.

The Event schema requires a name, startDate in ISO 8601 format, and a location with either a physical address (Place type) or a virtual URL (VirtualLocation type). Recommended fields include description, image, performer, organizer, offers for ticket pricing, and eventStatus to indicate whether the event is scheduled, postponed, or cancelled. For recurring events, each occurrence should have its own Event schema object.

async function generateEventSchema(eventSlug) {
  const result = await wixData.query('Events')
    .eq('slug', eventSlug)
    .find();

  if (result.items.length === 0) return null;

  const event = result.items[0];

  const schema = {
    '@context': 'https://schema.org',
    '@type': 'Event',
    'name': event.title,
    'description': event.description,
    'image': event.coverImage,
    'startDate': new Date(event.startDate).toISOString(),
    'endDate': event.endDate ? new Date(event.endDate).toISOString() : undefined,
    'eventStatus': 'https://schema.org/EventScheduled',
    'eventAttendanceMode': event.isOnline
      ? 'https://schema.org/OnlineEventAttendanceMode'
      : 'https://schema.org/OfflineEventAttendanceMode',
    'location': event.isOnline
      ? {
          '@type': 'VirtualLocation',
          'url': event.virtualUrl
        }
      : {
          '@type': 'Place',
          'name': event.venueName,
          'address': {
            '@type': 'PostalAddress',
            'streetAddress': event.streetAddress,
            'addressLocality': event.city,
            'addressRegion': event.state,
            'postalCode': event.zip,
            'addressCountry': event.country
          }
        },
    'organizer': {
      '@type': 'Organization',
      'name': event.organizerName || 'YourBusiness',
      'url': 'https://www.yourbusiness.com'
    }
  };

  if (event.ticketPrice) {
    schema.offers = {
      '@type': 'Offer',
      'price': event.ticketPrice.toFixed(2),
      'priceCurrency': event.currency || 'USD',
      'url': 'https://www.yourbusiness.com/events/' + event.slug,
      'availability': event.soldOut
        ? 'https://schema.org/SoldOut'
        : 'https://schema.org/InStock',
      'validFrom': new Date(event.ticketSaleStart).toISOString()
    };
  }

  return schema;
}

BreadcrumbList Schema for Navigation Context

BreadcrumbList schema gives Google explicit navigation context for your pages, which can result in breadcrumb-style display in search results instead of the raw URL. This is particularly valuable for deep dynamic pages where the URL alone does not convey the full hierarchy. A product page URL like /products/blue-widget tells Google nothing about the category, but breadcrumb schema can show Home > Clothing > Accessories > Blue Widget.

function generateBreadcrumbSchema(breadcrumbs) {
  return {
    '@context': 'https://schema.org',
    '@type': 'BreadcrumbList',
    'itemListElement': breadcrumbs.map((crumb, index) => ({
      '@type': 'ListItem',
      'position': index + 1,
      'name': crumb.name,
      'item': crumb.url
    }))
  };
}

$w.onReady(async function () {
  const product = await getCurrentProduct();
  const breadcrumbs = [
    { name: 'Home', url: 'https://www.yourstore.com' },
    { name: product.category, url: 'https://www.yourstore.com/shop/' + product.categorySlug },
    { name: product.name, url: 'https://www.yourstore.com/products/' + product.slug }
  ];

  const productSchema = await generateProductSchema(product.slug);
  const breadcrumbSchema = generateBreadcrumbSchema(breadcrumbs);

  wixSeo.structuredData = [productSchema, breadcrumbSchema].filter(Boolean);
});

Performance Considerations and Caching Strategies

Every database query adds latency to your page load. When you generate structured data dynamically, you are adding at least one query, potentially more if your schema pulls from multiple collections. On high-traffic pages, this can affect both user experience and Googlebot crawl efficiency. The key is to minimize the number of queries and keep them fast.

Use indexed fields in your queries. If you filter by slug, ensure the slug field is indexed in your Wix collection settings. Avoid using .include() for referenced collections unless you specifically need that data for your schema. If a product has 200 reviews but you only include 5 in the schema, query the reviews separately with a .limit(5) rather than loading all 200 through the product reference.

Schema Validation Warning: Always validate your dynamically generated schema using the Google Rich Results Test before rolling it out to all pages. Test with several different items from your collection, including items with missing optional fields. A single malformed schema object can prevent Google from processing any structured data on the page, even the valid objects.

Putting It All Together: Multi-Schema Page Setup

Production dynamic pages typically need multiple schema types. A product page might need Product, BreadcrumbList, and Organization schemas. A blog post might need Article, BreadcrumbList, and FAQPage schemas. The pattern is always the same: generate each schema object from your CMS data, filter out any nulls from items with insufficient data, and set them all at once on the structuredData property.

import wixSeo from 'wix-seo';
import wixData from 'wix-data';
import wixLocationFrontend from 'wix-location-frontend';

$w.onReady(async function () {
  const slug = wixLocationFrontend.path[wixLocationFrontend.path.length - 1];

  const [productResult, faqResult] = await Promise.all([
    wixData.query('Products').eq('slug', slug).find(),
    wixData.query('FAQs').eq('productSlug', slug).limit(10).find()
  ]);

  if (productResult.items.length === 0) return;

  const product = productResult.items[0];
  const schemas = [];

  schemas.push({
    '@context': 'https://schema.org',
    '@type': 'Product',
    'name': product.name,
    'description': product.description,
    'image': product.mainImage,
    'sku': product.sku,
    'offers': {
      '@type': 'Offer',
      'price': product.price.toFixed(2),
      'priceCurrency': 'USD',
      'availability': product.inStock
        ? 'https://schema.org/InStock'
        : 'https://schema.org/OutOfStock'
    }
  });

  schemas.push({
    '@context': 'https://schema.org',
    '@type': 'BreadcrumbList',
    'itemListElement': [
      { '@type': 'ListItem', 'position': 1, 'name': 'Home', 'item': 'https://www.yourstore.com' },
      { '@type': 'ListItem', 'position': 2, 'name': product.category, 'item': 'https://www.yourstore.com/shop/' + product.categorySlug },
      { '@type': 'ListItem', 'position': 3, 'name': product.name, 'item': 'https://www.yourstore.com/products/' + product.slug }
    ]
  });

  if (faqResult.items.length >= 2) {
    schemas.push({
      '@context': 'https://schema.org',
      '@type': 'FAQPage',
      'mainEntity': faqResult.items.map(faq => ({
        '@type': 'Question',
        'name': faq.question,
        'acceptedAnswer': {
          '@type': 'Answer',
          'text': faq.answer
        }
      }))
    });
  }

  wixSeo.structuredData = schemas;
});


Complete How-To Guide: Generating Dynamic Schema Markup with Velo

This guide covers building dynamic JSON-LD schema generators for the most valuable schema types including Product, FAQ, Event, and BreadcrumbList, all powered by your Wix CMS data.

How to build dynamic schema generators for Wix CMS pages

How to Generate and Validate Dynamic JSON-LD Schema with Velo

Building dynamic schema that pulls from your Wix CMS requires Velo code and systematic testing. These steps walk you through creating, testing, and maintaining dynamic structured data on your Wix site.

How to implement dynamic JSON-LD schema markup using Velo on Wix

Schema Performance: Every database query adds latency. For high-traffic pages, minimise queries by using indexed fields in .eq() filters, limiting review queries with .limit(5), and using .fields() projections to retrieve only the columns needed for schema generation. Consider caching pre-computed schema JSON in a dedicated CMS field for your highest-traffic pages.

This lesson on Dynamic schema markup with Velo: generating JSON-LD from your database is part of Module 20: Wix Studio & Velo Advanced SEO in The Most Comprehensive Complete Wix SEO Course in the World (2026 Edition). Created by Michael Andrews, the UK's No.1 Wix SEO Expert with 14 years of hands-on experience, 750+ completed Wix SEO projects and 425+ verified five-star reviews.