Carousel Schema Framework

Use conditional-logic schema generation to keep Carousel markup aligned with real page state and avoid stale or invalid static templates.

When To Use It

Google Search CentralCarousel documentation

Use when a page lists several items of one supported type (courses, movies, recipes, or restaurants) that Google can show as a host carousel, a list-like result people can swipe through on mobile devices. ItemList markup alone is not enough: it is only eligible when combined with Course list, Movie, Recipe, or Restaurant markup.

Key Implementation Documentation Highlights

  • Required on ItemList: itemListElement, with at least two ListItem entries. Google lists no recommended properties for the list itself.
  • Required on each ListItem: position, a 1-based integer. Items are shown in position order.
  • Each ListItem needs either url or item, depending on the page pattern. Summary page: url is the canonical URL of the item detail page; all URLs must be unique and live on the same domain. All-in-one page: item holds the full object with item.name, item.url (the current page URL plus an anchor to the visible item), and the properties of its own type.
  • Supported item types: Course (course list), Movie, Recipe, and Restaurant. All items in one list must be the same type, and each must meet that type's own required properties.
  • Availability depends on the type: the course list is available in English in all regions where Google Search is available; the movie carousel is only available on mobile devices; the restaurant carousel is limited to a small set of providers who register interest through a Google form.
  • The carousel markup must contain all the items listed on the page, and the visible text must be similar to the structured data.
  • Use the Rich Results Test and URL Inspection to validate rendering, eligibility, and crawlability.

Rules the framework enforces before it emits a list:

  • Never mix patterns: either every ListItem has url (summary) or every ListItem has item (all-in-one).
  • Positions are assigned 1..n from display order, so they are unique and increase by 1.
  • Summary pattern: detail URLs are absolute, unique, and share one domain.
  • All-in-one pattern: item @type is Course, Movie, Recipe, or Restaurant for every item, each anchor is unique, and each item carries its type's required properties (name and image for Movie, Recipe, and Restaurant; name and description for Course).

Template Approach

const pageData = {
  recipeUrls: [
    "https://www.example.com/recipes/cacio-e-pepe",
    "https://www.example.com/recipes/pasta-al-limone",
    "https://www.example.com/recipes/pasta-e-ceci"
  ]
};

const staticTemplate = {
  "@context": "https://schema.org",
  "@type": "ItemList",
  "itemListElement": [
    { "@type": "ListItem", "position": 1, "url": pageData.recipeUrls[0] },
    { "@type": "ListItem", "position": 2, "url": pageData.recipeUrls[1] },
    { "@type": "ListItem", "position": 3, "url": pageData.recipeUrls[2] }
  ]
};

Conditional-logic Framework

const ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
const ISO_DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})T([01]\d|2[0-3]):[0-5]\d(:[0-5]\d(\.\d+)?)?(Z|[+-]\d{2}:\d{2})$/;

function isAbsoluteUrl(value) {
  if (typeof value !== "string") return false;
  try {
    const url = new URL(value);
    return (url.protocol === "https:" || url.protocol === "http:") && Boolean(url.hostname);
  } catch {
    return false;
  }
}

function isRealDate(y, m, d) {
  const date = new Date(Date.UTC(Number(y), Number(m) - 1, Number(d)));
  return date.getUTCMonth() === Number(m) - 1 && date.getUTCDate() === Number(d);
}

// Calendar date (YYYY-MM-DD); a full date-time is cut to its date part.
function toIsoDate(value) {
  if (typeof value !== "string") return null;
  const v = value.trim();
  const m = v.match(ISO_DATE) || v.match(ISO_DATE_TIME);
  return m && isRealDate(m[1], m[2], m[3]) ? v.slice(0, 10) : null;
}

// Date-time with Z or an offset (kept as written), or a plain date.
// A date-time without a timezone is rejected instead of guessed.
function toIsoDateTime(value) {
  if (typeof value !== "string") return null;
  const v = value.trim();
  const m = v.match(ISO_DATE) || v.match(ISO_DATE_TIME);
  return m && isRealDate(m[1], m[2], m[3]) ? v : null;
}

// Drop null, undefined, "", [] and {} recursively.
function compact(value) {
  if (Array.isArray(value)) {
    const items = value.map(compact).filter((v) => v !== undefined);
    return items.length ? items : undefined;
  }
  if (value && typeof value === "object") {
    const out = {};
    for (const [key, v] of Object.entries(value)) {
      const c = compact(v);
      if (c !== undefined) out[key] = c;
    }
    return Object.keys(out).length ? out : undefined;
  }
  if (value === null || value === undefined || value === "") return undefined;
  return value;
}

// Page gates: indexable, self-canonical, and the marked-up content is visible.
function passesPageGates(source) {
  if (!source.isIndexable || !source.isContentVisible) return false;
  if (!isAbsoluteUrl(source.url)) return false;
  return !source.canonicalUrl || source.canonicalUrl === source.url;
}

