Video Schema Framework

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

When To Use It

Google Search CentralVideo documentation

Use on watch pages where the video is the main content and users can play it. Key moments (Clip or SeekToAction) and the LIVE badge (BroadcastEvent) are added only when the source supports them.

Key Implementation Documentation Highlights

  • Required properties: name, thumbnailUrl, uploadDate.
  • Recommended properties: contentUrl, embedUrl, description, duration, creator (or author), expires, hasPart, interactionStatistic, publication, regionsAllowed or ineligibleRegion.
  • Provide contentUrl (the most effective way for Google to fetch the video file) or embedUrl (the player URL). Neither may be the watch page, and contentUrl must point to a supported video file type, never a data URL.
  • uploadDate and expires are ISO 8601 date-times with timezone; without one Google uses Googlebot's timezone. duration is an ISO 8601 duration such as PT12M34S. Only add expires if the video expires.
  • Thumbnails must be at least 60x30 pixels, crawlable by Googlebot and Googlebot-Image, and served from a stable URL. If creator or author is set, it needs name or alternateName.
  • regionsAllowed and ineligibleRegion take ISO 3166-1 alpha-2 or alpha-3 country codes; use one or the other, not both. interactionStatistic uses WatchAction, LikeAction, CommentAction or ShareAction with an integer userInteractionCount.
  • Key moments: Clip needs name, startOffset, url (endOffset recommended); the clip url must be the video's URL path plus a time parameter, and no two clips may share a startOffset. SeekToAction needs target containing {seek_to_second_number} and startOffset-input set to required name=seek_to_second_number. The video must be at least 30 seconds long and deep linkable.
  • LIVE badge: publication BroadcastEvent needs isLiveBroadcast (true), startDate and endDate. endDate is required once the stream ends; give an approximate one in advance. Notify Google with the Indexing API when the stream starts, ends and whenever the markup changes.
  • Videos can appear in the main results, Video mode, Google Images and Discover. Validate with the Rich Results Test and monitor the Video indexing report in Search Console.

Template Approach

const pageData = {
  title: "How to Shape a Sourdough Boule",
  description: "A baker walks through pre-shaping, bench rest and final shaping for a round sourdough loaf.",
  thumbnail: "https://www.example.com/media/shaping-sourdough-16x9.jpg",
  uploadDate: "2026-03-14T08:00:00-05:00",
  duration: "PT12M34S",
  contentUrl: "https://cdn.example.com/video/shaping-sourdough.mp4"
};

const staticTemplate = {
  "@context": "https://schema.org",
  "@type": "VideoObject",
  "name": pageData.title,
  "description": pageData.description,
  "thumbnailUrl": [pageData.thumbnail],
  "uploadDate": pageData.uploadDate,
  "duration": pageData.duration,
  "contentUrl": pageData.contentUrl
};

Conditional-logic Framework

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

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

function toIsoDate(value) {
  const match = typeof value === "string" ? ISO_DATE.exec(value) : null;
  if (!match) return null;
  const [y, m, d] = match.slice(1).map(Number);
  const date = new Date(Date.UTC(y, m - 1, d));
  return date.getUTCMonth() === m - 1 && date.getUTCDate() === d ? value : null;
}

// Keeps the original timezone offset; rejects naive or unparseable values.
function toIsoDateTime(value) {
  const match = typeof value === "string" ? ISO_DATETIME.exec(value) : null;
  return match && toIsoDate(match[1]) ? value : null;
}

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

// Page gates: indexable, self-canonical, marked-up content visible.
function passesPageGates(page) {
  if (!page || page.noindex || page.robotsBlocked) return false;
  if (!isAbsoluteUrl(page.url)) return false;
  if (page.canonicalUrl && page.canonicalUrl !== page.url) return false;
  return page.contentVisible === true;
}

// Video helpers
const REGION_CODE = /^[A-Z]{2,3}$/;
const VIDEO_FILE = /\.(3gp|3g2|asf|avi|divx|m2v|m3u|m3u8|m4v|mkv|mov|mp4|mpeg|ogv|qvt|ram|rm|vob|webm|wmv|xap)$/i;

// Seconds to ISO 8601 duration, e.g. 754 -> "PT12M34S".
function toIsoDuration(seconds) {
  if (!Number.isInteger(seconds) || seconds <= 0) return null;
  const h = Math.floor(seconds / 3600);
  const m = Math.floor((seconds % 3600) / 60);
  const s = seconds % 60;
  return "PT" + (h ? h + "H" : "") + (m ? m + "M" : "") + (s ? s + "S" : "");
}

function withTimeParam(pageUrl, value) {
  return pageUrl + (pageUrl.includes("?") ? "&" : "?") + "t=" + value;
}

function buildParty(party) {
  if (!party || !party.name) return null; // creator/author needs name
  return {
    "@type": party.type === "Person" ? "Person" : "Organization",
    name: party.name,
    url: isAbsoluteUrl(party.url) ? party.url : null
  };
}

