---
title: "Video SEO"
slug: "video-seo"
category: "seo"
tags: ["seo", "video", "youtube", "schema-org", "transcripts", "captions"]
status: "stable"
last_updated: 2026-05-15
summary: "How to rank video content with VideoObject schema, on-page transcripts, dual hosting on YouTube and an owned domain, and a video sitemap."
related: ["[[seo/structured-data]]", "[[seo/schema-markup-deep]]", "[[seo/serp-features]]", "[[seo/glossary]]", "[[seo/audit-checklist]]"]
---

## 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.

```html
<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 [[seo/structured-data]] for the general JSON-LD rules and [[seo/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`.

```html
<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`.

```xml
<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 [[seo/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.

## Related

- [[seo/structured-data]]
- [[seo/schema-markup-deep]]
- [[seo/serp-features]]
- [[seo/glossary]]
- [[seo/audit-checklist]]
