DEV Community

ahmet gedik
ahmet gedik

Posted on

Building VideoObject Schema Markup Generator in PHP

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)
  • contentUrl or embedUrl (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');
    }
}
Enter fullscreen mode Exit fullscreen mode

Using It in Templates

<!-- watch.php template -->
<?php
$schemaGen = new VideoSchemaGenerator();
echo $schemaGen->generateVideoObject($video);
?>
Enter fullscreen mode Exit fullscreen mode

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"
);
?>
Enter fullscreen mode Exit fullscreen mode

Validation

Always test your markup with Google's Rich Results Test. Common mistakes:

  1. Missing thumbnailUrl — Required, not optional
  2. Wrong duration format — Must be ISO 8601 (PT4M5S, not 245)
  3. Future uploadDate — Google rejects dates in the future
  4. 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;
}
Enter fullscreen mode Exit fullscreen mode

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)