// Host carousels only support these item types, each with its own required fields.
const REQUIRED_BY_TYPE = {
  Course: ["name", "description"],
  Movie: ["name", "image"],
  Recipe: ["name", "image"],
  Restaurant: ["name", "image"]
};

function hostOf(url) {
  return isAbsoluteUrl(url) ? new URL(url).hostname : null;
}

// Summary page: every item links to its own detail page, all on one domain.
// All-in-one page: every item is described on this page under its own anchor.
function chooseCarouselPattern(source) {
  const items = source.items || [];
  const hosts = new Set(items.map((i) => hostOf(i.detailUrl)));
  if (items.length && hosts.size === 1 && !hosts.has(null)) return "summary";
  if (items.length && items.every((i) => i.anchor && i.properties)) return "allInOne";
  return null;
}

function buildSummaryElements(items) {
  const urls = items.map((i) => i.detailUrl);
  if (new Set(urls).size !== urls.length) return null; // every url must be unique
  return items.map((i, index) => ({ "@type": "ListItem", "position": index + 1, "url": i.detailUrl }));
}

function buildAllInOneElements(source) {
  const itemType = source.itemType;
  const required = REQUIRED_BY_TYPE[itemType];
  if (!required) return null; // Course, Movie, Recipe or Restaurant only
  const anchors = source.items.map((i) => i.anchor);
  if (new Set(anchors).size !== anchors.length) return null; // one unique anchor per item

  const elements = [];
  for (const [index, i] of source.items.entries()) {
    // item.url is this page plus the anchor of the visible item.
    const item = compact({ "@type": itemType, ...i.properties, "url": `${source.url}#${i.anchor}` });
    if (!item || required.some((key) => item[key] === undefined)) return null;
    if (item.image !== undefined && ![].concat(item.image).every((u) => isAbsoluteUrl(typeof u === "string" ? u : u.url))) return null;
    elements.push({ "@type": "ListItem", "position": index + 1, "item": item });
  }
  return elements;
}

function buildCarouselSchema(source) {
  if (!passesPageGates(source)) return null;
  const items = source.items || [];
  if (items.length < 2) return null; // an ItemList needs at least two ListItems

  const pattern = chooseCarouselPattern(source);
  const itemListElement =
    pattern === "summary" ? buildSummaryElements(items) :
    pattern === "allInOne" ? buildAllInOneElements(source) :
    null; // never mix patterns in one list
  if (!itemListElement) return null;

  return compact({
    "@context": "https://schema.org",
    "@type": "ItemList",
    "itemListElement": itemListElement
  }) || null;
}

// Usage
// const source = {
//   "isIndexable": true,
//   "isContentVisible": true,
//   "url": "https://www.example.com/collections/weeknight-pasta",
//   "canonicalUrl": "https://www.example.com/collections/weeknight-pasta",
//   "itemType": "Recipe",
//   "items": [
//     {
//       "anchor": "cacio-e-pepe",
//       "properties": {
//         "name": "Cacio e Pepe",
//         "image": [
//           "https://www.example.com/img/cacio-e-pepe-16x9.jpg"
//         ],
//         "author": {
//           "@type": "Person",
//           "name": "Lena Russo"
//         },
//         "datePublished": "2026-03-02",
//         "totalTime": "PT25M"
//       }
//     },
//     {
//       "anchor": "pasta-al-limone",
//       "properties": {
//         "name": "Pasta al Limone",
//         "image": [
//           "https://www.example.com/img/pasta-al-limone-16x9.jpg"
//         ],
//         "author": {
//           "@type": "Person",
//           "name": "Lena Russo"
//         },
//         "datePublished": "2026-04-18",
//         "totalTime": "PT20M"
//       }
//     },
//     {
//       "anchor": "pasta-e-ceci",
//       "properties": {
//         "name": "Pasta e Ceci",
//         "image": [
//           "https://www.example.com/img/pasta-e-ceci-16x9.jpg"
//         ],
//         "author": {
//           "@type": "Person",
//           "name": "Marco Bianchi"
//         },
//         "datePublished": "2026-05-09",
//         "totalTime": "PT35M"
//       }
//     }
//   ]
// };
// const jsonLd = buildCarouselSchema(source); // null means: emit nothing

The Output As An Entity Graph

The conditional-logic output above, parsed in your browser and drawn as the graph a crawler builds from it. Every nested object is a node and the property that holds it is the edge. Tap a node to inspect its properties, or drag to rearrange.

Why Conditional Logic Is Better Than Static Templates

  • Picks the summary or all-in-one pattern from the data and never mixes the two in one list.
  • Keeps the list complete and in display order as items are added, removed, or reordered, with positions recomputed every time.
  • Checks each item against its own type's required properties instead of emitting a list Google cannot use.
  • Makes large-scale schema maintenance safer when content models evolve over time.

Property Reference