// Key moments, option 1: Clip per chapter (video must be 30s or longer).
function buildClips(chapters, durationSeconds, pageUrl) {
  if (!Number.isInteger(durationSeconds) || durationSeconds < 30) return [];
  const valid = (chapters || [])
    .filter((c) => c && c.name && Number.isInteger(c.startSeconds) && c.startSeconds >= 0 && c.startSeconds < durationSeconds)
    .sort((a, b) => a.startSeconds - b.startSeconds);
  const clips = [];
  const seenStarts = new Set();
  for (const c of valid) {
    if (seenStarts.has(c.startSeconds)) continue; // no two clips may share a start time
    seenStarts.add(c.startSeconds);
    const end = Number.isInteger(c.endSeconds) && c.endSeconds > c.startSeconds && c.endSeconds <= durationSeconds ? c.endSeconds : null;
    clips.push({
      "@type": "Clip",
      name: c.name,
      startOffset: c.startSeconds,
      endOffset: end,
      url: withTimeParam(pageUrl, c.startSeconds) // same URL path as the video
    });
  }
  return clips;
}

// Key moments, option 2: SeekToAction when the player deep links via ?t=.
function buildSeekToAction(source, pageUrl) {
  if (!source.supportsTimeParam || !Number.isInteger(source.durationSeconds) || source.durationSeconds < 30) return null;
  return {
    "@type": "SeekToAction",
    target: withTimeParam(pageUrl, "{seek_to_second_number}"),
    "startOffset-input": "required name=seek_to_second_number"
  };
}

// LIVE badge: BroadcastEvent with startDate and a (possibly approximate) endDate.
function buildBroadcastEvent(live) {
  if (!live) return null;
  const start = toIsoDateTime(live.startDate);
  const end = toIsoDateTime(live.endDate);
  if (!start || !end || Date.parse(end) <= Date.parse(start)) return null;
  return { "@type": "BroadcastEvent", isLiveBroadcast: true, startDate: start, endDate: end };
}

function buildVideoObjectSchema(source) {
  const page = source.page;
  if (!passesPageGates(page)) return null;

  // Required: name, thumbnailUrl, uploadDate (with timezone).
  const thumbnails = (source.thumbnailUrls || []).filter(isAbsoluteUrl);
  const uploadDate = toIsoDateTime(source.uploadDate);
  if (!source.title || !thumbnails.length || !uploadDate) return null;

  // contentUrl must be a supported video file; neither URL may be the watch page.
  const contentUrl = isAbsoluteUrl(source.contentUrl) && source.contentUrl !== page.url
    && VIDEO_FILE.test(new URL(source.contentUrl).pathname) ? source.contentUrl : null;
  const embedUrl = isAbsoluteUrl(source.embedUrl) && source.embedUrl !== page.url ? source.embedUrl : null;

  const expires = toIsoDateTime(source.expires);
  const regionsAllowed = (source.regionsAllowed || []).filter((c) => REGION_CODE.test(c));
  // Use regionsAllowed or ineligibleRegion, never both.
  const ineligibleRegion = regionsAllowed.length ? [] : (source.ineligibleRegion || []).filter((c) => REGION_CODE.test(c));
  const views = Number.isInteger(source.viewCount) && source.viewCount >= 0 ? source.viewCount : null;
  const clips = buildClips(source.chapters, source.durationSeconds, page.url);

  const schema = {
    "@context": "https://schema.org",
    "@type": "VideoObject",
    name: source.title,
    description: source.description,
    thumbnailUrl: thumbnails,
    uploadDate,
    duration: toIsoDuration(source.durationSeconds),
    contentUrl,
    embedUrl,
    expires: expires && Date.parse(expires) > Date.parse(uploadDate) ? expires : null,
    creator: buildParty(source.creator),
    regionsAllowed,
    ineligibleRegion,
    interactionStatistic: views === null ? null : {
      "@type": "InteractionCounter",
      interactionType: { "@type": "WatchAction" },
      userInteractionCount: views
    },
    hasPart: clips,
    potentialAction: clips.length ? null : buildSeekToAction(source, page.url),
    publication: buildBroadcastEvent(source.live)
  };

  return compact(schema) ?? null;
}

