Introduction
Structured data is how you communicate with search engines. For video pages, Google expects VideoObject markup in JSON-LD format. Without it, your videos won't appear in Google's video carousel or rich results. Here's the generator I built for ViralVidVault.
What Google Expects
Google's documentation requires these fields for VideoObject:
-
name(required) -
description(required) -
thumbnailUrl(required) -
uploadDate(required) -
duration(recommended, ISO 8601) -
contentUrlorembedUrl(recommended) -
interactionStatistic(recommended)
The Generator Class
<?php
class VideoSchemaGenerator
{
public function generateVideoObject(array $video): string
{
$schema = [
'@context' => 'https://schema.org',
'@type' => 'VideoObject',
'name' => $this->sanitize($video['title']),
'description' => $this->sanitize($video['description'] ?? $video['title']),
'thumbnailUrl' => $this->getThumbnailUrl($video),
'uploadDate' => $this->formatDate($video['published_at']),
'embedUrl' => "https://www.youtube.com/embed/{$video['id']}",
'contentUrl' => "https://www.youtube.com/watch?v={$video['id']}",
];
// Duration (convert seconds to ISO 8601)
if (!empty($video['duration'])) {
$schema['duration'] = $this->secondsToIso8601((int)$video['duration']);
}
// Interaction statistics
if (!empty($video['views'])) {
$schema['interactionStatistic'] = [
'@type' => 'InteractionCounter',
'interactionType' => ['@type' => 'WatchAction'],
'userInteractionCount' => (int)$video['views'],
];
}
// Region restrictions if applicable
if (!empty($video['regions'])) {
$schema['regionsAllowed'] = implode(', ', $video['regions']);
}
return '<script type="application/ld+json">' . "\n"
. json_encode($schema, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
. "\n" . '</script>';
}
public function generateItemList(array $videos, string $listName, string $listUrl): string
{
$items = [];
foreach ($videos as $position => $video) {
$items[] = [
'@type' => 'ListItem',
'position' => $position + 1,
'item' => [
'@type' => 'VideoObject',
'name' => $this->sanitize($video['title']),
'url' => "https://viralvidvault.com/watch/{$video['id']}",
'thumbnailUrl' => $this->getThumbnailUrl($video),
'uploadDate' => $this->formatDate($video['published_at']),
],
];
}
$schema = [
'@context' => 'https://schema.org',
'@type' => 'ItemList',
'name' => $listName,
'url' => $listUrl,
'numberOfItems' => count($videos),
'itemListElement' => $items,
];
return '<script type="application/ld+json">' . "\n"
. json_encode($schema, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
. "\n" . '</script>';
}
private function secondsToIso8601(int $seconds): string
{
$hours = intdiv($seconds, 3600);
$minutes = intdiv($seconds % 3600, 60);
$secs = $seconds % 60;
$duration = 'PT';
if ($hours > 0) $duration .= "{$hours}H";
if ($minutes > 0) $duration .= "{$minutes}M";
$duration .= "{$secs}S";
return $duration;
}
private function getThumbnailUrl(array $video): string
{
return $video['thumbnail_url']
?? "https://img.youtube.com/vi/{$video['id']}/maxresdefault.jpg";
}
private function formatDate(string $date): string
{
return date('Y-m-d', strtotime($date));
}
private function sanitize(string $text): string
{
return htmlspecialchars(strip_tags($text), ENT_QUOTES, 'UTF-8');
}
}
Using It in Templates
<!-- watch.php template -->
<?php
$schemaGen = new VideoSchemaGenerator();
echo $schemaGen->generateVideoObject($video);
?>
For category pages with video lists:
<!-- category.php template -->
<?php
$schemaGen = new VideoSchemaGenerator();
echo $schemaGen->generateItemList(
$videos,
listName: "Trending Music Videos",
listUrl: "https://viralvidvault.com/category/music"
);
?>
Validation
Always test your markup with Google's Rich Results Test. Common mistakes:
- Missing thumbnailUrl — Required, not optional
- Wrong duration format — Must be ISO 8601 (PT4M5S, not 245)
- Future uploadDate — Google rejects dates in the future
- HTML in description — Strip tags before inserting into JSON-LD
public function validate(array $schema): array
{
$errors = [];
$required = ['name', 'description', 'thumbnailUrl', 'uploadDate'];
foreach ($required as $field) {
if (empty($schema[$field])) {
$errors[] = "Missing required field: {$field}";
}
}
if (!empty($schema['uploadDate'])) {
$date = strtotime($schema['uploadDate']);
if ($date > time()) {
$errors[] = 'uploadDate is in the future';
}
}
if (!empty($schema['duration']) && !preg_match('/^PT(\d+H)?(\d+M)?(\d+S)?$/', $schema['duration'])) {
$errors[] = 'Invalid ISO 8601 duration format';
}
return $errors;
}
Impact on SEO
After implementing structured data on ViralVidVault, Google Search Console showed:
- Video rich results: Appearing for 340+ video pages
- Click-through rate: 15% higher on pages with rich results vs without
- Impressions: 2x increase in video-related search queries
Proper schema markup is one of those SEO wins that costs nothing but developer time. Every video page on viralvidvault.com now outputs validated VideoObject JSON-LD.
Part of the "Building ViralVidVault" series.
Top comments (0)