VideoSchema
Outputs a VideoObject with thumbnail, upload date, duration, video URLs and view count; use it on pages whose main content is a video.
VideoSchema describes one video on the page. It outputs a
VideoObject and targets Google’s
video rich results
(thumbnail, duration and upload date in search results, the Videos tab and Discover). Google
needs a way to fetch the video: the file itself (contentUrl), a player (embedUrl) or both.
Usage
A tutorial page with an embedded YouTube video:
---
// src/pages/videos/[slug].astro
import { getCollection } from 'astro:content';
import { VideoSchema } from '@casoon/astro-structured-data/components';
export async function getStaticPaths() {
const videos = await getCollection('videos');
return videos.map((video) => ({ params: { slug: video.id }, props: { video } }));
}
const { data } = Astro.props.video;
// data.youtubeId: 'abc123xyz00', data.views: 15420
---
<VideoSchema
name={data.title}
description={data.description}
thumbnailUrl={`https://i.ytimg.com/vi/${data.youtubeId}/maxresdefault.jpg`}
uploadDate={data.uploadDate}
duration={data.duration}
embedUrl={`https://www.youtube.com/embed/${data.youtubeId}`}
interactionCount={data.views}
publisher={{ name: 'Example Media', logoUrl: 'https://example.com/logo.png' }}
/>
<iframe src={`https://www.youtube.com/embed/${data.youtubeId}`} title={data.title} allowfullscreen></iframe>
Output
On /videos/server-islands/, with uploadDate: '2026-06-01T10:00:00+02:00' and
duration: 'PT8M45S':
{
"@context": "https://schema.org",
"@type": "VideoObject",
"name": "Astro server islands tutorial",
"description": "Learn how to use server islands in Astro.",
"thumbnailUrl": [
"https://i.ytimg.com/vi/abc123xyz00/maxresdefault.jpg"
],
"uploadDate": "2026-06-01T10:00:00+02:00",
"duration": "PT8M45S",
"embedUrl": "https://www.youtube.com/embed/abc123xyz00",
"interactionStatistic": {
"@type": "InteractionCounter",
"interactionType": {
"@type": "WatchAction"
},
"userInteractionCount": 15420
},
"publisher": {
"@type": "Organization",
"name": "Example Media",
"logo": {
"@type": "ImageObject",
"url": "https://example.com/logo.png"
}
}
}
thumbnailUrlis always output as an array.interactionCountbecomes anInteractionCounterwithWatchAction, the count as number.publisherbecomes anOrganization;logoUrlis output aslogo(ImageObject).
Self-hosted video
A video file served by the site itself uses contentUrl. expires tells search engines when
the video is no longer available:
<VideoSchema
name="Launch event recap"
description="Highlights from the launch event."
thumbnailUrl={['/videos/launch-recap-16x9.jpg', '/videos/launch-recap-4x3.jpg']}
uploadDate="2026-09-15"
duration="PT2M30S"
contentUrl="/videos/launch-recap.mp4"
expires="2027-09-15"
/>
{
"@context": "https://schema.org",
"@type": "VideoObject",
"name": "Launch event recap",
"description": "Highlights from the launch event.",
"thumbnailUrl": [
"/videos/launch-recap-16x9.jpg",
"/videos/launch-recap-4x3.jpg"
],
"uploadDate": "2026-09-15",
"duration": "PT2M30S",
"contentUrl": "/videos/launch-recap.mp4",
"expires": "2027-09-15"
}
Root-relative URLs are written as given, not resolved against siteUrl. Pass absolute URLs
if you want absolute URLs in the JSON-LD.
Props
| Prop | Type | Required | Default / fallback | Description |
|---|---|---|---|---|
id |
string |
No | – | Output as the video’s @id, so other nodes can reference it |
name |
string |
Yes | – | Video title |
description |
string |
Yes | – | Video description |
thumbnailUrl |
string | string[] (URL or /path) |
Yes | – | Thumbnail image(s), output as an array |
uploadDate |
string | Date |
Yes | – | Date of first publication, ISO 8601 |
duration |
string (ISO 8601 duration) |
No (recommended) | – | Length, e.g. 'PT8M45S' |
contentUrl |
string (URL or /path) |
One of contentUrl / embedUrl (recommended) |
– | URL of the video file |
embedUrl |
string (URL or /path) |
One of contentUrl / embedUrl (recommended) |
– | URL of the player, e.g. https://www.youtube.com/embed/… |
interactionCount |
number | string |
No (recommended) | – | View count; a whole number, as a string digits only |
expires |
string | Date |
No | – | Date after which the video is no longer available; not before uploadDate |
publisher |
{ name: string; logoUrl?: string; id?: string } |
No (recommended) | – | Publishing organisation; logoUrl URL or /path; id output as @id |
isPartOf |
{ type?, id?, name?, headline?, url? } or an array |
No | – | Reference by id or url; type is free text, default 'CreativeWork' |
hasPart |
{ type?, id?, name?, headline?, url?, position? }[] |
No | – | Reference by id or url; type is free text, default 'CreativeWork' |
changefreq, priority |
sitemap hints | No | – | See Meta tags and sitemap hints |
“URL or /path” means an absolute http(s) URL or a root-relative path. publisher allows
no other fields.
Rules
Violations fail the build (see Validation):
- At least one of
contentUrlandembedUrlis required (Either contentUrl or embedUrl is required for Google video indexing). expiresmust not be beforeuploadDate. Dates are ISO 8601 orDateobjects; aDateis output as an ISO date-time in UTC.durationis an ISO 8601 duration (PT8M45S,PT1H2M).- Text fields must not be empty.
The component uses no integration options. Recommended fields (duration, contentUrl,
embedUrl, interactionStatistic, publisher) are reported after the build when missing,
unless warnOnMissingRecommended is off. Both contentUrl and embedUrl count as
recommended, so a video with only one of them gets a warning for the other.
With generateMeta, a VideoSchema can drive the page’s meta tags, including
video:duration (see Meta tags and sitemap hints).