// Usage:
// const source = {
//   "page": {
//     "url": "https://www.example.com/videos/shaping-sourdough",
//     "canonicalUrl": "https://www.example.com/videos/shaping-sourdough",
//     "noindex": false,
//     "contentVisible": true
//   },
//   "title": "How to Shape a Sourdough Boule",
//   "description": "A baker walks through pre-shaping, bench rest and final shaping for a round sourdough loaf.",
//   "thumbnailUrls": [
//     "https://www.example.com/media/shaping-sourdough-16x9.jpg",
//     "https://www.example.com/media/shaping-sourdough-4x3.jpg",
//     "https://www.example.com/media/shaping-sourdough-1x1.jpg"
//   ],
//   "uploadDate": "2026-03-14T08:00:00-05:00",
//   "durationSeconds": 754,
//   "contentUrl": "https://cdn.example.com/video/shaping-sourdough.mp4",
//   "embedUrl": "https://www.example.com/embed/shaping-sourdough",
//   "creator": {
//     "type": "Organization",
//     "name": "Example Bakery School",
//     "url": "https://www.example.com/about"
//   },
//   "regionsAllowed": [
//     "US",
//     "CA"
//   ],
//   "viewCount": 18452,
//   "chapters": [
//     {
//       "name": "Pre-shaping the dough",
//       "startSeconds": 30,
//       "endSeconds": 210
//     },
//     {
//       "name": "Bench rest",
//       "startSeconds": 210,
//       "endSeconds": 400
//     },
//     {
//       "name": "Final shaping and banneting",
//       "startSeconds": 400,
//       "endSeconds": 754
//     }
//   ],
//   "live": null
// };
// const schema = buildVideoObjectSchema(source);

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

  • Prevents emitting empty, null, or stale fields that frequently trigger rich result warnings.
  • Supports multiple page states (for example: no rating yet, no image yet, no offers yet) without duplicate templates.
  • Lets engineering teams centralize schema policy checks and validation in reusable code paths.
  • Makes large-scale schema maintenance safer when content models evolve over time.

Property Reference

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

Video Google doc

Required, one of: In creator: if creator is specified, provide name or alternateName.

Required, one of: In author: if author is specified, provide name or alternateName.

Video properties in Google's documentation
PropertyGoogleExpectsNotes
nameRequiredTextThe title of the video.
thumbnailUrlRequiredURL, can repeatA URL to the video's unique thumbnail image. Images at least 60px wide.
uploadDateRequiredDateTimeISO 8601 date and time, with timezone, when the video was first published.
contentUrlRecommendedURLThe URL of the video file itself (not the watch page)
creatorRecommendedPerson or Organization, can repeatThe person or organization that created the video. Or use author.
creator.nameOne ofTextThe name of the creator or organization.
creator.urlRecommendedURLA page that uniquely identifies the creator.
authorRecommendedPerson or Organization, can repeatAccepted alternative to creator: the person or organization that created or published the video. If specified, provide either name or alternateName. Or use creator.
author.nameOne ofTextThe name of the creator or organization.
author.urlRecommendedURLA link to a web page that uniquely identifies the creator or organization (such as a profile page or homepage).
descriptionRecommendedTextA unique description of the video.
durationRecommendedDurationIn ISO 8601 format (e.g. PT1M33S)
embedUrlRecommendedURLThe URL of the video player (not the watch page)
expiresRecommendedDateTimeOnly if the video expires (ISO 8601 with timezone)
hasPartRecommendedClip, can repeatClip items for important video segments (key moments)
ineligibleRegionRecommendedPlace or Text, can repeatIneligibleRegion: use ISO 3166-1 alpha-2 or alpha-3 country codes.
interactionStatisticRecommendedInteractionCounter, can repeatE.g. WatchAction count.
interactionStatistic.interactionTypeRecommendedEnumInteractionType must be WatchAction, LikeAction, CommentAction or ShareAction. One of: WatchAction, LikeAction, CommentAction, ShareAction.
interactionStatistic.userInteractionCountRecommendedIntegerAs an integer.
publicationRecommendedBroadcastEvent, can repeatBroadcastEvent for livestreams (LIVE badge)
regionsAllowedRecommendedPlace or Text, can repeatRegionsAllowed: use ISO 3166-1 alpha-2 or alpha-3 country codes.

Video key moments Google doc

Video key moments properties in Google's documentation
PropertyGoogleExpectsNotes
hasPartRecommendedClip, can repeatClip items to define key moments.
hasPart.nameRequiredTextClip requires name: a descriptive title for the clip.
hasPart.startOffsetRequiredNumberClip requires startOffset in seconds from the start of the video.
hasPart.urlRequiredURLClip requires url pointing to the clip start time on the same URL path as the video.
hasPart.endOffsetRecommendedNumberEndOffset in seconds.
potentialActionRecommendedSeekToActionSeekToAction to enable automatic key moments.
potentialAction.startOffset-inputRequiredTextSeekToAction requires startOffset-input set to 'required name=seek_to_second_number'. One of: required name=seek_to_second_number.
potentialAction.targetRequiredEntryPoint or URLSeekToAction requires target URL containing {seek_to_second_number}.

Video LIVE badge Google doc

Video LIVE badge properties in Google's documentation
PropertyGoogleExpectsNotes
publicationRequiredBroadcastEvent, can repeatLIVE badge requires publication with a BroadcastEvent.
publication.isLiveBroadcastRequiredBooleanPublication.isLiveBroadcast must be true. One of: true.
publication.startDateRequiredDateTimePublication.startDate is required (ISO 8601 DateTime)
publication.endDateRequiredDateTimePublication.endDate is required once the stream has ended; provide an approximate one in advance.

Explore Other Schema Framework Pages

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 Carousel Use when a page lists multiple related entities in a scannable sequence that can be surfaced as cards. ItemList Open 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

Implement Video Schema Frameworks Correctly

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