What Google's Carousel documentation requires and recommends, property by property. These are the same rules the SchemaCDN validator checks. Each name links to its schema.org definition.

Required, one of: In itemListElement: each ListItem needs url (summary page) or item (all-in-one page).

Carousel properties in Google's documentation
PropertyGoogleExpectsNotes
itemListElementRequiredListItem, can repeatAn ItemList must contain at least two ListItem elements.
itemListElement.positionRequiredIntegerThe item's position in the carousel (1-based).
itemListElement.urlRequiredURLThe canonical URL of the item detail page. All URLs must be unique and on the same domain.
itemListElement.itemRequiredCourse, Movie, Recipe, Restaurant or ThingThe full object for this list item (all-in-one page pattern).

Explore Other Schema Framework Pages

Course Use for educational offerings where curriculum details, provider identity, and delivery context are explicit. Course Open Dataset Use for dataset landing pages so datasets are discoverable in Google Dataset Search with license, creator and download details. Dataset Open Discussion Forum Use for forum and community threads where a primary post and subsequent replies are publicly visible. DiscussionForumPosting Open Limited Education Q&A Use for educational question-answer pages where a single canonical answer and supporting context are visible. Quiz Open Employer Aggregate Rating Use on pages that publish employer rating aggregates from a clear and policy-compliant review source. EmployerAggregateRating Open Retired Estimated Salary Use for compensation insight pages that present transparent salary ranges for specific occupations and locations. Occupation Open Event Use for upcoming events with a real-world or virtual occurrence, schedule, and location context. Event Open Retired Fact Check Use for fact-checking pages that evaluate a specific claim with a transparent rating methodology. ClaimReview Open Retired Home Activities Use for virtual classes and activities where participation details and timing are clearly listed. Event Open Retired How-To Use for instructional content that teaches a process through ordered steps and actionable detail. HowTo Open Image Metadata Use on image pages and image hosts that expose ownership and licensing information to search engines. ImageObject Open Job Posting Use for active job listings that include role details, hiring entity, and location/salary context. JobPosting Open Retired Learning Video Use for educational videos where the learning objective and pedagogical structure are explicit. LearningResource Open Local Business Use for physical businesses serving local customers with publicly verifiable contact and location details. LocalBusiness Open Loyalty Program Use to publish a loyalty program and its tiers once at the organization level, so member prices and member shipping can reference them. MemberProgram Open Math Solvers Use for pages that solve mathematical expressions and present step-by-step computational guidance. MathSolver Open Merchant Listing Use on pages where a product can be bought, with price, availability, shipping and return details. Product Open Limited Movie Use for movie detail pages with canonical film information and transparent metadata attribution. Movie Open Organization Use for official organization pages to clarify brand identity and authoritative entity signals. Organization Open Retired Practice Problems Use for educational practice content that presents solvable problems and answer validation structure. Quiz Open Product Snippet Use for product pages where pricing, availability, and product identity are visible to users. Product Open Product Variants Use when one product comes in variants such as size or color, grouped with ProductGroup. ProductGroup Open Profile Page Use for creator or contributor profile pages where identity attributes and ownership are explicit. ProfilePage Open Q&A Use for pages where users submit questions and community or editorial answers are displayed. QAPage Open Recipe Use for recipe pages with complete ingredients, instructions, and preparation details. Recipe Open Return Policy Use to publish a merchant return policy once at the organization level, with offer-level overrides. MerchantReturnPolicy Open Review Snippet Use when a page publishes genuine reviews tied to a clearly identified product, service, or entity. Review Open Shipping Policy Use to publish shipping rates, delivery times and destinations once at the organization level. ShippingService Open Site Name Use on the home page so Google shows the preferred site name in search results. WebSite Open Retired Sitelinks Search Box Use on sites with a functioning internal search endpoint that can accept URL query parameters. WebSite Open Software App (Beta) Use for software product pages that describe platform support and application category clearly. SoftwareApplication Open Limited Speakable Use for news-style pages where specific text segments are appropriate for text-to-speech playback. SpeakableSpecification Open Subscription and Paywalled Content Use for pages with restricted sections so Google can distinguish free previews from protected content. CreativeWork Open Limited Vacation Rental Use for rental property detail pages where booking-relevant amenities and location signals are explicit. VacationRental Open Retired Vehicle Listing Use for inventory pages selling vehicles with standardized listing fields and offer information. Car Open Video Use for pages where a primary video is embedded and key media metadata is visible on-page. VideoObject Open Article Use for editorial pages where the visible content is a complete article with a clear byline and publication timestamps. Article Open Limited Book Actions Use when your page helps users read, preview, or purchase books through clearly described actions. Book Open Breadcrumb Use on pages that belong to a clear hierarchy and display navigational trail links users can follow. BreadcrumbList Open

Implement Carousel Schema Frameworks Correctly

Get implementation support for conditional schema architecture, validation, and deployment governance.