Overview

Video ranks in three places at once: the web SERP video carousel, the Google Videos vertical, and YouTube search. Each surface reads different signals, but all three reward the same on-page work. Ship a VideoObject JSON-LD block, a full transcript in the page body, captions on the player, and a video sitemap entry. Skip any of those and the video stays invisible to crawlers even when it plays fine for users.

Emit VideoObject JSON-LD with the required fields

Every page that hosts a video carries a VideoObject schema block in the same HTML response as the player. The four required fields are name, description, thumbnailUrl, and uploadDate. Add duration (ISO 8601), contentUrl or embedUrl, and transcript whenever they exist.

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "VideoObject",
  "name": "How to ship a static site to Cloudflare Pages",
  "description": "Twelve-minute walkthrough from git push to live URL.",
  "thumbnailUrl": "https://example.com/thumbs/cf-pages.jpg",
  "uploadDate": "2026-05-10T09:00:00-07:00",
  "duration": "PT12M14S",
  "contentUrl": "https://example.com/videos/cf-pages.mp4",
  "embedUrl": "https://www.youtube.com/embed/abc123",
  "transcript": "Open the terminal and run npm create cloudflare..."
}
</script>

Validate every block with the Schema.org Rich Results Test before deploy. A missing thumbnailUrl or a malformed duration drops the video from the carousel silently. See structured-data for the general JSON-LD rules and schema-markup-deep for the field reference.

Put the full transcript in the page body, not behind a toggle

Transcripts in the visible HTML do three jobs: Google reads them for keyword surface, AI search engines cite the sentences directly, and screen readers consume them when the player is inaccessible. Hide the transcript behind a JavaScript accordion and the crawler still finds it, but AI engines that score “first viewable content” weight it lower.

  • Keep the transcript inline below the player.
  • Use <h2>Transcript</h2> so the section is part of the page outline.
  • Time-stamp every 30 to 60 seconds so the transcript doubles as a chapter index.
  • Repeat the transcript text in the transcript field of VideoObject for engines that ignore body text.

Host on YouTube and on the owned domain when traffic matters

YouTube wins on discovery; the owned domain wins on session attribution and on-site engagement. For any video where organic traffic is a goal, ship both copies.

  • YouTube hosts the canonical embed and accrues YouTube search traffic.
  • The owned domain hosts an <video> tag with controls, poster, and a <track> element for captions.
  • Both copies cite the same transcript and the same VideoObject schema (with different embedUrl and contentUrl values per copy).
  • The owned-domain page is the canonical for web SERP ranking; the YouTube URL competes for the YouTube SERP.

Embed YouTube only when the page does not need to rank for web search; the YouTube iframe gives Google a weaker on-page signal than a self-hosted <video> tag with structured data.

Ship captions as a <track> element and as a .vtt file

Captions are accessibility and crawl signal at once. Provide a .vtt (WebVTT) file referenced from a <track> element with kind="captions" and the correct srclang.

<video controls poster="/thumbs/cf-pages.jpg">
  <source src="/videos/cf-pages.mp4" type="video/mp4" />
  <track kind="captions" src="/captions/cf-pages.en.vtt" srclang="en" label="English" default />
</video>

Auto-generated captions from YouTube are a starting point, not a shipping artifact. Edit them for accuracy before publishing; misspelled captions teach the crawler the wrong terms.

Submit a video sitemap variant

A standard sitemap entry tells Google the page exists; a video sitemap tells Google what the video is. Either ship a separate video-sitemap.xml or extend the main sitemap with the video namespace. Required fields: video:thumbnail_loc, video:title, video:description, and either video:content_loc or video:player_loc.

<url>
  <loc>https://example.com/cf-pages</loc>
  <video:video>
    <video:thumbnail_loc>https://example.com/thumbs/cf-pages.jpg</video:thumbnail_loc>
    <video:title>How to ship a static site to Cloudflare Pages</video:title>
    <video:description>Walkthrough from git push to live URL.</video:description>
    <video:content_loc>https://example.com/videos/cf-pages.mp4</video:content_loc>
    <video:duration>734</video:duration>
  </video:video>
</url>

Reference the video sitemap from robots.txt and resubmit on every new video. See audit-checklist for the verification list.

Common errors

  • Schema mismatch. The thumbnail in JSON-LD points to a different image than the one Open Graph and the player use. Pick one thumbnail URL and reuse it everywhere.
  • Transcript hidden in a JS-rendered modal. Crawlers may not execute the toggle; AI engines weight it lower. Put the transcript in the initial HTML.
  • Missing uploadDate or wrong timezone offset. Carousels drop the video. Use ISO 8601 with the offset, not a bare date.
  • Burned-in captions instead of a <track> file. Burned-in text is unreadable to crawlers and screen readers. Use WebVTT.
  • Auto-translated captions shipped without editorial review. Carries the same risk as auto-translated body copy; demotion is silent.
  • Single hosting on YouTube with no owned-domain copy when the page targets web search. The web SERP carousel rarely picks bare YouTube embeds on third-party pages.